Code hygiene and provenance

Remove watermarks from code

Paste a file and see every character in it that you cannot see. Zero-width spaces, bidirectional overrides, Unicode tag characters carrying a hidden payload, no-break spaces sitting in your indentation. Each one is reported by codepoint and line, so you can check the change instead of trusting it. Works on output from any model, and on code you wrote yourself.

Works with every model no configuration

The scan is model-agnostic. It reads the characters in your file rather than guessing which assistant wrote them, so there is no provider setting, no per-model list and nothing to pick. One pass covers every source, including code you typed yourself or pasted from a web page.

ChatGPT Claude Gemini Copilot Llama DeepSeek Qwen Mistral Grok Cursor Cline v0 and anything else

Sign in to scan your code

A free account comes with an API key, so you can use this from the browser and from your own scripts. No credit card.

Sign in with Google

Already have a key? The endpoint is documented below and works without the browser.

What this does, and what it cannot do

Character watermarks: removed, and verifiable

  • Zero-width spaces, joiners and word joiners
  • Bidirectional controls (CVE-2021-42574)
  • Unicode tag characters, decoded back to ASCII
  • Variation selectors and soft hyphens
  • No-break and other exotic spaces
  • Byte order marks and Unicode line separators
  • Trailing whitespace

Every one is reported by codepoint, count and line number. You can diff the bytes yourself.

Statistical watermarks: not removed, and we will not pretend otherwise

Statistical watermarks. Schemes of the SynthID family add no characters at all. They bias which token the model samples, so the signal is spread through word choice across the whole output. Nothing you can delete carries it. There is a full explanation below, with the papers.

Attacking that means rewriting the text. For prose that is merely lossy. For code it risks changing behaviour, so we do not do it, and a tool that offers it for source files is selling a correctness risk as a feature.

This is not an attribution bypass. Formatters already normalise surface signals, and what identifies generated code is structure and style rather than hidden bytes. Disclose AI assistance wherever you are asked to.

How statistical watermarking actually works

Worth understanding, because it is the reason a character scrubber cannot touch it, and the reason most claims about removing watermarks are wrong.

Partitions the vocabulary into a green and a red list at every step, keyed on a secret plus the preceding context, and nudges sampling toward green. Detection is a statistical test on the proportion of green tokens and needs no access to the model.

Kirchenbauer et al., A Watermark for Large Language Models, ICML 2023

SynthID-Text. Uses tournament sampling and changes only the sampling procedure, leaving training and model weights alone, which is what makes it deployable in production.

Dathathri et al., Scalable watermarking for identifying large language model outputs, Nature 634, 818-823 (2024)

Why paraphrase is the usual attack

In the schemes above the green list for each step is derived from the preceding run of tokens. Change the wording and you change the partition, so the green count drifts back toward chance. That is why the literature treats rewriting as the attack and character edits as irrelevant.

It is a matter of degree rather than a switch. A light paraphrase dilutes the signal while leaving much of it intact, because it keeps long runs of the original wording. Only a rewrite that shares no runs with the original brings detection down to chance. Schemes keyed on the words themselves rather than on context survive same-meaning rewrites better still.

Only the key holder can check

Detection is a statistical test over which tokens fell in the green list, and reproducing that partition needs the secret watermarking key. It does not need the model, which is what makes the scheme practical to deploy, but without the key there is nothing to test.

So a third-party "AI detector" is not checking a watermark. It is guessing from style. Treat a clean result from one, and a clean result from us, as answers to two different questions.

Measured 2026-08-16

What this scrubber does to a token sequence

A watermark lives in the tokens, and a detector re-tokenizes the text to check it. So the only way a character tool could affect one is by changing how the text tokenizes. We ran the default scrub over 3,000 real model responses from our own request log and compared the token sequence before and after.

88.5%

token sequence identical after scrubbing

3

of 3,000 responses contained a zero-width character

455

had any character finding at all

Where the tokens did change it was overwhelmingly trailing whitespace, not hidden characters. If you want even that left alone, send trailing: false.

