``` _ _ _ ____ ___ ____ ____ _ _ ____ ____ _ _ ____ ____ ____ _ _ ____ _ _ ____ ____ | | | |__| | |___ |__/ |\/| |__| |__/ |_/ [__ __ |__/ |___ |\/| | | | | |___ |__/ |_|_| | | | |___ | \ | | | | | \ | \_ ___] | \ |___ | | |__| \/ |___ | \ ``` # watermarks-remover [![CI](https://github.com/guillaumemeyer/watermarks-remover/actions/workflows/ci.yml/badge.svg)](https://github.com/guillaumemeyer/watermarks-remover/actions/workflows/ci.yml) [![Release](https://img.shields.io/github/v/release/guillaumemeyer/watermarks-remover)](https://github.com/guillaumemeyer/watermarks-remover/releases) [![Stars](https://img.shields.io/github/stars/guillaumemeyer/watermarks-remover)](https://github.com/guillaumemeyer/watermarks-remover/stargazers) [![Forks](https://img.shields.io/github/forks/guillaumemeyer/watermarks-remover)](https://github.com/guillaumemeyer/watermarks-remover/forks) 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, WebP, 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](https://github.com/guillaumemeyer/watermarks-remover/releases/tag/v0.4.0) Skill path: [`skills/remove-ai-marks/`](skills/remove-ai-marks/) (migration: formerly `remove-claude-marks`; slash alias `/remove-claude-marks` still documented) ## Install (agent skill) ```bash # 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`](https://github.com/contentauth/c2pa-rs/tree/main/cli) | Inspect C2PA manifests | | [`exiftool`](https://exiftool.org/) | Residual metadata strip (esp. **PDF**) | | [`qpdf`](https://qpdf.sourceforge.io/) | 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) ```bash 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 ``` ### 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: ```bash 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. ## 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`](https://github.com/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) ```bash 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). On Windows use `setup_synthid.ps1` (`-Dir`, `-Ref`, `-Full`), which creates the venv at `.venv\Scripts\` — the layout `image_meta.py` already looks for on `os.name == "nt"`. ### Option 2: local Docker build ```bash 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. ## Optional CtrlRegen pixel removal 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`](https://github.com/mertizci/noai-watermark), a maintained reimplementation of the ICLR 2025 [CtrlRegen](https://arxiv.org/abs/2410.05470) 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. ### Bootstrap ```bash 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 ``` On Windows use `setup_ctrlregen.ps1` (same flags as `-Dir`, `-Ref`, `-Python`); the venv lands in `.venv\Scripts\`, which `clean_image.py` already resolves. It picks the torch wheel index from the GPU's **compute capability** rather than the CUDA version `nvidia-smi` prints — that number is the maximum the *driver* supports, and drivers are backward compatible, so deriving the wheel tag from it installs `cu130` on a Pascal card whose kernels were dropped in `cu128`. The script forces `cu126` below compute capability 7.5 and then verifies the result with `torch.cuda.get_arch_list()`. ### From `clean_image.py` ```bash NOAI_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). ### Image size (512×512 native limit) CtrlRegen is a 512×512 Stable Diffusion 1.5 ControlNet. The backend resolves this for arbitrary inputs, so no extra tiling is exposed here: - **≤512 px:** single pass — center-crop/resize to 512, regenerate, resize back. - **>512 px:** automatic overlapping tiling (512 px tiles, 192 px overlap), width/height aligned to multiples of 8, then cosine-blended seams. - **Either path:** output is resized to the original size and color-matched to the original image. 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. ### Compute, gated models, and verification 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. ### Docker ```bash 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 ``` ## Optional MarkLLM text-watermark verification For **controlled experiments**, an optional external harness wraps [`THU-BPM/MarkLLM`](https://github.com/THU-BPM/MarkLLM) (Apache-2.0) to watermark test text and re-detect it after a Layer B rewrite — e.g. prove that a KGW (Kirchenbauer, your "open-LLM" row) or SynthID-Text (Gemini row) mark disappears under your rewrite. It is a **verification harness, not an oracle**: MarkLLM detection is only valid against the *same* scheme config + keys used at generation, and it cannot certify a vendor detector will fail. The backend is **not bundled**. `setup_markllm.sh` clones upstream at a pinned commit, creates a venv, and installs pinned deps (torch + transformers); the scoring model (default `facebook/opt-1.3b`, Apache-2.0) downloads from Hugging Face on first run. ```bash SCRIPTS=skills/remove-ai-marks/scripts # Bootstrap (clones upstream, creates ~/MarkLLM/.venv, installs deps). "$SCRIPTS/setup_markllm.sh" # Generate watermarked + unwatermarked sample text under the KGW scheme. MARKLLM_DIR=~/MarkLLM \ ~/MarkLLM/.venv/bin/python "$SCRIPTS/detect_text_watermark.py" watermark prompt.txt \ --scheme kgw -o wm.txt -o2 plain.txt # Detect the scheme mark in a text file. MARKLLM_DIR=~/MarkLLM \ ~/MarkLLM/.venv/bin/python "$SCRIPTS/detect_text_watermark.py" detect wm.txt --scheme kgw --json ``` **Verification around a Layer B rewrite:** pass `--markllm-scheme` to `rewrite_text.py` (with `--markllm-dir`), and it records the MarkLLM detection before/after plus a `cleared` flag: ```bash export WATERMARKS_REWRITE_BACKEND=ollama WATERMARKS_REWRITE_MODEL=llama3.2 MARKLLM_DIR=~/MarkLLM \ python3 "$SCRIPTS/rewrite_text.py" wm.txt -o wm.rewritten.txt \ --markllm-scheme kgw --markllm-dir "$HOME/MarkLLM" --json-stats ``` If the backend is unconfigured or its deps are missing, the rewrite proceeds and the report notes verification was unavailable. A GPU is recommended; CPU runs work but are slow, and the model download is a few GB. Hardening knobs: - `--offline` on the adapter (or any MarkLLM run) loads the scoring model from the Hugging Face cache only — zero network egress; fails fast if not cached. Custom remote code is never executed (transformers `trust_remote_code` is never enabled). - `WATERMARKS_MARKLLM_RLIMIT_AS=` (env, POSIX) applies an address-space limit to the MarkLLM subprocess spawned by `rewrite_text.py`. Off by default because torch/CUDA usually needs large address spaces. - Config files are capped at 1 MiB; the upstream checkout and the base image are pinned by SHA/digest. ### Docker ```bash make docker-markllm-build docker run --rm --user "$(id -u):$(id -g)" -v "$(pwd):/data" \ watermarks-remover-markllm detect /data/wm.txt --scheme kgw --json ``` ## 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 + CtrlRegen removal (external) | Out of scope | Optional CtrlRegen removal (external) | | Training backdoors | Out of scope | Out of scope | Out of scope | Out of scope | Details: [`skills/remove-ai-marks/references/vendor-notes.md`](skills/remove-ai-marks/references/vendor-notes.md), [`mark-classes.md`](skills/remove-ai-marks/references/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 / WebP | C2PA chunks / APP11 / RIFF `C2PA`, AI XMP hints | Drop metadata segments | | SVG | ``, XMP | Strip blocks | | PDF | Byte/XMP + optional tools | **exiftool** then **qpdf**; degraded without either | | 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 | #### Why PDF needs qpdf, not just exiftool ExifTool writes PDFs **incrementally**. `exiftool -all=` appends a `%BeginExifToolUpdate` block that frees the Info object and drops `/Info` from the trailer — but the original metadata bytes stay in the file verbatim, and exiftool itself can undo the edit with `-PDF-update:all=`. The command exits `0`, viewers show no metadata, and the file gets *larger*, which is the tell. For a provenance-stripping tool that is a silent leak, so `clean_pdf` follows the exiftool pass with `qpdf --linearize`, which re-serializes the document from its object graph and drops the now-unreferenced objects. Without `qpdf` installed the clean still runs, but it says so: ``` warning: exiftool PDF edits are incremental — the original metadata bytes remain recoverable; install qpdf for a structural rewrite ``` 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. ### 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](https://github.com/contentauth/c2pa-rs/tree/main/cli), [Content Credentials verify](https://contentcredentials.org/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](https://deepmind.google/science/synthid/) / Vertex detector where offered); optional local [reverse-SynthID](https://github.com/aloshdenny/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](https://www.institutepm.com/knowledge-hub/ai-content-provenance-watermarking). --- ## 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](#disclaimer-what-removing-a-text-watermark-costs) | | 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 | Matrix: [`skills/remove-ai-marks/references/removal-matrix.md`](skills/remove-ai-marks/references/removal-matrix.md). ## Ethics and disclaimer See [`skills/remove-ai-marks/references/ethics.md`](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. ## Tests ```bash 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 ### Unreleased - Add stdlib-only WebP inspection and metadata cleaning for RIFF `C2PA`, XMP, EXIF, and ICC profile chunks - New optional MarkLLM harness (external `THU-BPM/MarkLLM` checkout, Apache-2.0): `detect_text_watermark.py` with `detect` / `watermark` subcommands for KGW and SynthID schemes - `rewrite_text.py --markllm-scheme` runs before/after detection around a Layer B rewrite (env-gated; reports `cleared`) - `setup_markllm.sh` bootstrap + `requirements-markllm.txt` (pinned deps) + `Dockerfile.markllm` and Makefile `bootstrap-markllm` / `smoke-markllm` / `docker-markllm-build` / `docker-markllm-help` - Mock-based tests (`tests/test_markllm_detect.py`, 21 cases) — no torch in CI - Docs: verification-harness caveat (same-config-only, not a vendor-detector oracle) in README, SKILL.md, `removal-matrix.md`, `vendor-notes.md` - Hardening: `--offline` cache-only model loading (no HF egress, no remote code), 1 MiB config cap, optional `WATERMARKS_MARKLLM_RLIMIT_AS` on the rewrite subprocess, pinned torch in the Dockerfile, and clone-SHA verification in `Dockerfile.markllm` ### [v0.4.0](https://github.com/guillaumemeyer/watermarks-remover/releases/tag/v0.4.0) — pixel removal, finding confidence, Windows & false-positive fixes **Optional CtrlRegen pixel removal (external backend)** - Optional pixel-domain watermark removal via an external `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-ctrlregen` - `clean_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 score - Conservative default strength `0.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 caps - Backend is never bundled: `noai-watermark` ships no LICENSE file (treated as all-rights-reserved), and its auto-install/restart code paths are bypassed by using `CtrlRegenEngine` directly **Finding confidence and aggregate audits** - Findings are now classified `confirmed` / `probable` / `informational` / `likely_false_positive`, exposed in text/image/container JSON and human reports - New `audit_dir.py` (recursive tree) and `audit_website.py` (sitemap discovery + crawl) aggregate reports; documented in SKILL.md **False-positive fixes** - DOCX: scan only `docProps`/`customXml`, not the visible body (#14) - Text Layer A: preserve emoji `VS16`/`ZWJ` after an emoji base; new `--strip-emoji-glue` paranoid flag (#22) - HTML: treat CMS generator tags as informational, not AI metadata (#13) - PDF: exclude stream payloads from the AI-marker byte scan (#13) - Inspect reports note unsupported/best-effort paths **Windows support** - Gate POSIX-only `preexec_fn` and `os.fchmod` so writes and optional tools run on Windows (#15, #23) - Reconfigure stdio to UTF-8 so redirected Windows streams no longer raise on invisible Unicode; Windows CI leg + CLI smoke run (#23) **Docs and supply chain** - README CtrlRegen section + research references (CtrlRegen, UnMarker, forensic-stealth caveat), responsible-use disclaimer; SKILL/matrix/vendor-notes/ethics updates - Dependabot config + security-path CODEOWNERS; bump scipy/numpy/opencv-python/scikit-learn/pywavelets and the base image to Python 3.14-slim - Mock-based CtrlRegen tests (no torch in CI) ### [v0.3.2](https://github.com/guillaumemeyer/watermarks-remover/releases/tag/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](https://github.com/guillaumemeyer/watermarks-remover/releases/tag/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](https://github.com/guillaumemeyer/watermarks-remover/releases/tag/v0.3.0) — optional SynthID pixel scoring - Optional pixel-domain SynthID scorer via an external [`aloshdenny/reverse-SynthID`](https://github.com/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](https://github.com/guillaumemeyer/watermarks-remover/releases/tag/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](https://github.com/guillaumemeyer/watermarks-remover/releases/tag/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](https://github.com/guillaumemeyer/watermarks-remover/releases/tag/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` ## License MIT — see [LICENSE](LICENSE). ## References - [How Claude marks AI-generated content](https://support.claude.com/en/articles/16266773-how-claude-marks-ai-generated-content) (Anthropic) - Dathathri et al., [*Scalable watermarking for identifying large language model outputs*](https://www.nature.com/articles/s41586-024-08025-4) (SynthID-Text, Nature 2024) - Google AI for Developers, [*SynthID safeguards*](https://ai.google.dev/responsible/docs/safeguards/synthid) (Gemini API docs) - [C2PA](https://c2pa.org/) / [c2patool](https://github.com/contentauth/c2pa-rs/tree/main/cli) - Kirchenbauer et al., [*A Watermark for Large Language Models*](https://arxiv.org/abs/2301.10226) - [THU-BPM/MarkLLM](https://github.com/THU-BPM/MarkLLM) (unified toolkit for evaluating LLM watermarking algorithms) - Zhang et al., [*Watermarks in the Sand: Impossibility of Strong Watermarking for Generative Models*](https://arxiv.org/abs/2311.04378) (ICML 2024) - [google-deepmind/synthid-text](https://github.com/google-deepmind/synthid-text) (research reference; not used for detection here) - [aloshdenny/reverse-SynthID](https://github.com/aloshdenny/reverse-SynthID) (research reference) - Liu et al., [*Image Watermarks are Removable Using Controllable Regeneration from Clean Noise*](https://arxiv.org/abs/2410.05470) (ICLR 2025) — the pixel-regeneration method the optional CtrlRegen backend implements — [code](https://github.com/yepengliu/CtrlRegen) - Kassis & Hengartner, [*UnMarker: A Universal Attack on Defensive Image Watermarking*](https://arxiv.org/abs/2405.08363) (arXiv:2405.08363; IEEE S&P 2025) — a universal watermark attack compared on a different metric than CtrlRegen - Goonatilake & Ateniese, [*Removing the Watermark Is Not Enough: Forensic Stealth in Generative-AI Watermark Removal*](https://arxiv.org/abs/2605.09203) (arXiv:2605.09203) — motivates the conservative-strength default: removal can still leave forensic traces - [mertizci/noai-watermark](https://github.com/mertizci/noai-watermark) (CLI/Python toolkit for SynthID/StableSignature/TreeRing removal and AI metadata stripping) - [0xROOTPLS/DeSynth](https://github.com/0xROOTPLS/DeSynth) (SynthID removal for OpenAI/Google images) - Institute of AI PM, [*AI Content Provenance and Watermarking: The PM's Guide to C2PA and SynthID*](https://www.institutepm.com/knowledge-hub/ai-content-provenance-watermarking) (two-layer industry model: C2PA + imperceptible watermark / soft binding; SB 942 / EU AI Act Art. 50 context)