watermarks-remover

Introduction: Strip multi-vendor AI provenance marks: Unicode text hygiene, statistical rewrite hooks, and C2PA/metadata from PNG/JPEG/SVG/PDF/DOCX/HTML/MD
More: Author   ReportBugs   OfficialWebsite   
Tags:
_ _ _ ____ ___ ____ ____ _  _ ____ ____ _  _ ____    ____ ____ _  _ ____ _  _ ____ ____
| | | |__|  |  |___ |__/ |\/| |__| |__/ |_/  [__  __ |__/ |___ |\/| |  | |  | |___ |__/
|_|_| |  |  |  |___ |  \ |  | |  | |  \ | \_ ___]    |  \ |___ |  | |__|  \/  |___ |  \

CI Release

Agent skill + stdlib Python scripts to strip multi-vendor AI provenance marks from text and files — for privacy and hygiene on content you own.

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, SVG, PDF, DOCX, ODT, HTML, Markdown

Vendors / ecosystems (class-level): Claude, Gemini / SynthID-Text, OpenAI provenance surfaces, open-LLM Kirchenbauer-style marks.

Latest release: v0.3.2

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

Install (agent skill)

# 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

Invoke with /remove-ai-marks or ask to “strip AI watermarks / C2PA / Claude marks / SynthID-class text.”

Optional system tools (auto-used when present):

Tool Role
c2patool Inspect C2PA manifests
exiftool Residual metadata strip (esp. PDF)

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

Quick use (scripts)

SCRIPTS=skills/remove-ai-marks/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 --strength 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

Optional SynthID pixel scoring

inspect_image.py and clean_image.py can report a pixel-domain SynthID confidence score when an external checkout of aloshdenny/reverse-SynthID is available. The scorer is not bundled: it is loaded at runtime from your checkout, and its code remains under the upstream project's non-commercial Research License.

Option 1: one-command bootstrap (no Docker)

SCRIPTS=skills/remove-ai-marks/scripts

# Clones upstream, creates a venv, and installs scorer-only dependencies.
"$SCRIPTS/setup_synthid.sh"

# Score an image (default checkout: ~/reverse-SynthID).
REVERSE_SYNTHID_DIR=~/reverse-SynthID \
~/reverse-SynthID/.venv/bin/python "$SCRIPTS/score_synthid.py" shot.png

# Or surface the score from inspect / clean (same venv Python).
REVERSE_SYNTHID_DIR=~/reverse-SynthID \
~/reverse-SynthID/.venv/bin/python "$SCRIPTS/inspect_image.py" shot.png

setup_synthid.sh accepts --dir PATH, --ref REF, and --full (install the full upstream requirements.txt, which adds torch/diffusers for the upstream VAE bypass this project does not use).

Option 2: local Docker build

make docker-synthid-build
# Run unprivileged and with a read-only rootfs; the scorer only needs to read
# /data and write to stdout/tmp.
docker run --rm \
  --user "$(id -u):$(id -g)" \
  --read-only --tmpfs /tmp \
  -v "$(pwd):/data" \
  watermarks-remover-synthid-scorer /data/shot.png

The image is built locally from the upstream source at build time. It is not published, so it does not redistribute the upstream code.

V4 scoring uses artifacts/spectral_codebook_v4.npz from the upstream checkout (~220 MB). This is detection/scoring only — it does not remove pixel watermarks.

Coverage matrix

Channel Claude Gemini/SynthID OpenAI Open-LLM
Unicode / edit-based text Layer A Layer A Layer A Layer A
Statistical sampling text Layer B best-effort Layer B best-effort Layer B if present Layer B best-effort
C2PA / file metadata Yes (listed formats) Yes when present Yes when present Yes when present
Pixel image marks Out of scope Optional SynthID score (external); removal out of scope Out of scope Out of scope
Training backdoors Out of scope Out of scope Out of scope Out of scope

Details: skills/remove-ai-marks/references/vendor-notes.md, mark-classes.md.


How text marking works (short)

Modern LLM watermarks often hide a signal in which tokens are chosen (generative / sampling bias), not only in invisible characters. Edit-based schemes inject Unicode or synonym rules. File schemes attach C2PA or generator metadata.

  • Layer A removes edit-based Unicode carriers (testable).
  • Layer B attacks sampling watermarks via heavy rewrite (best-effort; literature-standard attacks such as paraphrase / back-translation).
  • File cleaners strip C2PA/XMP/props from supported containers.

Until vendors ship public detectors and keys, no tool can honestly certify “this fails the official check.” Reports must separate verifiable vs best-effort work.

