v0.6.0 is a big breadth release: the cleaners now reach many more formats, Layer A (invisible-Unicode) scrubbing is much more thorough, and the skill can be installed and run as a Claude Code plugin with a deterministic PostToolUse hook so files get cleaned whether or not the model cooperates. On the detection side, Layer B rewriting is now iterative and evaluation-driven, and a new stdlib-only keyed-Gumbel (EXP) detector lets you verify a mark clears without a GPU, model, or logits.
Everything in v0.5.0 keeps working — the CLIs remain the primary local interface, and the HTTP service now offers batch endpoints for cleaning, inspecting, and detecting in bulk.
Format & container coverage
- AVIF / HEIC: native stdlib metadata and C2PA stripping (#84, #85)
- BMP / GIF / TIFF: stdlib detection, inspection, and metadata cleaning — GIF comment/XMP extensions dropped while
NETSCAPE2.0looping is preserved; TIFF IFD metadata (XMP/EXIF/GPS/IPTC/MakerNote) dropped with payloads zeroed and strip offsets kept (classic and BigTIFF); BMP trailing metadata truncated with the file-size field rewritten (#107) - EPUB: stdlib container cleaning — OPF metadata and XHTML meta/JSON-LD scrubbed, embedded raster/SVG media stripped, Layer A applied to XHTML body text, marker-carrying parts dropped, OCF-encrypted parts untouched (#107)
- XLSX / PPTX / DOCX (OOXML): native stdlib container metadata, text, and embedded media scrubbing; empty DOCX
docPropsprovenance fields; prune dangling relationships aftercustomXmlremoval; run Layer A over DOCX/ODT body text; decode XML entities before the Layer A scrub (#91, #100, #76, #83, #73, #80, #74, #81, #142) - SVG / ODT: linear-time metadata stripping (GHSA-7vpp-96qp-j9wh) (#147); recursively inspect and clean embedded raster data URIs in SVGs, HTML, and Markdown (#87, #88)
- Audio / video: AI/C2PA metadata stripping for MP4/MOV, WAV, and MP3 (#139); WAV RIFF C2PA chunk removal; FLAC C2PA support; reject partial ID3v2 frame parsing (#232); preserve MP4 media offsets when stripping metadata (#183)
- PDF: reach metadata inside embedded images and stop resizing the PDF to strip XMP; run the deep-image pass even without exiftool; honour JPEG marker fill bytes and share one segment walker
- PNG: detect AI generator product names in PNG text metadata; detect AI markers in compressed PNG text (#127); keep the truncated tail instead of dropping it in png/isobmff strips (#182)
Layer A (invisible Unicode) hardening
- Consolidated Layer A hardening (#133): strip reserved
Default_Ignorablecode points with no legitimate interchange use (U+2065,U+FFF0–U+FFF8,U+E0000,U+E0080–U+E00FF,U+E01F0–U+E0FFF), the 66 noncharacters, and three blank-rendering Default_Ignorable carriers theCfcatch-all never saw (U+180F,U+3164,U+FFA0) — each with the same in-context preservation as its already-covered siblings, in both the service engine and the vendored lightweight-skill copy - Stop stripping visible-layout format controls next to their own script: Egyptian hieroglyph quadrat controls (
U+13430–U+1343F), Duployan shorthand controls (U+1BCA0–U+1BCA3), and musical beam/tie/slur/phrase controls (U+1D173–U+1D17A) are preserved when adjacent to their own script and still stripped (and flagged) when floating between unrelated text;--strip-emoji-glueparanoid mode still strips them everywhere - Emoji / script polish: preserve VS16 after emoji singletons outside the block ranges; preserve script joiners, flag emoji, and Arabic Cf marks; preserve multilingual Unicode during text cleanup (#34)
Layer B rewriting & watermark detection
- Iterative, detection-guided Layer B rewriting: each round generates
--candidatesvariants (default 1,WATERMARKS_REWRITE_CANDIDATES) and--max-loopscaps evaluation rounds, stopping as soon as an attempt passes detection. Evaluator priority: MarkLLM (--markllm-scheme) > bigram-Jaccard lexical divergence.rewrite_text.py --json-statsnow reportsevaluator/max_loops/attempts_made/passedand per-attemptcandidate_scores(#153) - Keyed-Gumbel (Aaronson EXP) same-key verification: new stdlib-only
detect_gumbel.py(no GPU, model, or logits) implements the model-free replay test with an exact Gamma-tail p-value and repeated-window masking.rewrite_text.py --gumbel-key(envWATERMARKS_GUMBEL_KEY, preferred) makes it the iterative-loop evaluator (priority: gumbel > markllm > lexical divergence), and it's exposed asgumbelin/capabilitiesand/detect(#190). Same-key-only, not a vendor oracle; the key is never logged - Benchmarks: multi-scheme MarkLLM text benchmark and detection (#188); reproducible SynthID-text removal benchmark (#145, default variants
paraphrase:3) with attempts-per-document in the report and CSV;--rewrite-loopsmirrors--max-loops - Detection: vendor text-watermark detection (Gemini SynthID, Claude seam, MarkLLM) plus a SynthID image scorer sidecar (#109); zero-LLM statistical and stylometric AI text detector for CI and audits (#68, #69)
Distribution: plugin, hooks, and skill installs
- Claude Code plugin & marketplace (
.claude-plugin/plugin.json+marketplace.json) — both skills install with/plugin marketplace add guillaumemeyer/watermarks-removerthen/plugin install watermarks-remover@watermarks-remover, and update in place.make plugin-validaterunsclaude plugin validate . --strict install_skill.pygrew--target(claude-code,claude-project,cowork,cursor) and a--skillselector, plus--list,--link, andCLAUDE_CONFIG_DIR. Thecoworktarget builds a reproducible upload bundle (dist/<skill>.zip) with a single top-level skill directory and validates against the Agent Skills packaging rules and the 30 MB upload limit- Deterministic
PostToolUsehook (hooks/hooks.json+hook_written_file.py): after the agent writes a file the harness runs the hook whether or not the model cooperates.check(default) reports marks to the model;cleanstrips them in place and tells the model the file moved, swapping only on a real difference so clean files keep their mtime. Mode comes from the plugin'shook_modesetting orWATERMARKS_HOOK_MODE. A hook still can't rewrite the assistant's chat message — no such hook point exists - Pre-commit hook integration for staged-file checking/cleaning (#138); lightweight Cursor text skill (#35)
HTTP service
- Batch endpoints:
POST /clean/batchand/inspect/batch(#137) plusPOST /detect/batch(#151) - Preserve image format extensions in
/cleanand use safe writes inav_meta(#150)
Audit, security & reliability
audit_dir.pygained multi-worker concurrency and SARIF 2.1.0 export (#101, #102)- Security: refuse DTD/entity bombs in the sitemap parser (GHSA-pjg6-92pm-mmcf) (#146); route website binary formats to their real scanners (#177)
- Reliability: a second
--in-placerun preserves the original.bak; keep collected evidence when a later zip member fails to read (#175); truncated ISOBMFF containers still run the C2PA byte-scan fallback (#176); distinguish a failed cleaner from an already-clean file (#159, #161); treat a failed c2patool run as inconclusive rather than "no C2PA" (#156); an unreadable text file is a failed scan, not a clean one (#169); validate clean option types (#111); never auto-select MPS device for text watermark detection (#99); Windowssubprocess_creationflagsfixes and no child console window
CI, tooling & docs
- Ruff linting and formatting with CI enforcement (#103); macOS added to the test matrix (#152); CodeRabbit config (#222); CODEOWNERS for CODE_OF_CONDUCT/LICENSE and main-review owners; copyright attributed to Guillaume Meyer and contributors (#228)
- Docs: voice-preserving rewrite guidance; Ecosystem additions (ClaudeWatermarks, unmark-web) plus a note discouraging look-alike names; arXiv 2402.14904 reference; Windows auto-start guide; portable base64 curl examples; Cursor-skill text engine pinned to the service copy (#96)
A big thanks to everyone who filed issues, reviewed PRs, and ran the harnesses — this release is a team effort. 🎉
Full changelog: README