watermarks-remover README

The project's README from github.com/guillaumemeyer/watermarks-remover. As one markdown file: README.md.

_ _ _ ____ ___ ____ ____ _  _ ____ ____ _  _ ____    ____ ____ _  _ ____ _  _ ____ ____
| | | |__|  |  |___ |__/ |\/| |__| |__/ |_/  [__  __ |__/ |___ |\/| |  | |  | |___ |__/
|_|_| |  |  |  |___ |  \ |  | |  | |  \ | \_ ___]    |  \ |___ |  | |__|  \/  |___ |  \

watermarks-remover

CI Release Stars Forks

Agent skill + stdlib Python service to strip multi-vendor AI provenance marks from text and files — for privacy and hygiene on content you own. The skill is a thin client: it drives the machinery over HTTP, so the agent host needs no Python.

Layer Target How
A Invisible Unicode, exotic spaces, bidi, tag chars Deterministic Python scripts
B Statistical (token-sampling) text watermarks Agent rewrite + optional rewrite_text.py hook
Files C2PA / EXIF / XMP / doc props PNG, JPEG, WebP, AVIF, HEIC, BMP, GIF, TIFF, SVG, PDF, DOCX, XLSX, PPTX, EPUB, ODT, HTML, Markdown, MP4/MOV/M4A/M4V, WAV, MP3, FLAC

Vendors / ecosystems (class-level): Claude, Gemini / SynthID-Text, OpenAI provenance surfaces, open-LLM Kirchenbauer-style (green-list) and keyed-Gumbel / EXP (Aaronson) marks.

Latest release: v0.7.0

Skill path: skills/remove-ai-marks/
Service path: service/
(migration: formerly remove-claude-marks; slash alias /remove-claude-marks still documented)

Install (agent skill)

The skill ships no code — it calls the service over HTTP. Install the skill (markdown only) and start the service, then set WATERMARKS_SERVICE_URL if it is not http://127.0.0.1:8765.

In Claude Code, the fastest route is the bundled plugin marketplace — no clone, and it updates in place. Everywhere else, one installer covers every supported host (Python 3.10+ stdlib, no dependencies):

python3 install_skill.py --skill remove-ai-marks --target claude-code
Host Target Lands in
Claude Code (personal) --target claude-code ~/.claude/skills/<skill> (honors CLAUDE_CONFIG_DIR)
Claude Code (project) --target claude-project --project-dir PATH PATH/.claude/skills/<skill>
Cowork, claude.ai, cloud sessions, routines --target cowork dist/<skill>.zip to upload under Customize → Skills
Cursor --target cursor (default) ~/.cursor/skills/<skill>

Shipped skills: remove-ai-marks (full, service-backed) and clean-user-facing-text (text only, self-contained). --list prints them. Existing installations are preserved unless you pass --force; replacement is staged first and the previous install is kept as a uniquely named backup. --link symlinks this checkout instead of copying, so edits are picked up live. On Windows, use py install_skill.py ...; the install-skill.sh wrapper is provided for macOS/Linux shells.

Before writing anything, the installer validates the skill against the Agent Skills packaging rules that claude.ai uploads and the Skills API enforce: spec-only frontmatter (name, description, license, compatibility, metadata, allowed-tools), a lowercase hyphenated name of at most 64 characters matching the directory, a non-empty description of at most 1024 characters. The Cowork bundle additionally has to fit the 30 MB upload limit, which the packager enforces.

Automatic cleaning via hook (deterministic)

A skill is an instruction: the model decides whether to invoke it, and the model is the thing producing the marks. A hook is executed by the harness on every matching tool call, cooperation not required. That makes the hook the deterministic half of this workflow.

The plugin registers a PostToolUse hook on Write|Edit|MultiEdit|NotebookEdit that runs service/scripts/hook_written_file.py against the file the agent just wrote. Two modes, matching the pre-commit convention of check-by-default:

Mode Behaviour
check (default) Reports provenance marks, leaves the file alone. Findings go to the model (exit 2), so it can offer to clean them.
clean Strips the marks in place, then tells the model the file on disk changed.

