
Strip multi-vendor AI provenance marks: Unicode text hygiene, statistical rewrite hooks, and C2PA/metadata from PNG/JPEG/SVG/PDF/DOCX/HTML/MD
_ _ _ ____ ___ ____ ____ _ _ ____ ____ _ _ ____ ____ ____ _ _ ____ _ _ ____ ____
| | | |__| | |___ |__/ |\/| |__| |__/ |_/ [__ __ |__/ |___ |\/| | | | | |___ |__/
|_|_| | | | |___ | \ | | | | | \ | \_ ___] | \ |___ | | |__| \/ |___ | \
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.4.0
Skill path: skills/remove-ai-marks/
(migration: formerly remove-claude-marks; slash alias /remove-claude-marks still documented)
# 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):
Core scripts need Python 3.10+ stdlib only. Layer B model calls are optional.
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
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.
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.
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).
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.
For pixel-domain image watermarks (SynthID-class, StegaStamp, Tree-Ring,
StableSignature), an optional external backend runs the CtrlRegen pipeline
(ControlNet + DINOv2 IP-Adapter controllable regeneration). The backend is
mertizci/noai-watermark, a
maintained reimplementation of the ICLR 2025
CtrlRegen method with automatic tiling.
The backend is not bundled and ships no LICENSE file, so it is treated as all-rights-reserved: it is cloned at a pinned commit and loaded at runtime.
SCRIPTS=skills/remove-ai-marks/scripts
# Clones upstream (pinned commit), creates a venv, installs torch + deps.
"$SCRIPTS/setup_ctrlregen.sh"
# Standalone removal (default checkout: ~/noai-watermark).
NOAI_WATERMARK_DIR=~/noai-watermark \
~/noai-watermark/.venv/bin/python "$SCRIPTS/clean_ctrlregen.py" shot.png -o shot.ctrlregen.png
clean_image.pyNOAI_WATERMARK_DIR=~/noai-watermark \
~/noai-watermark/.venv/bin/python "$SCRIPTS/clean_image.py" shot.png \
-o shot.cleaned.png --remove-pixel ctrlregen
Order of operations: metadata strip first, then CtrlRegen pixel removal, then
an optional reverse-SynthID before/after score (when REVERSE_SYNTHID_DIR is
also set).
Strength is conservative by default (--ctrlregen-strength 0.25), because
higher strength removes more watermark but regenerates more of the image.
Documented presets: 0.15 minimal / 0.25 default / 0.35 balanced /
0.5 aggressive / 0.7 max (backend default is 0.5). --ctrlregen-steps
defaults to 50 (effective denoising steps ≈ steps × strength).
CtrlRegen is a 512×512 Stable Diffusion 1.5 ControlNet. The backend resolves this for arbitrary inputs, so no extra tiling is exposed here:
Very large images (e.g. 4K) produce many tiles, so runs scale with tile count (slower and higher VRAM). Pre-downscale large inputs when practical; tile size and overlap are hardcoded upstream and are not exposed as flags.
Expect ~10 GB of model downloads; a GPU is strongly recommended and CPU runs
are slow. Some upstream models are gated, so export HF_TOKEN (env only —
never argv). clean_ctrlregen.py refuses to auto-install dependencies; run
setup_ctrlregen.sh first.
There is no local detector for StegaStamp/Tree-Ring/StableSignature, so the
only local signal is the reverse-SynthID score (a surrogate). When available,
clean_image.py --remove-pixel ctrlregen reports that score before/after; the
official Google SynthID check remains the final authority.
make docker-ctrlregen-build
docker run --rm -e HF_TOKEN="$HF_TOKEN" \
--user "$(id -u):$(id -g)" \
-v "$(pwd):/data" \
watermarks-remover-ctrlregen /data/shot.png -o /data/shot.ctrlregen.png
Details: skills/remove-ai-marks/references/vendor-notes.md, mark-classes.md.
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.
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).
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.
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.
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:
Pixel-domain watermark removal is now available as an optional external CtrlRegen backend (see above); it is a regenerating remover, not a guarantee. C2PA soft binding (in-content watermark that can re-link a remote Content Credentials manifest after metadata is stripped) remains out of scope. Stripping hard-bound C2PA does not clear those channels.
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):
Industry two-layer context (C2PA + imperceptible watermark): Institute of AI PM guide.
Matrix: skills/remove-ai-marks/references/removal-matrix.md.
See skills/remove-ai-marks/references/ethics.md. For privacy and research on your content — not academic fraud or false “human-written” claims.
Responsible use: This project is for content you own or are authorized to process. Users must adhere to local regulations and use it responsibly. The developers disclaim any liability for potential misuse by users.
python3 -m venv .venv && .venv/bin/pip install pytest
.venv/bin/python -m pytest # or: make test
make smoke # quick CLI smoke on fixtures
Optional CtrlRegen pixel removal (external backend)
mertizci/noai-watermark checkout: clean_ctrlregen.py adapter + setup_ctrlregen.sh bootstrap (pinned commit, sparse checkout, venv, SHA verification), plus Dockerfile.ctrlregen and make bootstrap-ctrlregen / docker-ctrlregen-build / smoke-ctrlregenclean_image.py --remove-pixel ctrlregen runs metadata strip → CtrlRegen removal → optional reverse-SynthID before/after score; inspect_image.py hints at the flag on a high SynthID score0.25 (presets 0.15/0.25/0.35/0.5/0.7); the 512×512-native pipeline is auto-tiled by the backend for larger images; the torch subprocess gets higher env-overridable resource capsnoai-watermark ships no LICENSE file (treated as all-rights-reserved), and its auto-install/restart code paths are bypassed by using directlyFinding confidence and aggregate audits
confirmed / probable / informational / likely_false_positive, exposed in text/image/container JSON and human reportsaudit_dir.py (recursive tree) and audit_website.py (sitemap discovery + crawl) aggregate reports; documented in SKILL.mdFalse-positive fixes
docProps/customXml, not the visible body (#14)VS16/ZWJ after an emoji base; new --strip-emoji-glue paranoid flag (#22)Windows support
preexec_fn and os.fchmod so writes and optional tools run on Windows (#15, #23)Docs and supply chain
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 filerewrite_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_KEYRLIMIT_AS/ applied to exiftool/c2patool/SynthID subprocesses (all caps env-overridable)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--strength humanize: zero-shot "write like a human" pass targeting formulaic AI-style phrasing--strength code: rewrites comments, docstrings, and string literals, and renames local identifiers while preserving behavior and public API names--temperature (default 0.9) for both Ollama and OpenAI-compatible backends--candidates N: generates N rewrites and selects the most lexically diverged (bigram Jaccard distance) with a length-drift guardSKILL.md, removal-matrix.md, and vendor-notes.md; tests cover new prompts, divergence scoring, and candidate selectionaloshdenny/reverse-SynthID checkout (score_synthid.py); surfaced in inspect_image.py / clean_image.py with REVERSE_SYNTHID_DIR or --synthid-dirsetup_synthid.sh bootstrap (scorer-only dependencies; --full installs upstream requirements); Dockerfile.synthid plus make docker-synthid-build / docker-synthid-helpsmoke-synthid and bootstrap-synthid targetsimage_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)tests/test_c2patool_report.py (4 cases: no claim, no JUMBF, genuine manifest, tool absent)c2patool links (repo moved to contentauth/c2pa-rs); added a disclaimer on the quality cost of text-watermark removalMakefile (test / smoke / install-skill) and pytest.iniremove-ai-marks (replaces Claude-only remove-claude-marks)inspect_text / clean_text)rewrite_text.py (print-prompt, Ollama, OpenAI-compatible)inspect_file.py / clean_file.pyc2patool / exiftoolMIT — see LICENSE.
| 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 + CtrlRegen removal (external) | Out of scope | Optional CtrlRegen removal (external) |
| Training backdoors | Out of scope | Out of scope | Out of scope | Out of scope |
| Format | Inspect | Clean |
|---|
| PNG / JPEG | C2PA chunks / APP11, AI XMP hints | Drop metadata segments |
| SVG | <metadata>, XMP | Strip blocks |
| 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 |
| 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 | Optional pixel removal (external CtrlRegen); local score otherwise | Audio/video watermark; residual pixel watermark after removal | 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 |
| 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 |
| CtrlRegen pixel removal (optional) | Pixel-domain image marks (SynthID-class, StegaStamp, Tree-Ring, StableSignature) | External backend; heavy compute; conservative strength default |
| Open-weight local models | Avoid re-stamping with origin model | Operational alternative |
CtrlRegenEngineRLIMIT_FSIZEpermissions: 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