
Repo: guillaumemeyer/watermarks-remover License: MIT (core project) · Latest tagged release: v0.6.0 One-liner: An agent skill plus a stdlib-Python HTTP service that strips AI-provenance marks (invisible Unicode, statistical text watermarks, and file/container metadata such as C2PA/EXIF/XMP) from content the user owns.
1. What it actually is
Under the marketing framing ("privacy-first," "AI provenance"), this is a watermark/metadata-stripping toolkit with three cooperating pieces:
A skill (markdown instructions for an LLM agent host — Claude Code, Cursor, Cowork, Grok) that tells the model when and how to invoke cleaning.
A stdlib Python HTTP service that does the actual work — no third-party dependencies required for the core path.
A set of optional, non-bundled external backends (CtrlRegen, a reverse-SynthID scorer, MarkLLM, MarkDiffusion) for pixel-domain removal and research-grade verification, each isolated behind its own venv/Docker image because of licensing or dependency conflicts.
The project is explicit that this only works against your own content, and it draws a hard line between what it can verify (deterministic Unicode/metadata removal) and what is best-effort (defeating statistical, sampling-based text watermarks).
2. Design philosophy: three layers, honestly labeled
The architecture is organized around a threat/marking taxonomy rather than around file types, which is the most interesting design decision in the project:
Layer | Target | Mechanism | Certainty |
|---|---|---|---|
Layer A | Invisible Unicode (ZWSP, bidi controls, exotic spaces, tag characters, noncharacters) | Deterministic scripts | Verifiable — you can count what was removed |
Layer B | Statistical/sampling-based text watermarks (Kirchenbauer-style green-list, SynthID-Text, keyed-Gumbel/Aaronson EXP) | LLM-driven rewrite, optionally detection-guided | Best-effort only — no universal detector exists |
File/container | C2PA, EXIF, XMP, document properties across ~20 formats | Format-specific parsers/rewriters | Verifiable per-format, with documented gaps (embedded images inside PDFs, pixel-domain marks) |
The README is unusually candid that Layer B is fundamentally different in kind from the other two: a statistical watermark is spread across every token choice, so removing it requires substantially rewording the text, which necessarily degrades voice and precision. The project frames this as an honest trade rather than hiding it — a design choice worth noting in a review, since most "watermark remover" tools don't disclose this.
3. System architecture
3.1 Skill/service split
This is the core architectural decision post-v0.5.0. The skill directory (skills/remove-ai-marks/) ships no implementation code — it's a thin HTTP client description that an agent model reads and acts on. All actual logic lives in service/scripts/, fronted by a stdlib server.py. This means:
The agent host (Claude Code, Cursor, etc.) needs no Python runtime or dependencies of its own.
The service can run locally (
127.0.0.1:8765by default, loopback-only) or remotely for cloud/Cowork sessions, with bearer-token auth viaWATERMARKS_SERVER_API_KEY.Multiple agent hosts share one service, avoiding duplicated cleaning logic.
3.2 The HTTP service surface
Method | Path | Purpose |
|---|---|---|
GET |
| Liveness + version |
GET |
| Which optional tools/backends are actually usable (version-probed, not just present on |
GET |
| Spec generated from the live route table, so it can't drift |
POST |
| Report findings without modifying the file |
POST |
| Run watermark detectors (separate from cleaning; never calls vendor APIs unless asked) |
POST |
| Strip marks; batch endpoints isolate per-file failures instead of aborting |
All payloads are base64-encoded JSON, and the service auto-detects file kind by extension + magic bytes rather than trusting the client's claim.
3.3 Distribution surfaces
The project supports an unusually wide range of install paths for what is fundamentally one Python codebase:
A generic installer (
install_skill.py, stdlib-only) that targets Claude Code (personal/project), Cowork/claude.ai, and Cursor, validating output against the community Agent Skills packaging spec before writing anything.A Claude Code plugin + single-plugin marketplace (
.claude-plugin/) for two-command install/update.A
PostToolUsehook (hook_written_file.py) that runs deterministically on every file write regardless of whether the model decides to invoke the skill — this is the project's answer to the fact that skills are cooperative (model-invoked) while hooks are not.A pre-commit hook wrapping the same CLIs, so marked files are caught before they're committed rather than only at write-time or in CI.
Docker / docker-compose, with the core image on GHCR and optional
harness(MarkLLM, MarkDiffusion) andheavy(CtrlRegen, SynthID scorer) profiles for anything with heavyweight or licensing-encumbered dependencies.
Worth flagging architecturally: the README is explicit that no hook can intercept the assistant's own chat message before the user reads it — Claude Code's Stop hook only sees the message read-only, with no pre-send filter. So the deterministic guarantee only covers files written to disk (plus anything caught by the pre-commit gate); text that only ever exists in a chat transcript depends on the model choosing to invoke the skill, which is best-effort by construction. This is a real architectural boundary, not a bug.
3.4 Optional heavy backends — why they're kept separate
Four capabilities are deliberately not bundled into the core service, each for a different reason:
Backend | What it adds | Why it's external |
|---|---|---|
CtrlRegen ( | Pixel-domain removal (SynthID-class, StegaStamp, Tree-Ring, StableSignature) via ControlNet+DINOv2 regeneration | Upstream ships no LICENSE file (treated as all-rights-reserved) — never redistributed, only cloned at a pinned commit into a local venv/image |
reverse-SynthID scorer | Confidence score for SynthID pixel watermarks | Upstream is under a non-commercial Research License |
MarkLLM | Same-config watermark/detect harness for research verification | Apache-2.0, but adds torch/transformers — kept in its own venv |
MarkDiffusion | Image-watermark generation/detection/purification harness | Same rationale — Apache-2.0 but heavy, and purification is a fallback with more content drift than CtrlRegen |
This licensing-aware isolation — pinned commits, dedicated venvs, "local build only, never published" image tags — is a genuinely careful piece of engineering hygiene that's easy to overlook but matters a lot for a project redistributing Docker images.
4. Detection subsystem: a separate concern from cleaning
Detection is architected as its own pipeline, not a side effect of cleaning, and it's fail-soft everywhere: an unconfigured or errored detector reports {"available": false} rather than blocking a clean. Three text detectors exist:
MarkLLM — valid only against the same scheme configuration used at generation; explicitly documented as "not a vendor oracle."
Keyed-Gumbel replay (
detect_gumbel.py) — a stdlib, model-free replay that recomputes the pseudo-random function from the text and tests the statistical tail. No GPU or model needed, but only valid for self-hosted engines where you hold the key.A reserved seam for a Claude vendor detector — not yet implemented, sitting ready for when Anthropic ships a public detection API.
The detection-guided Layer B rewrite loop is the most sophisticated piece of the text pipeline: it generates candidate rewrites, evaluates each against whatever detector is configured (Gumbel > MarkLLM > lexical-divergence fallback), and stops as soon as one passes — with a documented --max-loops ceiling so it doesn't run indefinitely. When no detector is configured, it falls back to picking the most lexically diverged candidate, with no pass/fail claim at all.
5. File-format coverage
The container-cleaning layer covers roughly twenty formats (PNG/JPEG/WebP/AVIF/HEIC/BMP/GIF/TIFF/SVG, PDF, DOCX/XLSX/PPTX, EPUB/ODT, HTML/Markdown, and MP4/WAV/MP3/FLAC), each with format-specific logic for where provenance data actually hides. The PDF path is the most architecturally interesting because it needs three cascading passes, and the README's own justification for this is worth restating because it reveals a subtlety many tools miss:
exiftoolalone writes PDFs incrementally — it appends an update block and hides the/Infodictionary from the trailer, but the original bytes remain physically present in the file and are technically recoverable.qpdf --linearizere-serializes the document from its object graph, which actually discards those now-unreferenced objects.Neither pass touches an image inside the PDF (e.g., a scanned page), so a Ghostscript-driven
deep_imagespass is layered on top, with a lossless-first, re-encode-only-on-evidence policy — it only recompresses pixel data when APPn metadata is demonstrably still present after the document-level strip.
This "check whether the previous layer actually worked before escalating" pattern (verified by hashing pre/post streams) is a solid piece of defensive engineering.
6. Getting started
6.1 Fastest path: Claude Code plugin
/plugin marketplace add guillaumemeyer/watermarks-remover
/plugin install watermarks-remover@watermarks-removerThis installs both shipped skills (remove-ai-marks, service-backed; clean-user-facing-text, self-contained/text-only) and registers the write-time hook, with no local clone required.
6.2 Manual skill install (any supported host)
python3 install_skill.py --skill remove-ai-marks --target claude-code
# --target claude-project --project-dir PATH for a per-project install
# --target cowork builds dist/<skill>.zip for claude.ai/Cowork upload
# --target cursor (default) symlinks/copies into ~/.cursor/skills6.3 Start the service
make serve
# or directly:
python3 service/scripts/server.py --host 127.0.0.1 --port 8765Requires only Python 3.10+ stdlib for the core path. Optional system tools (c2patool, exiftool, qpdf) are auto-detected and each unlock a specific capability tier — the service degrades gracefully (with an explicit warning) when any is missing.
6.4 Command-line quick use
SCRIPTS=service/scripts
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.pngNothing above requires configuration — invisible-Unicode and file-metadata cleaning work out of the box. Layer B (the rewrite path) additionally needs a rewrite backend, configured via WATERMARKS_REWRITE_BACKEND (print-prompt by default — just prints the prompt, no model call — or ollama/openai-compatible).
6.5 Docker / compose, if you want the full stack
docker compose up -d # core service only
docker compose --profile harness up -d # + MarkLLM / MarkDiffusion
docker compose --profile heavy up -d # + CtrlRegen / SynthID scorer (local builds)
make compose-check # validates the running stack6.6 Talking to the HTTP API directly
curl -s -X POST http://127.0.0.1:8765/clean \
-H 'Content-Type: application/json' \
-d "{\"file\": \"$(base64 < notes.md | tr -d '\n')\", \"name\": \"notes.md\"}"7. Security & hardening posture
A few choices stand out as more careful than typical for a project this size:
Secrets never touch argv. API keys and the Gumbel replay key are read from environment variables only, and the docs repeatedly call this out (
WATERMARKS_REWRITE_API_KEY,WATERMARKS_GUMBEL_KEY).Remote calls require explicit opt-in. The rewrite hook defaults to loopback-only; sending content to a non-loopback endpoint requires
WATERMARKS_REWRITE_ALLOW_REMOTE=1.Offline mode for research harnesses.
--offlineforces MarkLLM/MarkDiffusion to load models from the local Hugging Face cache only, withtrust_remote_codenever enabled.Resource caps. Config files are capped at 1 MiB; batch endpoints cap file counts (
WATERMARKS_MAX_BATCH_FILES, default 50); parsers explicitly reject decompression-bomb-style inputs (the changelog references specific GHSA advisories fixed for PNGzTXt/iTXt, SVG/ODT, and sitemap DTD entity expansion).Unrecognized input is refused, not guessed. Files that don't match a known text/image/container signature are classified
unknownand rejected by/clean(400) rather than being decoded as UTF-8 and silently corrupted — a real bug class the project says it used to have and fixed.Fail-soft, not fail-silent-success. Missing optional tools produce explicit warnings in the report (e.g., "install qpdf for a structural rewrite") rather than reporting a clean that didn't fully happen.
8. Strengths
The verifiable vs. best-effort distinction is threaded through the entire design — reports, docs, and API responses all separate what was deterministically removed from what was best-effort rewritten. This is the single best architectural decision in the project and the thing most similar tools skip.
Licensing discipline around third-party backends (pinned commits, non-published images, explicit LICENSE-absence handling) is unusually rigorous.
The hook vs. skill distinction (deterministic-on-write vs. cooperative-on-invocation) is a clear-eyed acknowledgment of what agent harnesses can and can't guarantee.
Format coverage is broad and the PDF cascading-pass design in particular reflects real production experience with where metadata actually survives naive stripping.
9. Limitations and open questions (a fair review should name these)
Layer B provides no removal guarantee, by the project's own repeated admission — it is a rewrite, not a certified bypass, and the README goes so far as to question the practice's own value proposition (why pay for a premium model's output only to have a cheaper model rewrite it away).
Pixel-domain image watermarks are only addressed via heavy, non-bundled, GPU-hungry external backends, and even those are described as "regenerating remover, not a guarantee."
C2PA soft binding (content that can re-link to a remote credentials manifest after local metadata is stripped) is explicitly out of scope.
Training-time backdoor watermarks are out of scope entirely, for all vendors.
The project depends on a fairly large number of pinned, occasionally licensing-encumbered upstream research repos (MarkLLM, MarkDiffusion, CtrlRegen, reverse-SynthID) — real supply-chain surface area even with the isolation described above.
10. Ecosystem
Two independent, unaffiliated projects are listed in the README for discoverability: MetaClean (a Rust/Tauri desktop GUI, MIT) and unmark-web (a static, MIT-licensed browser client that can optionally call this project's service). The README is careful to state it doesn't vouch for either.
11. Bottom line
Architecturally, this is a well-factored project: a clean separation between cooperative (skill) and deterministic (hook/service) enforcement, an honest three-layer taxonomy that doesn't oversell what statistical-watermark rewriting can do, and real engineering care around licensing isolation for the heavier optional backends. The project is explicit — repeatedly, not just in a disclaimer footer — that it's meant for hygiene and privacy on content you already own or are authorized to process, not for defeating provenance systems on content that isn't yours, and the architecture backs that framing up by refusing to claim certainty it can't verify.
Comments (0)
Login to post a comment.