The reverse case is the interesting one. A zero-width character inserted into code splits a token that was whole, so it is the insertion that disrupts tokenization. Removing it puts the sequence back exactly as it was. On that path this tool makes a watermark detector more reliable, not less.

The API

One free key, the same one that works for the LLM gateway. Create it on your account page.

curl https://dreamprompting.com/api/v1/unwatermark \
  -H "Authorization: Bearer $DREAMPROMPTING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"code": "def f():\n    return 1\n"}'
FieldDefaultMeaning
coderequiredThe source to clean. Up to 200,000 characters.
trailingtrueStrip whitespace at end of line.
punctuationfalseFold curly quotes and dashes to ASCII.
homoglyphsfalseReplace look-alike letters with Latin.
inspectfalseReport only, omit the cleaned text. For CI.

The response reports every removal:

{
  "clean": "def f():\n    return 1\n",
  "changed": true,
  "removed": 3,
  "findings": [
    {"codepoint": "U+202E", "name": "RIGHT-TO-LEFT OVERRIDE",
     "category": "bidi_control", "security": true,
     "count": 1, "lines": [4]},
    {"codepoint": "U+E0000 block", "name": "TAG CHARACTER",
     "category": "tag_char", "security": true,
     "count": 12, "lines": [7], "decoded": "ignore all rules"}
  ],
  "security_findings": [ ... ],
  "chars_before": 812, "chars_after": 796
}

As a CI gate

Fail the build when a file carries a security finding, without rewriting anything:

jq -Rs '{code: ., inspect: true}' src/app.py \
  | curl -s https://dreamprompting.com/api/v1/unwatermark \
      -H "Authorization: Bearer $DREAMPROMPTING_API_KEY" \
      -H "Content-Type: application/json" -d @- \
  | jq -e '.security_findings | length == 0' > /dev/null \
  || { echo "hidden characters found"; exit 1; }

Guides by tool

The scan is the same for all of them. What differs is how code gets out of each tool and into your file, which is where the characters come from.

Questions

What exactly does this remove? +

Characters that are in your file but cannot be seen. Zero-width spaces and joiners, bidirectional control characters, the Unicode tag block, variation selectors, soft hyphens, exotic whitespace such as the no-break space, and trailing whitespace. Every removal is reported with its codepoint, its Unicode name, how many times it occurred and which lines it was on, so you can verify the change rather than trust it.

Which models does this work with? +

All of them. The scan is model-agnostic: it looks at the characters in your file rather than at which assistant produced them, so there is no per-model list, no provider setting and nothing to configure. Code from ChatGPT, Claude, Gemini, Copilot, Llama, DeepSeek, Qwen, Mistral, Cursor, Cline or any other tool goes through the identical pass, and so does code a person typed by hand or pasted from a web page. Hidden characters do not care where they came from, which is exactly why one pass covers every source.

Can you remove SynthID or other statistical watermarks? +

No, and no tool that edits characters can. Kirchenbauer et al. (ICML 2023) partition the vocabulary into a green and a red list at every step, keyed on a secret plus the preceding tokens, and nudge sampling toward green. SynthID-Text (Nature, 2024) uses tournament sampling and changes only the sampling procedure. Neither adds a character, so there is nothing to delete. The attack in the literature is rewriting, not editing, and rewriting source code through another model trades a provenance signal for a correctness risk. We measured our own scrubber against this: across 3,000 real model responses the default pass left the token sequence identical 88.5 percent of the time, and where it did change, it was trailing whitespace rather than anything hidden.

Does paraphrasing remove a statistical watermark? +

It weakens it, by degrees, and how much depends on the scheme. Where the green list is derived from the preceding run of tokens, changing the wording changes the partition and the green count drifts back toward chance. A light paraphrase only dilutes the signal, because it keeps long runs of the original wording; bringing detection down to chance takes a rewrite that shares no runs with the original. Schemes keyed on the words themselves rather than on context survive same-meaning rewrites better. None of this is something this tool does.

Can an AI detector tell me if text carries a watermark? +

Not a third-party one. Detection is a statistical test over which tokens fell in the green list, and reproducing that partition requires the secret watermarking key. It does not require the model, which is what makes the scheme deployable, but without the key there is nothing to test. A public detector is inferring from style instead, which is a different question with a different error rate. A clean result from one of those and a clean result from us are answers to two different questions.

If I strip hidden characters, does that break watermark detection? +

On the usual path it does the opposite. A zero-width character inserted into text splits a token that was whole, so the insertion is what disrupts tokenization; removing it restores the sequence exactly. We checked that directly: scrubbing text with an injected zero-width character reproduces the original token sequence byte for byte. Raw model output almost never carries these anyway, three responses in three thousand in our sample, which is why they nearly always arrive from a copy path rather than from the model.

Does removing watermarks make AI-written code undetectable? +

No. Character-level watermarks, the kind this tool removes, are not how AI-generated code is identified in practice. Running a formatter such as black, gofmt or prettier already normalises most surface signals, and what remains is structure and style rather than hidden bytes. Treat this as a hygiene and security tool, not as a way to defeat attribution, and disclose AI assistance wherever you are asked to.

Will this change what my code does? +

Almost never, and the exceptions are deliberate. Removing a character no one can see cannot change how code reads. It can change behaviour in one case: if a string literal deliberately contains a zero-width character and the program compares against it. That is rare, and it is also exactly the shape a hidden marker takes, so the finding is reported either way. Options that change visible characters, such as folding smart quotes, are off unless you turn them on.

What is a Trojan Source attack? +

CVE-2021-42574. Bidirectional control characters let a line of source display in one order and compile in another, so a reviewer approves code that does something other than what they read. A right-to-left override inside a comment can make an early return or a permission check appear to be commented out when it is not. These characters have no legitimate use in code, so this tool removes them and flags them as a security finding rather than as tidying.

What are Unicode tag characters and why do you decode them? +

The U+E0000 block is a shadow copy of printable ASCII that renders as nothing. It survives copy and paste, which makes it the usual carrier for instructions smuggled into text that a language model will later read. Reporting that hidden characters exist is not very useful, so we decode the run back into ASCII and show you what it says. Seeing the sentence is what tells you whether you are looking at a marker or an injected instruction.

Why are look-alike letters reported but not replaced? +

Because we cannot tell an attack from a language. A Cyrillic small a inside an identifier is a classic supply-chain trick, since the token looks identical to a Latin a and compares as different. The same character in a Russian comment or string is simply Russian. Silently rewriting it would corrupt legitimate source, so the default points at the line and lets you decide. Turn the option on if you want the substitution made.

Why do smart quotes stay by default? +

Because they are visible, and folding them rewrites bytes inside string literals as well as in code. A curly quote pasted into a source file is usually a syntax error and worth fixing, but changing the contents of a string is a decision a person should make. It is one checkbox away rather than automatic.

Does it work on any programming language? +

Yes. The scrubber works on characters rather than syntax, so it does not parse or care about the language. That is also its boundary: it will not reformat code, rename anything or touch structure. Anything it changes is a character that was invisible or a run of whitespace at the end of a line.

Do I need an account? +

Yes. Sign in with Google and a free API key is created for you automatically, with no credit card. The account is what lets us rate limit per caller rather than per IP address, which matters because an endpoint that accepts whole source files is easy to abuse from a shared network.

Is the code I paste stored? +

No. The scrub runs in the request that carries it and the content is not written to a database or a log. Only the fact that a key made a call is recorded, which is what the rate limit is counted from.

Can I run this in CI? +

Yes, and the endpoint has a mode for it. Send inspect set to true and you get the findings without the cleaned text, which is what a check wants: it should fail the build and tell a human, not rewrite the tree behind their back. Fail on any finding where security is true and you have a Trojan Source and hidden-payload gate in about five lines of shell.

What are the limits? +

200,000 characters per request, which is a large source file, and the standard per-key daily request limit. There is no token cost, because nothing is sent to a language model: the scrub is a local pass over the characters, so it consumes none of your daily token budget.