Set the mode from the plugin's settings (Hook mode in /plugin manage, read by the hook as CLAUDE_PLUGIN_OPTION_HOOK_MODE), or with WATERMARKS_HOOK_MODE=clean in the environment. The hook command deliberately does not interpolate ${user_config.hook_mode}: Claude Code refuses to run a hook that references an option the user has never opened /plugin manage to set — a declared default does not satisfy it — so interpolating it would mean the hook silently never runs on a fresh install. Detection reuses audit_lib's scan_file / is_actionable, so the hook, the pre-commit gate, and the CI SARIF export agree on what counts as actionable; cleaning shells out to clean_file.py, so no cleaning logic is duplicated. clean mode writes to a sibling temp file and swaps only on a real difference, so files that were already clean keep their mtime and don't retrigger file watchers.

Without the plugin, wire it in ~/.claude/settings.json (or a project .claude/settings.json) yourself:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit|NotebookEdit",
        "hooks": [
          {
            "type": "command",
            "command": "python3",
            "args": ["/path/to/watermarks-remover/service/scripts/hook_written_file.py",
                     "--mode", "check"],
            "timeout": 30
          }
        ]
      }
    ]
  }
}

On Windows, replace python3 with py.

What a hook cannot do. No hook can rewrite the assistant's chat message before you read it. Claude Code's Stop hook receives last_assistant_message read-only, and there is no pre-send filter for final responses — the same limit this project already documents for Cursor rules. So the deterministic guarantee covers files the agent writes, plus the pre-commit gate for anything on its way into git. Text that only ever exists in the chat transcript still depends on the skill workflow, which is model-instruction-based and therefore best-effort.

Claude Code plugin (marketplace)

The repository is also a Claude Code plugin and a single-plugin marketplace (.claude-plugin/), so both skills install and update in two commands, no clone or script required:

/plugin marketplace add guillaumemeyer/watermarks-remover
/plugin install watermarks-remover@watermarks-remover

The skills then load namespaced: /watermarks-remover:remove-ai-marks and /watermarks-remover:clean-user-facing-text (the bare /remove-ai-marks also works when nothing else claims the name). /plugin marketplace update watermarks-remover pulls later versions. The same works from the CLI with claude plugin marketplace add … / claude plugin install …, and from a local checkout by passing a path instead of owner/repo.

Maintainers: make plugin-validate runs claude plugin validate . --strict against both manifests; tests/test_plugin_manifest.py covers the same files without needing the CLI.

Claude Code

# Personal — available in all your projects
python3 install_skill.py --skill remove-ai-marks --target claude-code
# or: make install-claude-code-skill

# Project — commit .claude/skills/ to share it with the repo
python3 install_skill.py --skill remove-ai-marks --target claude-project \
  --project-dir /path/to/project
# or: make install-claude-project-skill PROJECT=/path/to/project

Claude Code picks up personal and project skills without a restart; /skills lists what it loaded. Invoke with /remove-ai-marks or ask to “strip AI watermarks / C2PA / Claude marks / SynthID-class text.” A project install is also what cloud sessions read, since they clone the repository and load its .claude/skills/.

Cowork (and claude.ai, cloud sessions, routines)

Cowork sessions do not read ~/.claude/skills on your machine — they load the skills enabled for your claude.ai account, synced when the session starts. So install there by uploading a bundle:

python3 install_skill.py --skill remove-ai-marks --target cowork
# writes dist/remove-ai-marks.zip   (make package-cowork-skill)

Then, in the Claude Desktop app, open Customize → Skills → Add and upload the zip (the same skill settings on claude.ai work too). The bundle is reproducible and contains a single top-level remove-ai-marks/ directory with SKILL.md at its root, which is the layout the upload expects.

Service reachability matters more here than in a local install: the skill is a thin HTTP client, so the session must be able to reach WATERMARKS_SERVICE_URL. Cowork sessions that run locally on your machine reach a local make serve; cloud sessions and routines run remotely and need a service URL reachable from there (and WATERMARKS_SERVER_API_KEY set on it). If you want a skill with no service at all, upload clean-user-facing-text instead — it is text-only and ships its own scripts:

python3 install_skill.py --skill clean-user-facing-text --target cowork

Grok

# Grok Build / project-local
mkdir -p .grok/skills
ln -sfn "$(pwd)/skills/remove-ai-marks" .grok/skills/remove-ai-marks

# User-global Grok
mkdir -p ~/.grok/skills
ln -sfn "$(pwd)/skills/remove-ai-marks" ~/.grok/skills/remove-ai-marks

Optional text-only skill

skills/clean-user-facing-text/ is a self-contained skill for authorized manuscripts, documentation, and web copy. It excludes image, C2PA, service, and external-model tooling, and runs its own vendored Layer A scripts instead of calling the service.

python3 install_skill.py --skill clean-user-facing-text --target claude-code
python3 install_skill.py --skill clean-user-facing-text --target cursor

Skill invocation is model-selected. Projects that explicitly adopt this workflow in Cursor can also copy the optional rule:

mkdir -p /path/to/project/.cursor/rules
cp integrations/cursor/clean-user-facing-text.mdc \
  /path/to/project/.cursor/rules/clean-user-facing-text.mdc

For all projects, put the same instruction in Cursor User Rules instead. Rules improve consistency but remain model instructions; Cursor does not expose a deterministic pre-send filter for final chat responses.

Start the service

The fastest path is a local HTTP server (Python 3.10+ stdlib only — no deps, no Docker):

make serve                 # http://127.0.0.1:8765
# or directly:
python3 service/scripts/server.py --host 127.0.0.1 --port 8765

Windows (no Docker)

See docs/windows-autostart.md for auto-starting the service at Windows login without Docker.

For the whole infra (core + optional harness/heavy backends), see Docker / compose below.

Optional system tools (auto-used when present — preinstalled in the core Docker image):

Tool Role
c2patool Inspect C2PA manifests
exiftool Residual metadata strip (esp. PDF)
qpdf Structural PDF rebuild — required for a real PDF strip (see below)

Core scripts need Python 3.10+ stdlib only. Layer B model calls are optional.

Quick use (scripts)

SCRIPTS=service/scripts

# Unified inspect / clean
python3 "$SCRIPTS/inspect_file.py" draft.md
python3 "$SCRIPTS/clean_file.py" draft.md -o draft.cleaned.md
python3 "$SCRIPTS/clean_file.py" photo.png -o photo.cleaned.png
python3 "$SCRIPTS/clean_file.py" notes.docx -o notes.cleaned.docx

# Text Layer A
python3 "$SCRIPTS/inspect_text.py" draft.md
python3 "$SCRIPTS/clean_text.py" draft.md -o draft.cleaned.md --stats

# Layer B rewrite hook (default: print prompt only — no model required)
python3 "$SCRIPTS/rewrite_text.py" draft.md --backend print-prompt --tactic paraphrase
# Optional local Ollama (loopback only by default — remote endpoints require
# WATERMARKS_REWRITE_ALLOW_REMOTE=1 or --allow-remote):
# WATERMARKS_REWRITE_BACKEND=ollama WATERMARKS_REWRITE_MODEL=llama3.2 \
#   python3 "$SCRIPTS/rewrite_text.py" draft.md -o draft.rewritten.md
# API keys are read from WATERMARKS_REWRITE_API_KEY only (never argv).

# Images
python3 "$SCRIPTS/inspect_image.py" shot.png
python3 "$SCRIPTS/clean_image.py" shot.png -o shot.cleaned.png

Text tools refuse binary input

inspect_text.py, clean_text.py and rewrite_text.py operate on text. Pointed at a .docx, .pdf or image they used to decode the compressed bytes and report whatever codepoints fell out — noise that tracks the compression, not the content — and clean_text.py then wrote those mangled bytes back, destroying the file. They now refuse binary input and name the tool that handles it:

python3 "$SCRIPTS/inspect_text.py" report.docx
# refusing to treat report.docx as text: it looks like a ZIP container (DOCX, ODT, …).
# Use inspect_file.py / clean_file.py, which route by format,
# or pass --force-text to scan the raw bytes anyway.

Detection is by magic number plus a control-byte ratio, so text in encodings other than UTF-8 keeps working. --force-text overrides it everywhere.

Unrecognized formats are never auto-cleaned

classify() labels bytes that match no supported text, image or container format as unknown — it no longer falls back to "text". In auto mode clean_file.py refuses such files (exit 2, no output written) instead of decoding them as UTF-8 and writing back mangled bytes; --as text or --force-text are the explicit opt-ins. inspect_file.py reports the file as unknown (exit 0), and the HTTP service answers /inspect with kind: "unknown" but rejects /clean of unknown formats (400 — send a filename with a known extension, e.g. notes.txt).

HTTP service

The same machinery runs as a stdlib HTTP service (service/scripts/server.py) — the interface the skill uses and the way any web app can integrate without vendoring:

Method Path Body Returns
GET /health — {"ok": true, "version": ...}
GET /capabilities — optional tools / backends usable (each tool is version-probed, not just found on PATH)
GET /openapi.json — dynamically generated OpenAPI 3.0.3 spec
POST /inspect {"file": "<base64>", "name": "notes.md"} {"ok", "kind", "suspicious", "report"}
POST /detect {"file": "<base64>", "name": "notes.txt"} {"ok", "kind", "detections": [...]}
POST /clean {"file": "<base64>", "name": "notes.md", "options": {...}} {"ok", "kind", "cleaned": "<base64>", "report"}
POST /watermark {"text": "...", "keys": [118, 504, ...], "options": {...}} or {"file": "<base64>", ...} {"ok", "kind", "watermarked_text", "report": {"scheme_used", ...}}
POST /inspect/batch {"files": [{"file": "<base64>", "name": "notes.md"}, ...]} {"ok", "results": [{"name", "ok", "kind", "suspicious", "report"}, ...]}
POST /detect/batch {"files": [{"file": "<base64>", "name": "notes.txt"}, ...]} {"ok", "results": [{"name", "ok", "kind", "detections", "report"}, ...]}
POST /clean/batch {"files": [{"file": "<base64>", "name": "notes.md", "options": {...}}, ...]} {"ok", "results": [{"name", "ok", "kind", "cleaned", "report"}, ...]}
POST /watermark/batch {"files": [{"text": "...", "keys": [...]}, {"file": "<base64>"}, ...]} {"ok", "results": [{"name", "ok", "kind", "watermarked_text", "report": {"scheme_used", ...}}, ...]}

Batch endpoints loop the same per-file pipeline as /inspect, /detect, /clean, and /watermark, capped at WATERMARKS_MAX_BATCH_FILES files per request (default 50). A malformed entry (bad base64, unknown option, unrecognized format) surfaces as that entry's "ok": false with an "error" string — it never aborts the rest of the batch.

WM="http://127.0.0.1:8765"
curl -s "$WM/health"                       # {"ok": true, "version": "..."}
curl -s "$WM/openapi.json"                 # machine-readable OpenAPI 3.0.3 contract
curl -s -X POST "$WM/clean" -H 'Content-Type: application/json' \
  -d "{\"file\": \"$(base64 < notes.md | tr -d '\n')\", \"name\": \"notes.md\"}"

The service routes by filename extension then magic bytes, so text / image / container are auto-detected. Set WATERMARKS_SERVER_API_KEY to require Authorization: Bearer <key> on every request. Loopback-only bind by default (--host to override); intended for a trusted network.

Watermark detection (/detect and detect_before / detect_after)

Detection is a separate step from cleaning — the service never calls vendor APIs unless you ask it to:

Text detectors (see /capabilities → text_detectors):

Text detectors (see /capabilities → text_detectors):

Detector Activated by Notes
markllm MARKLLM_DIR (host checkout) Research harness (KGW / SynthID schemes), same-config-only — not a vendor oracle.
gumbel WATERMARKS_GUMBEL_KEY Model-free same-key replay of the keyed-Gumbel (Aaronson EXP) scheme (see detect_gumbel.py), stdlib-only — self-hosted engines such as arbi-serve; same-key-only, not a vendor oracle.
claude-text — (placeholder) Anthropic has announced a watermark detection API; this seam activates when it ships.

