Build a Claude Watermark Detector in Python
In this tutorial, you will build a working watermark detector from scratch in Python. We will implement the core statistical test, compute green list fractions, calculate z-scores, and create a command-line tool that can analyze any text for Claude watermark patterns. By the end, you will understand exactly how detection works at the code level and have a reusable tool for your own projects.
Prerequisites
This tutorial assumes intermediate Python knowledge and familiarity with basic statistics. You will need Python 3.8 or later, pip for package management, and a text editor or IDE. We will use numpy for statistical calculations and hashlib from the standard library for the hashing function. No GPU or machine learning frameworks are required since the detection algorithm is purely statistical and runs on CPU in milliseconds.
pip install numpy
Step 1: Understanding the Token Partition
The foundation of watermark detection is the vocabulary partition function. At each token position, the vocabulary is split into green and red lists using a hash of the preceding context. We will simulate this with a simplified version that hashes the previous token to determine group membership. In a real implementation, you would need access to the exact hash function used by the watermarking system, but for demonstration purposes, our approximation captures the statistical mechanics correctly.
class="syn-keyword">import hashlib
class="syn-keyword">import numpy as np
class="syn-keyword">from typing class="syn-keyword">import List, Tuple
class="syn-keyword">def partition_vocabulary(context_token: str, vocab_size: int = 50000) -> set:
class=class="syn-string">"syn-string">""class=class="syn-string">"syn-string">"Determine the green list class="syn-keyword">for a given context token.
Uses SHA-256 hash of the context token to deterministically
assign approximately half the vocabulary to the green list.
"class=class="syn-string">"syn-string">""
green_list = set()
class="syn-keyword">for token_id class="syn-keyword">in range(vocab_size):
class=class="syn-string">"syn-comment"># Hash the context + candidate token
hash_input = fclass=class="syn-string">"syn-string">"{context_token}:{token_id}".encode()
hash_value = hashlib.sha256(hash_input).hexdigest()
class=class="syn-string">"syn-comment"># Use first 8 hex chars as a number, check class="syn-keyword">if even
class="syn-keyword">if int(hash_value[:8], 16) % 2 == 0:
green_list.add(token_id)
class="syn-keyword">return green_list
Step 2: Tokenizing the Input
For our detector, we need to convert text into tokens. A production system would use the exact tokenizer matching the model that generated the text, but for demonstration we will use a simple whitespace-based tokenizer. The important thing is consistency: the same tokenization scheme must be used for both the context hashing and the green fraction calculation. Each token gets assigned a numeric ID by hashing its string representation, simulating a vocabulary lookup.
class="syn-keyword">def simple_tokenize(text: str) -> List[str]:
class=class="syn-string">"syn-string">"""Split text into word-level tokens.
A production detector would use the model&class=class="syn-string">"syn-comment">#39;s actual tokenizer
(e.g., tiktoken class="syn-keyword">for Claude). This simplified version demonstrates
the statistical mechanics without requiring the real tokenizer.
class=class="syn-string">"syn-string">""class=class="syn-string">"syn-string">"
class=class="syn-string">"syn-comment"># Split on whitespace and punctuation boundaries
class="syn-keyword">import re
tokens = re.findall(r"[\w&class=class="syn-string">"syn-comment">#39;]+|[.,!?;:\-]", text.lower())
class="syn-keyword">return tokens
class="syn-keyword">def token_to_id(token: str, vocab_size: int = 50000) -> int:
class=class="syn-string">"syn-string">""class=class="syn-string">"syn-string">"Map a token string to a numeric ID via hashing."class=class="syn-string">"syn-string">""
hash_val = hashlib.md5(token.encode()).hexdigest()
class="syn-keyword">return int(hash_val[:8], 16) % vocab_size
Step 3: Computing the Green Fraction
The green fraction is the core measurement. For each token in the text (starting from the second token, since the first has no preceding context), we check whether it falls in the green list determined by the preceding token. We then count the total number of green hits and divide by the total number of tokens checked. Under the null hypothesis of unwatermarked text, this fraction should be approximately 0.5. Watermarked text will show a significantly higher green fraction.
class="syn-keyword">def compute_green_fraction(tokens: List[str], vocab_size: int = 50000) -> Tuple[float, int, int]:
class=class="syn-string">"syn-string">""class=class="syn-string">"syn-string">"Compute the fraction of tokens that fall class="syn-keyword">in their green lists.
Returns:
green_fraction: proportion of tokens class="syn-keyword">in green list
green_count: number of green list tokens
total_checked: total tokens evaluated
"class=class="syn-string">"syn-string">""
green_count = 0
total_checked = 0
class="syn-keyword">for i class="syn-keyword">in range(1, len(tokens)):
context = tokens[i - 1]
current_id = token_to_id(tokens[i], vocab_size)
green_list = partition_vocabulary(context, vocab_size)
class="syn-keyword">if current_id class="syn-keyword">in green_list:
green_count += 1
total_checked += 1
green_fraction = green_count / total_checked class="syn-keyword">if total_checked > 0 class="syn-keyword">else 0.5
class="syn-keyword">return green_fraction, green_count, total_checked
Step 4: The Statistical Test
With the green fraction computed, we apply a one-proportion z-test to determine whether the observed proportion is significantly greater than 0.5. The z-score measures how many standard deviations the observed green fraction is above the expected value under the null hypothesis. A higher z-score means stronger evidence of watermarking. We use a one-tailed test because watermarking only increases the green fraction, never decreases it. The p-value gives us the probability of observing such a high green fraction by chance in unwatermarked text.
class="syn-keyword">def compute_z_score(green_fraction: float, n: int) -> Tuple[float, float]:
class=class="syn-string">"syn-string">"""Compute z-score and p-value class="syn-keyword">for the watermark test.
H0: green_fraction = 0.5 (no watermark)
H1: green_fraction > 0.5 (watermark present)
Returns:
z_score: standard deviations above expected
p_value: probability of this result under H0
class=class="syn-string">"syn-string">"""
class="syn-keyword">from scipy class="syn-keyword">import stats
expected = 0.5
std_error = np.sqrt(expected * (1 - expected) / n)
z_score = (green_fraction - expected) / std_error
p_value = 1 - stats.norm.cdf(z_score)
class="syn-keyword">return z_score, p_value
Step 5: Putting It All Together
Now we combine all the components into a complete detector function that takes raw text as input and returns a structured result with the watermark probability, z-score, green fraction, and a human-readable verdict. The detection threshold of z=4.0 corresponds to a false positive rate below 0.003%, which provides excellent reliability for practical use. You can adjust this threshold based on your specific requirements for sensitivity versus specificity.
class="syn-keyword">def detect_watermark(text: str, threshold: float = 4.0) -> dict:
class=class="syn-string">"syn-string">""class=class="syn-string">"syn-string">"Detect whether text contains a Claude watermark.
Args:
text: the text to analyze
threshold: z-score threshold class="syn-keyword">for positive detection
Returns:
dict with detection results
"class=class="syn-string">"syn-string">""
tokens = simple_tokenize(text)
class="syn-keyword">if len(tokens) < 20:
class="syn-keyword">return {
class=class="syn-string">"syn-string">"watermarked": False,
class=class="syn-string">"syn-string">"confidence": 0.0,
class=class="syn-string">"syn-string">"z_score": 0.0,
class=class="syn-string">"syn-string">"green_fraction": 0.5,
class=class="syn-string">"syn-string">"tokens_analyzed": len(tokens),
class=class="syn-string">"syn-string">"verdict": class=class="syn-string">"syn-string">"Text too short class="syn-keyword">for reliable detection"
}
green_frac, green_count, total = compute_green_fraction(tokens)
z_score, p_value = compute_z_score(green_frac, total)
watermarked = z_score > threshold
confidence = min(1.0, max(0.0, (z_score - 2) / 6))
class="syn-keyword">if z_score > 6:
verdict = class=class="syn-string">"syn-string">"Strong watermark detected"
class="syn-keyword">elif z_score > threshold:
verdict = class=class="syn-string">"syn-string">"Watermark likely present"
class="syn-keyword">elif z_score > 2:
verdict = class=class="syn-string">"syn-string">"Weak signal, possibly watermarked"
class="syn-keyword">else:
verdict = class=class="syn-string">"syn-string">"No watermark detected"
class="syn-keyword">return {
class=class="syn-string">"syn-string">"watermarked": watermarked,
class=class="syn-string">"syn-string">"confidence": round(confidence, 4),
class=class="syn-string">"syn-string">"z_score": round(z_score, 4),
class=class="syn-string">"syn-string">"p_value": p_value,
class=class="syn-string">"syn-string">"green_fraction": round(green_frac, 4),
class=class="syn-string">"syn-string">"green_count": green_count,
class=class="syn-string">"syn-string">"tokens_analyzed": total,
class=class="syn-string">"syn-string">"verdict": verdict
}
Step 6: Command-Line Interface
Finally, let us wrap the detector in a command-line interface so you can analyze text files directly from your terminal. The CLI accepts either a file path or piped stdin, making it easy to integrate into shell scripts and automated pipelines. The output is formatted as JSON for easy parsing by other tools, and a human-readable summary is printed to stderr for interactive use.
class="syn-keyword">import sys
class="syn-keyword">import json
class="syn-keyword">def main():
class="syn-keyword">if len(sys.argv) > 1:
with open(sys.argv[1], &class=class="syn-string">"syn-comment">#39;r39;) as f:
text = f.read()
class="syn-keyword">else:
text = sys.stdin.read()
result = detect_watermark(text)
class=class="syn-string">"syn-comment"># JSON output to stdout
print(json.dumps(result, indent=2))
class=class="syn-string">"syn-comment"># Human-readable summary to stderr
print(fclass=class="syn-string">"syn-string">"\n--- Watermark Detection Report ---", file=sys.stderr)
print(f"Tokens analyzed: {result[&class=class="syn-string">"syn-comment">#39;tokens_analyzed39;]}class=class="syn-string">"syn-string">", file=sys.stderr)
print(f"Green fraction: {result[&class=class="syn-string">"syn-comment">#39;green_fraction39;]:.1%}class=class="syn-string">"syn-string">", file=sys.stderr)
print(f"Z-score: {result[&class=class="syn-string">"syn-comment">#39;z_score39;]:.2f}class=class="syn-string">"syn-string">", file=sys.stderr)
print(f"Verdict: {result[&class=class="syn-string">"syn-comment">#39;verdict39;]}class=class="syn-string">"syn-string">", file=sys.stderr)
class="syn-keyword">if __name__ == "__main__":
main()
Running the Detector
Save the complete script and run it against any text file. The detector processes text in milliseconds on standard hardware since it performs only hashing and arithmetic operations without any neural network inference. For production use, consider switching to the real model tokenizer and calibrating the hash function against known watermarked samples. The Claude Watermark Remover API handles all of these production concerns automatically if you prefer a managed solution.
"syn-comment"># Analyze a text file
python detector.py article.txt
"syn-comment"># Pipe text from clipboard
pbpaste | python detector.py
"syn-comment"># Use with the API for comparison
curl -s https:"syn-comment">//api.claudewatermark.org/v1/text/scan \
--data-urlencode "syn-string">"text@article.txt" | jq .
Next Steps
You now have a working watermark detector that demonstrates the core statistical principles behind Claude watermark detection. To improve accuracy for production use, you would need access to the specific hash function and tokenizer used by Anthropic's watermarking system, or use our API which handles these details internally. Check out the full API documentation for programmatic detection and removal, or explore the technical deep dive article for more background on how distributional watermarking works at a theoretical level.