Prefer a non-origin model for Layer B (do not rewrite Claude text with Claude if you are trying to avoid re-stamping).


Disclaimer: what removing a text watermark costs

Text watermarks live in the wording itself: the signal is spread across token choices, so nearly every sentence carries a little of it. Two consequences follow, and they are why Layer B is honestly described as best-effort rather than a magic eraser.

  1. Removal means rewording, not restructuring. Shuffling paragraphs, changing headings, or light touch-ups barely move the signal. Stripping a statistical mark requires rewriting a substantial fraction of the text — sentence by sentence, not section by section.

  2. Rewording degrades the copy. Any rewrite replaces the original word choices with the rewriting model's, which flattens tone, voice, and precision. On production copy (SEO, marketing, client work) that degradation is real and often visible to the people who care most about the writing. It is like taking text from a top-tier model and asking a less capable model to rewrite it from scratch: the result cannot exceed the rewrite model's ceiling.

Which leads to the honest full-circle question:

If the plan is to rewrite the text with a cheaper model anyway, why pay for a premium model in the first place? Generating directly with the cheaper model is simpler, cheaper, and produces the same — or better — end result.

Layer B makes sense when you specifically want the premium model's thinking and drafting and accept a rewrite pass to satisfy a hygiene or privacy requirement — not as a cheap route to mark-free text.

When to skip Layer B:

  • Quality matters more than hygiene: use the lossless path — Layer A Unicode scrub plus the file metadata cleaners — and keep the original prose.
  • Rewriting anyway: use a non-origin model (rewriting with the origin model can re-stamp the text), and remember residual risk remains — no tool can certify a vendor detector will fail.

File formats

Format Inspect Clean
PNG / JPEG C2PA chunks / APP11, AI XMP hints Drop metadata segments
SVG <metadata>, XMP Strip blocks
PDF Byte/XMP + optional tools exiftool preferred; degraded without it
DOCX docProps / customXml Scrub props, drop customXml
ODT meta.xml Drop generator / AI-ish meta
HTML meta, JSON-LD, data-ai* Strip tags/attrs
Markdown YAML frontmatter AI keys Drop keys + Layer A body

Pixel-domain watermark removal and C2PA soft binding (in-content watermark that can re-link a remote Content Credentials manifest after metadata is stripped) remain out of scope. Stripping hard-bound C2PA does not clear those channels. An optional local SynthID scorer is available for detection only (see above).

Residual risk after a clean

This tool reports verifiable removals (Unicode counts, metadata actions) and best-effort Layer B rewrites. It cannot certify that vendor detectors will fail.

To check residual signals yourself (optional, external):

Channel What we remove What may remain External check (examples)
Hard-bound C2PA / EXIF / XMP Yes Soft-bound / pixel marks c2patool, Content Credentials verify
SynthID-class media No (optional local score only) Pixel/audio/video watermark Provider tools (e.g. Google SynthID / Vertex detector where offered); optional local reverse-SynthID scorer
Statistical text Best-effort rewrite Strong marks after light edit No public universal detector; vendor tools when available

Industry two-layer context (C2PA + imperceptible watermark): Institute of AI PM guide.


Removal options (summary)

Option Removes Notes
Unicode scrub (Layer A) ZWSP, bidi, tags, exotic spaces, … Safe default for text
Rewrite (Layer B) Statistical token marks (best-effort) Always offered by skill; costs style — see Disclaimer
Container/metadata strip File provenance See format table
Open-weight local models Avoid re-stamping with origin model Operational alternative

Matrix: skills/remove-ai-marks/references/removal-matrix.md.

Ethics

See skills/remove-ai-marks/references/ethics.md. For privacy and research on your content — not academic fraud or false “human-written” claims.

Tests

python3 -m venv .venv && .venv/bin/pip install pytest
.venv/bin/python -m pytest          # or: make test
make smoke                          # quick CLI smoke on fixtures

Changelog

v0.3.2 — security hardening (safe writes, HTTP client, CI supply chain)

  • Safe, atomic output writes: every cleaner now writes via temp-file + atomic rename (safe_write_bytes / safe_write_text), refuses symlinked destinations, and creates .bak backups through the same safe path — pre-placed symlinks (e.g. in /tmp or download dirs) can no longer redirect a clean write onto an arbitrary file
  • rewrite_text.py HTTP client hardening: redirects are refused outright, so an API key in the Authorization header can never be re-sent to an unvalidated host; non-loopback endpoints are denied by default (opt in with --allow-remote or WATERMARKS_REWRITE_ALLOW_REMOTE=1); only http(s) schemes are accepted; --api-key was removed — keys are env-only via WATERMARKS_REWRITE_API_KEY
  • Resource caps: default max input 1 GiB → 256 MiB, new 64 MiB stdin cap, DOCX/ODT zip budget 512 MiB → 128 MiB, and RLIMIT_AS/RLIMIT_FSIZE applied to exiftool/c2patool/SynthID subprocesses (all caps env-overridable)
  • Supply chain: CI actions SHA-pinned with permissions: contents: read, pinned dev deps (requirements-dev.txt), a pip-audit step, and a new CodeQL workflow; the Docker image now runs as an unprivileged user with pip pinned
  • Scorer deps: Pillow bumped 10.4.0 → 12.3.0 (24 known CVEs); API usage verified against the pinned upstream commit
  • Tests: 18 new security regression tests (60 total, all passing)

v0.3.1 — stronger Layer B statistical-watermark rewrite

  • rewrite_text.py default paraphrase now performs an explicit word-choice + syntax attack (clause order, connectors, transition words, sentence boundaries, function words) rather than a generic rewrite
  • New --strength humanize: zero-shot "write like a human" pass targeting formulaic AI-style phrasing
  • New --strength code: rewrites comments, docstrings, and string literals, and renames local identifiers while preserving behavior and public API names
  • Structural pass now emits "natural, varied human prose" instead of AI-typical "clear professional style"
  • New --temperature (default 0.9) for both Ollama and OpenAI-compatible backends
  • New --candidates N: generates N rewrites and selects the most lexically diverged (bigram Jaccard distance) with a length-drift guard
  • Stronger model hygiene: prefer local open-weight models and avoid any known-watermarked vendor, not just the suspected origin
  • Residual-risk reporting now distinguishes short/highly predictable text (lower risk) from long, high-entropy prose (higher risk)
  • Docs updated in SKILL.md, removal-matrix.md, and vendor-notes.md; tests cover new prompts, divergence scoring, and candidate selection

v0.3.0 — optional SynthID pixel scoring

  • Optional pixel-domain SynthID scorer via an external aloshdenny/reverse-SynthID checkout (score_synthid.py); surfaced in inspect_image.py / clean_image.py with REVERSE_SYNTHID_DIR or --synthid-dir
  • setup_synthid.sh bootstrap (scorer-only dependencies; --full installs upstream requirements); Dockerfile.synthid plus make docker-synthid-build / docker-synthid-help
  • Makefile smoke-synthid and bootstrap-synthid targets
  • Tests for the scorer adapter, CLI unavailable path, JSON parsing, and runtime errors
  • Docs: detection/scoring only (no pixel removal); upstream code is not bundled and remains under its non-commercial Research License

v0.2.0 — c2patool false-positive fix

  • image_meta.py: has_manifest no longer flags Error: No claim found / No JUMBF data found as a manifest (operator-precedence bug: the negative markers now veto every positive branch)
  • New tests/test_c2patool_report.py (4 cases: no claim, no JUMBF, genuine manifest, tool absent)
  • Docs: fixed c2patool links (repo moved to contentauth/c2pa-rs); added a disclaimer on the quality cost of text-watermark removal

v0.1.0 — packaging polish + provenance honesty

  • Makefile (test / smoke / install-skill) and pytest.ini
  • Fixture samples for Markdown, HTML, SVG; PDF degraded-clean test
  • Docs: industry two-layer model (hard-bound C2PA vs soft binding / SynthID-media)
  • README residual-risk table + links to external verify tools
  • Reference: Institute of AI PM C2PA/SynthID guide
  • Soft-binding and pixel/audio/video watermarks explicitly out of scope in skill/matrix/ethics

v0.0.1 — initial multi-vendor release

  • Agent skill remove-ai-marks (replaces Claude-only remove-claude-marks)
  • Layer A: invisible Unicode / bidi / tag chars / space homoglyphs (inspect_text / clean_text)
  • Layer B: rewrite guidance + optional rewrite_text.py (print-prompt, Ollama, OpenAI-compatible)
  • Files: C2PA/AI metadata strip for PNG, JPEG, SVG, PDF, DOCX, ODT, HTML, Markdown
  • Unified inspect_file.py / clean_file.py
  • Multi-vendor docs (Claude, Gemini/SynthID-class, OpenAI, open-LLM)
  • Stdlib-first scripts; optional c2patool / exiftool

Star History

Star History Chart

License

MIT — see LICENSE.

References

Apps
About Me
GitHub: Trinea
Facebook: Dev Tools
AI Daily Digest