Image scoring: when WATERMARKS_SYNTHID_SCORER_URL is set, the service scores images through the wr-synthid-score sidecar (heavy profile); with a local REVERSE_SYNTHID_DIR it uses the checkout directly. Detection is fail-soft: unconfigured, timed-out, or errored detectors report {"available": false, "error": ...} and never block cleaning.

Watermark generation (/watermark and /watermark/batch)

Generates watermarked text for benchmark evaluation and round-trip testing. When WATERMARKS_SYNTHID_TEXT_URL is set, the service delegates generation to the wr-synthid-text sidecar (harness profile); with a local MARKLLM_DIR it uses the checkout directly.

watermarks-remover (guillaumemeyer/watermarks-remover)

Searching Database…

这是什么?它用来去除你自己内容里的 AI 来源痕迹——文本中不可见的 Unicode 花招、统计性文本水印,以及文件里的 C2PA/EXIF/XMP 元数据。通过一个 agent 技能加本地 Python 服务实现,隐私优先。
这是需要安装的应用,还是我的编程 agent 用的工具?两者都是,构成一条流水线:一个是 markdown 编写的 agent 技能,本质是个轻量 HTTP 客户端(agent 宿主不需要 Python);另一个是做实际工作的标准库 Python 服务。另外还附带可独立运行的 CLI 脚本。
我该怎么把它跑起来?安装技能(只有 markdown 文件)并启动本地服务。在 Claude Code 里用自带的插件市场最快——两条命令,原地更新。其他环境:运行 python3 installskill.py --target claude-code(Windows 上用 py),然后启动服务。
我需要先装 Python 或 Docker 吗?只需 Python 3.10+ 标准库——没有第三方依赖,不需要 Docker。可选的系统工具如果存在会被自动调用:c2patool、exiftool 和 qpdf,其中 qpdf 是真正清除 PDF 痕迹所必需的。
它能覆盖我 agent 产生的所有内容吗?不能——确定性保障只覆盖 agent 写入的文件,外加一个 pre-commit 拦截关卡;只存在于聊天记录里的文本只能尽力而为,因为没有任何钩子能在你读到助手消息之前把它改写掉。
我的内容会离开我的电脑吗?默认不会——服务只绑定回环地址(可用 --host 覆盖),还可以要求 Bearer API key。有一个例外:claude.ai 云端会话在远程运行,所以需要一个能从那里访问到的服务地址。
它能去除哪些类型的 AI 痕迹?三类:不可见 Unicode(特殊空格、双向控制符、标签字符),用确定性脚本处理;统计型令牌采样的文本水印,通过 agent 重写处理;以及文件元数据——C2PA、EXIF、XMP、文档属性。按类覆盖各家方案:Claude、Gemini/SynthID-Text、OpenAI,以及开源大模型的 green-list 和 Aaronson 式水印。
它支持哪些文件格式?图片:PNG、JPEG、WebP、AVIF、HEIC、BMP、GIF、TIFF、SVG。文档:PDF、DOCX、XLSX、PPTX、EPUB、ODT、HTML、Markdown。媒体:MP4/MOV/M4A/M4V 视频和 WAV、MP3、FLAC 音频,另外还有纯文本。
我的 agent 工作时它能自动清理文件吗?可以——插件会在 Write/Edit/MultiEdit/NotebookEdit 上注册一个 PostToolUse 钩子。检查模式(默认)只报告痕迹,让模型主动提出清理;清理模式则原地去除,需设置 WATERMARKSHOOKMODE=clean。
不用编程 agent 也能用吗?可以——同一套机制可以作为本地 HTTP 服务运行,提供 /inspect、/detect、/clean 和 /watermark 接口以及对应的批量版本(默认每次请求 50 个文件),并附带自动生成的 OpenAPI 3.0.3 规范文档,任何网页应用都能接入。
Size
2.1 MB
Stars
24.3K
Age
2mo
Commits · 90 Days