Skip to content

Instantly share code, notes, and snippets.

@ruvnet
Last active August 16, 2026 01:41
Show Gist options
  • Select an option

  • Save ruvnet/b87f9055710e2aadf0ee3a8c360c476d to your computer and use it in GitHub Desktop.

Select an option

Save ruvnet/b87f9055710e2aadf0ee3a8c360c476d to your computer and use it in GitHub Desktop.
The Invisible Signature — how AI text watermarking works (plain-language guide to @claude-flow/watermark)

The Invisible Signature — how AI text watermarking works

A plain-language guide to the method behind ai-text-watermark / @claude-flow/watermark. Live playground & animated explainer: https://ruvnet.github.io/ai-text-watermark/ · Source: https://github.com/ruvnet/ai-text-watermark


The problem

As of August 2026 the EU AI Act asks AI providers to mark AI-generated text so it can be recognised later. But a paragraph is just words — there's nowhere to stamp a logo without wrecking the sentence. The mark has to hide inside the choices the model was already making: invisible to a reader, unmistakable to anyone with a secret key.

The one idea you need

A language model writes one word at a time. At most positions, several next words are equally good:

"The weather today is cold and overcast / grey…"

A reader won't notice or care which one lands. (It would never pick "sugary" — that was never in the running.) The watermark only ever plays inside that tie. It never reaches for a strange word the model wouldn't have used anyway — so the writing quality is untouched.

The trick: change where the "dice" come from

Normally the model breaks a tie with a private roll of the dice — pure chance. Watermarking swaps the dice for something that looks just as random but isn't: a secret key plus the words that just came before decide which tied word to pick.

The Monopoly analogy. Imagine a game where, instead of rolling, every player reads the next digit of π for their move. The moves are still effectively random — the game plays out identically — but if you knew π, you could look back and prove the game had followed it. Watermarked text is that game.

Crucially, it isn't biased toward any one word. "overcast" might win here, "grey" the next time — it depends entirely on the key and the preceding words.

Reading the signature back

Later, someone with the same key re-walks the text and asks at every tie: is this the word the key would have chosen?

  • In ordinary human writing: "sometimes, by chance."
  • In watermarked text: "yes — far more often than luck allows."

Stack up enough of those little agreements and the odds of coincidence collapse. That's the whole detector — a confidence score that climbs as the text gets longer.

See it happen: the live playground generates a watermarked stream in your browser (real WebAssembly, nothing leaves the page) and shows the confidence land around a one-in-a-billion z-score — while a wrong key sits at zero.

What it honestly can't do

  • Short or factual text carries almost no mark. "Newton's masterwork was the Principia…" has exactly one right next word — no tie to hide in. Same for most code, and for a light proofread. Fewer choices → fainter signature.
  • A wrong key sees nothing. The mark is specific to whoever made it, and it carries no information about you — not your identity, account, or chat. It answers one question only: was a keyed model likely involved? Not who, not when.
  • It is not a way to launder AI text into looking human. This toolkit only writes and checks marks. If you legitimately need un-marked output of your own model, you simply don't apply the mark — you never strip a finished text.

Three flavours

scheme trade-off
gumbel Provably changes the model's word odds by zero. The gentlest signature.
tournament The SynthID method — a stronger mark for a whisper of extra bias.
tournament_nd The middle path: unbiased on average, still firm.

Plus two specialist detectors: one that survives editing and rearranging, and one that squeezes confidence out of very short passages.

Hide a secret message (multi-bit provenance)

The mark can carry more than one bit. Split a short payload — a model id, an author handle, a run tag — into bits, and encode each bit by which of two key-derived streams watermarks a block of tokens. To read it back, run the detector on each block under both keys and take the stronger; the blocks spell the message out. It needs only the key and block size — never the original text.

The project dogfoods this: the playground embeds and recovers the provenance payload ruvnet end-to-end, 100% of the bits. Same honest boundary as everything else — the payload is provenance (who/what/which run), never end-user identity, and it's applied at generation, not stamped onto finished prose.

Watermark a live stream (ultra-low-latency proxy)

For real serving, StreamProxy drops into a decode loop between the model and the emitted token. Give it a step's raw logits (what a serving stack produces) or a truncated top-k (ids, logprobs) set (what an OpenAI-compatible API returns); it applies temperature + top-k/top-p to match your sampler, watermarks the candidate set, and returns the token id to emit. Scratch buffers are reused, so the per-token cost is fixed and allocation-free after warmup — the mark rides the sampling you already do.

const { StreamProxy, detect } = require('ai-text-watermark');
const proxy = new StreamProxy({ key: 'my-secret', scheme: 'gumbel', temperature: 0.9, topK: 40, topP: 0.95 });
const out = [];
for (const logits of decodeSteps) out.push(proxy.pushLogits(logits)); // logits → watermarked token id
detect(Uint32Array.from(out), { key: 'my-secret', scheme: 'gumbel' }).isWatermarked(1e-6); // true

Try it — ten lines, no native build

It ships as a WebAssembly module. Node or browser, nothing to compile.

npm install ai-text-watermark          # standalone name
# or: npm install @claude-flow/watermark   (same package, canonical name)
# or, in Rust: cargo add ruflo-watermark
const { Watermarker, detect } = require('ai-text-watermark');

// The watermarker sits between the model and the sampled token: you give it the
// model's candidate token ids + their probabilities each step; it returns which
// candidate to emit.
const wm = new Watermarker({ key: 'my-secret', scheme: 'gumbel' });
const out = new Uint32Array(600);
for (let i = 0; i < out.length; i++) out[i] = candidates[wm.step(candidates, probs)];
wm.free();

// Later, with the same key:
const r = detect(out, { key: 'my-secret', scheme: 'gumbel' });
console.log(r.zScore, r.isWatermarked(1e-6)); // strong signal, true

In a browser it's the same API after one async init:

import { init, Watermarker, detect } from 'ai-text-watermark/web';
await init(); // auto-fetches the wasm

Detectors return { zScore, pValue, log10P, scoredPositions, isWatermarked(alpha) }. Confidence grows with the number of low-stakes word choices — so give the detector length.

Where it lives

what where
Live playground + animated explainer https://ruvnet.github.io/ai-text-watermark/
Source, README, ADRs https://github.com/ruvnet/ai-text-watermark
npm (standalone) ai-text-watermark
npm (canonical) @claude-flow/watermark
Rust crate ruflo-watermark (crates.io)

Design decisions are recorded as ADRs in the repo: the watermarking component (383), the bounded-evolution detector tuner (384), the ultra-low-latency proxy (385), multi-bit secret message (386), and the proposed gateway service + OAuth-gated generation (387/388).


Method: SynthID-Text (Dathathri et al., Nature 2024) and the Aaronson (2022) / Kuditipudi et al. (2024) distortion-free family. Provenance, not surveillance — a mark that says "a machine helped here," and nothing else.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment