An open-source replication and extension of Anthropic's Constitutional Classifiers++ (CC++, January 2026) on Google's Gemma 3 models. A linear probe on frozen Gemma activations beats ShieldGemma (Google's 2B–27B safety classifier) on every public benchmark, at ~1000× fewer parameters and ~$5 total compute.
| Model | Params | ToxicChat F1 | OpenAI Mod F1 |
|---|---|---|---|
| OpenAI Mod API | ? | 0.254 | 0.782 |
| LlamaGuard 1 | 7B | 0.616 | 0.758 |
| LlamaGuard 2 | 8B | 0.471 | 0.761 |
| GPT-4 (as classifier) | ~1.8T | 0.683 | 0.810 |
| ShieldGemma 2B | 2B | 0.704 | 0.825 |
| ShieldGemma 9B | 9B | 0.694 | 0.828 |
| WildGuard | 7B | 0.708 | 0.721 |
| Aegis-Guard-Permissive | 7B | 0.730 | 0.747 |
| ShieldGemma 27B | 27B | 0.729 | 0.830 |
| Ours (Gemma 4B, 47MB LoRA) | ~10M | 0.762 | 0.822 |
| gpt-oss-safeguard-20B | 20B | 0.799 | 0.829 |
| gpt-oss-safeguard-120B | 120B | 0.793 | 0.829 |
The 4B + LoRA classifier beats every model up to 27B parameters on ToxicChat and matches ShieldGemma 27B on OpenAI Mod. Only OpenAI's much larger gpt-oss-safeguard (20–120B, Oct 2025) scores higher on ToxicChat — at 500–3000× our parameter count.
On adversarial jailbreak benchmarks, a combined text+probe classifier on Gemma 27B detects 67.4% of WildJailbreak attacks at 5% FPR and 84.4% at 10% FPR.
Anthropic's CC++ paper (arxiv 2601.04603) showed that linear probes on all-layer activations can detect harmful content from inside a frozen model. The model never changes. You hook the residual stream at every layer, concatenate, and train a tiny classifier on top.
This repo replicates and extends that on Gemma:
Total cloud spend: ~$4.14 (RTX 3060 local + rented A100s on Vast.ai).
User text → Gemma (frozen, bf16) → layer activations → probe → harmful / benign
│
all layers concatenated
(87K dim for 4B, 333K for 27B)
The probe is a single nn.Linear(dim, 1). One matrix multiply per input. At inference it piggybacks on the serving model's forward pass — no extra model loaded, no extra GPU.
Stage 1 — Text Classifier: The base model scores each input via YES/NO prompting. Cheap (already running) but has a hard ceiling: 12–14% of benign prompts saturate the YES-logit at exactly 1.0 on the base 27B model, making FPR ≤ 10% impossible from text alone.
Stage 2 — Activation Probe: A linear probe on all-layer concatenated activations breaks the text classifier's ceiling. The probe sees the model's internal representation of "is this harmful?" which is richer than the single YES/NO logit.
Combined: score = α · text_score + (1 − α) · probe_score. The probe pulls the saturated-to-1.0 text scores below threshold, unlocking operating points neither component can reach alone.
Trained on 13,266 harmful + 13,500 benign prompts from 8 sources (WildJailbreak, in-the-wild jailbreaks, PKU-SafeRLHF, HH-RLHF, AdvBench, ForbiddenQuestions, HarmBench, WildChat, Alpaca, Dolly).
WildJailbreak (2000 adversarial jailbreaks + 210 adversarial benign):
| ≤1% FPR | ≤2% FPR | ≤5% FPR | ≤10% FPR | |
|---|---|---|---|---|
| Text only (base 27B) | 0% | 0% | 0% | 0% |
| Probe only | 48.9% | 50.3% | 60.4% | 68.2% |
| Combined | 48.9% | 53.9% | 67.4% | 84.4% |
In-the-Wild Jailbreaks (686 leakage-filtered prompts + 500 benign):
| ≤2% FPR | ≤5% FPR | ≤10% FPR | |
|---|---|---|---|
| Text only | 0% | 0% | 0% |
| Probe only | 16.3% | 21.9% | 31.9% |
| Combined | 21.0% | 30.5% | 64.6% |
WildJailbreak retrained probe (Gemma 4B + LoRA):
A critical methodological finding: 51% of the public in-the-wild jailbreak eval set overlaps with WildJailbreak-adjacent training data. The v1 evaluation reported 26.5% @ 5% FPR on 1,405 ITW prompts without checking this overlap. After adding a leakage filter (hash-set of training texts, applied at eval time), v2 reports on 686 honest prompts: 30.5% @ 5% FPR. The v2 numbers are genuinely better than v1 despite being measured on a harder (filtered) eval set.
src/constitutional_classifier/ Library code: probes, SAEs, LoRA, cascade
models/ probe.py, sae.py, exchange_classifier.py, cascade.py
training/ probe_trainer.py, rl_trainer.py, exchange_trainer.py
data/ constitution.py, datasets.py
scripts/ CLI entrypoints (cc-train-probe, cc-evaluate, ...)
scripts/ Standalone training/eval scripts referenced in the writeup
train_27b_ccpp.py Full pipeline: extract → BCE train → online SWiM → eval
build_mixed_corpus.py Mixed corpus builder with leakage guard
wildjailbreak_eval.py WildJailbreak evaluation harness
eval_full_system.py End-to-end eval (text + probe combined)
finetune_lora_v2_plus.py LoRA training that produced the 0.762 ToxicChat F1
score_shieldgemma.py ShieldGemma baseline scoring
jailbreakbench_eval.py JailbreakBench eval harness
checkpoints/ Trained weights
27b_ccpp_probe_v2.pt Best 27B probe (mixed corpus, BCE) — 3.9 MB
27b_ccpp_probe_v1.pt v1 (WJ-only, SWiM fine-tuned)
27b_ccpp_probe_pre_swim.pt BCE-only checkpoint before online SWiM phase
4b_jailbreak_probe.pt 4B activation probe
4b_wildjailbreak_probe.pt 4B WildJailbreak-tuned probe
lora/ LoRA v1 adapter (13 MB)
lora_v2/ LoRA v2 adapter — the 47MB one that beats ShieldGemma 27B
eval_scores/ Score files for re-plotting without re-running
itw_27b_v2_scores.pt In-the-wild eval scores (686 leakage-filtered)
itw_27b_scores.pt, inthewild_scores.pt
jailbreakbench_scores.pt
multi_prompt_scores.pt Multi-prompt ensemble scores
configs/default.yaml Default hyperparameters (probe, SAE, RL, LoRA, cascade)
# Install
pip install -e .
# Extract activations from Gemma 3 27B (needs ~80GB GPU)
python scripts/extract_activations.py --model google/gemma-3-27b-it --layers all
# Build the mixed-corpus training set with leakage filtering
python scripts/build_mixed_corpus.py --out data/mixed_harmful_v1.pt
# Train the probe
python scripts/train_27b_ccpp.py --train-data data/mixed_harmful_v1.pt \
--out checkpoints/27b_ccpp_probe_v2.pt
# Evaluate on WildJailbreak
python scripts/wildjailbreak_eval.py --probe checkpoints/27b_ccpp_probe_v2.pt \
--combine text+probe --alpha 0.2
# Or use the published probe directly
python scripts/eval_full_system.py --probe checkpoints/27b_ccpp_probe_v2.pt
The 4B pipeline runs on a single RTX 3060 (12 GB). The 27B pipeline needs an A100 80 GB (~90 min on Vast.ai, ~$1.66/run).
| Phase | Hardware | Time | Cost |
|---|---|---|---|
| 4B dev (extraction, LoRA, probes) | RTX 3060 local | ~8 h | $0 |
| 4B Vast.ai runs | Various | ~2 h | ~$1.00 |
| 27B v1 (WJ-only, bug fixes) | A100 80 GB | ~80 min | ~$1.48 |
| 27B v2 (mixed corpus) | A100 80 GB | ~90 min | ~$1.66 |
| Total | ~$4.14 |
Apache-2.0. See LICENSE.
You don't need a 27B-parameter fine-tuned safety LLM to classify harmful content. A 10M-parameter linear probe on a frozen model's activations, trained for $5 on rented GPUs, beats every public safety classifier on standard benchmarks.
The signal is already inside the model. You just have to read it.
Python
100.0%
An open-source replication and extension of Anthropic's Constitutional Classifiers++ (CC++, January 2026) on Google's Gemma 3 models. A linear probe on frozen Gemma activations beats ShieldGemma (Google's 2B–27B safety classifier) on every public benchmark, at ~1000× fewer parameters and ~$5 total compute.
| Model | Params | ToxicChat F1 | OpenAI Mod F1 |
|---|---|---|---|
| OpenAI Mod API | ? | 0.254 | 0.782 |
| LlamaGuard 1 | 7B | 0.616 | 0.758 |
| LlamaGuard 2 | 8B | 0.471 | 0.761 |
| GPT-4 (as classifier) | ~1.8T | 0.683 | 0.810 |
| ShieldGemma 2B | 2B | 0.704 | 0.825 |
| ShieldGemma 9B | 9B | 0.694 | 0.828 |
| WildGuard | 7B | 0.708 | 0.721 |
| Aegis-Guard-Permissive | 7B | 0.730 | 0.747 |
| ShieldGemma 27B | 27B | 0.729 | 0.830 |
| Ours (Gemma 4B, 47MB LoRA) | ~10M | 0.762 | 0.822 |
| gpt-oss-safeguard-20B | 20B | 0.799 | 0.829 |
| gpt-oss-safeguard-120B | 120B | 0.793 | 0.829 |
The 4B + LoRA classifier beats every model up to 27B parameters on ToxicChat and matches ShieldGemma 27B on OpenAI Mod. Only OpenAI's much larger gpt-oss-safeguard (20–120B, Oct 2025) scores higher on ToxicChat — at 500–3000× our parameter count.
On adversarial jailbreak benchmarks, a combined text+probe classifier on Gemma 27B detects 67.4% of WildJailbreak attacks at 5% FPR and 84.4% at 10% FPR.
Anthropic's CC++ paper (arxiv 2601.04603) showed that linear probes on all-layer activations can detect harmful content from inside a frozen model. The model never changes. You hook the residual stream at every layer, concatenate, and train a tiny classifier on top.
This repo replicates and extends that on Gemma:
Total cloud spend: ~$4.14 (RTX 3060 local + rented A100s on Vast.ai).
User text → Gemma (frozen, bf16) → layer activations → probe → harmful / benign
│
all layers concatenated
(87K dim for 4B, 333K for 27B)
The probe is a single nn.Linear(dim, 1). One matrix multiply per input. At inference it piggybacks on the serving model's forward pass — no extra model loaded, no extra GPU.
Stage 1 — Text Classifier: The base model scores each input via YES/NO prompting. Cheap (already running) but has a hard ceiling: 12–14% of benign prompts saturate the YES-logit at exactly 1.0 on the base 27B model, making FPR ≤ 10% impossible from text alone.
Stage 2 — Activation Probe: A linear probe on all-layer concatenated activations breaks the text classifier's ceiling. The probe sees the model's internal representation of "is this harmful?" which is richer than the single YES/NO logit.
Combined: score = α · text_score + (1 − α) · probe_score. The probe pulls the saturated-to-1.0 text scores below threshold, unlocking operating points neither component can reach alone.
Trained on 13,266 harmful + 13,500 benign prompts from 8 sources (WildJailbreak, in-the-wild jailbreaks, PKU-SafeRLHF, HH-RLHF, AdvBench, ForbiddenQuestions, HarmBench, WildChat, Alpaca, Dolly).
WildJailbreak (2000 adversarial jailbreaks + 210 adversarial benign):
| ≤1% FPR | ≤2% FPR | ≤5% FPR | ≤10% FPR | |
|---|---|---|---|---|
| Text only (base 27B) | 0% | 0% | 0% | 0% |
| Probe only | 48.9% | 50.3% | 60.4% | 68.2% |
| Combined | 48.9% | 53.9% | 67.4% | 84.4% |
In-the-Wild Jailbreaks (686 leakage-filtered prompts + 500 benign):
| ≤2% FPR | ≤5% FPR | ≤10% FPR | |
|---|---|---|---|
| Text only | 0% | 0% | 0% |
| Probe only | 16.3% | 21.9% | 31.9% |
| Combined | 21.0% | 30.5% | 64.6% |
WildJailbreak retrained probe (Gemma 4B + LoRA):
A critical methodological finding: 51% of the public in-the-wild jailbreak eval set overlaps with WildJailbreak-adjacent training data. The v1 evaluation reported 26.5% @ 5% FPR on 1,405 ITW prompts without checking this overlap. After adding a leakage filter (hash-set of training texts, applied at eval time), v2 reports on 686 honest prompts: 30.5% @ 5% FPR. The v2 numbers are genuinely better than v1 despite being measured on a harder (filtered) eval set.
src/constitutional_classifier/ Library code: probes, SAEs, LoRA, cascade
models/ probe.py, sae.py, exchange_classifier.py, cascade.py
training/ probe_trainer.py, rl_trainer.py, exchange_trainer.py
data/ constitution.py, datasets.py
scripts/ CLI entrypoints (cc-train-probe, cc-evaluate, ...)
scripts/ Standalone training/eval scripts referenced in the writeup
train_27b_ccpp.py Full pipeline: extract → BCE train → online SWiM → eval
build_mixed_corpus.py Mixed corpus builder with leakage guard
wildjailbreak_eval.py WildJailbreak evaluation harness
eval_full_system.py End-to-end eval (text + probe combined)
finetune_lora_v2_plus.py LoRA training that produced the 0.762 ToxicChat F1
score_shieldgemma.py ShieldGemma baseline scoring
jailbreakbench_eval.py JailbreakBench eval harness
checkpoints/ Trained weights
27b_ccpp_probe_v2.pt Best 27B probe (mixed corpus, BCE) — 3.9 MB
27b_ccpp_probe_v1.pt v1 (WJ-only, SWiM fine-tuned)
27b_ccpp_probe_pre_swim.pt BCE-only checkpoint before online SWiM phase
4b_jailbreak_probe.pt 4B activation probe
4b_wildjailbreak_probe.pt 4B WildJailbreak-tuned probe
lora/ LoRA v1 adapter (13 MB)
lora_v2/ LoRA v2 adapter — the 47MB one that beats ShieldGemma 27B
eval_scores/ Score files for re-plotting without re-running
itw_27b_v2_scores.pt In-the-wild eval scores (686 leakage-filtered)
itw_27b_scores.pt, inthewild_scores.pt
jailbreakbench_scores.pt
multi_prompt_scores.pt Multi-prompt ensemble scores
configs/default.yaml Default hyperparameters (probe, SAE, RL, LoRA, cascade)
# Install
pip install -e .
# Extract activations from Gemma 3 27B (needs ~80GB GPU)
python scripts/extract_activations.py --model google/gemma-3-27b-it --layers all
# Build the mixed-corpus training set with leakage filtering
python scripts/build_mixed_corpus.py --out data/mixed_harmful_v1.pt
# Train the probe
python scripts/train_27b_ccpp.py --train-data data/mixed_harmful_v1.pt \
--out checkpoints/27b_ccpp_probe_v2.pt
# Evaluate on WildJailbreak
python scripts/wildjailbreak_eval.py --probe checkpoints/27b_ccpp_probe_v2.pt \
--combine text+probe --alpha 0.2
# Or use the published probe directly
python scripts/eval_full_system.py --probe checkpoints/27b_ccpp_probe_v2.pt
The 4B pipeline runs on a single RTX 3060 (12 GB). The 27B pipeline needs an A100 80 GB (~90 min on Vast.ai, ~$1.66/run).
| Phase | Hardware | Time | Cost |
|---|---|---|---|
| 4B dev (extraction, LoRA, probes) | RTX 3060 local | ~8 h | $0 |
| 4B Vast.ai runs | Various | ~2 h | ~$1.00 |
| 27B v1 (WJ-only, bug fixes) | A100 80 GB | ~80 min | ~$1.48 |
| 27B v2 (mixed corpus) | A100 80 GB | ~90 min | ~$1.66 |
| Total | ~$4.14 |
Apache-2.0. See LICENSE.
You don't need a 27B-parameter fine-tuned safety LLM to classify harmful content. A 10M-parameter linear probe on a frozen model's activations, trained for $5 on rented GPUs, beats every public safety classifier on standard benchmarks.
The signal is already inside the model. You just have to read it.
Python
100.0%