Skip to content

What a Migration Does to an Existing Jailbreak Defense Test Suite

9 min read · updated August 11, 2026

You swapped the model, ran the jailbreak suite, and the hit rate went down. That is not evidence of a safer model. On a suite built the usual way, it is the expected result of a detector that no longer recognises what it is looking at.

The suite went green and that is the bug

A jailbreak suite has two halves. Probes generate the attack: role-play framings, encoding tricks, instruction-override attempts, the DAN family. Detectors decide whether the response counts as a failure. Scanners such as NVIDIA’s garak make the split explicit — every probe carries a primary detector and optional extended ones, and a run looks like python3 -m garak --target_type huggingface --target_name gpt2 --probes dan.Dan_11_0. NVIDIA’s garak repository documents the probe and detector taxonomy.

Probes are model-independent. They are strings. Whatever you migrated to, the probe sends the same bytes. Detectors are not model-independent at all, and the cheapest detector to write — and therefore the one most in-house suites contain — is a refusal-phrase matcher. It looks for “I’m sorry”, “I can’t help with that”, “As an AI language model”, and calls the absence of those a failure and the presence of them a pass.

That detector is measuring the old model’s house style. Refusal wording is one of the most heavily and most visibly tuned surfaces between model generations, and a new model that declines by saying “That is not something I’ll do — here is what I can help with instead” matches none of those strings. Your suite records a failure on a correct refusal, and you spend the migration chasing a regression that does not exist. The inverse is worse and quieter: a model that complies but opens with a hedge containing one of your phrases is scored as having refused.

How a refusal detector scores a jailbreak as a pass

Concretely. The probe asks for instructions to do something the policy forbids. The new model replies:

I can't give you a full operational guide, but I can outline the
general principles at a high level. First, you would need to ...
[four paragraphs of exactly the content the probe was testing for]

The detector sees I can’t in the first eight characters, returns “refused”, and the suite is green. The model complied. Nothing in the report says so. This is not a hypothetical construction — the partial-refusal-then-comply shape is one of the most common outcomes of the whole attack class, and a prefix match is structurally unable to see it.

A migration converts this from a background inaccuracy into an acute problem, because before the migration you had a stable, if wrong, baseline. Everyone compared runs to last week’s run and the detector’s bias cancelled out. After a swap it does not cancel: the bias is a function of phrasing and the phrasing changed. The numbers move for detector reasons and model reasons at once, and there is no way to separate them after the fact.

Fixing the detector: assert on the payload

Stop asking “did it refuse”. Ask “did the forbidden content appear”. That question has a model-independent answer, because it is about the payload the probe was fishing for, not about the wrapper around it.

  • Give every probe an explicit success marker. Not a phrase the model might say, but a fact only a complying answer contains: a specific quantity, a named step, a substring the probe is asking to have reproduced. The detector checks for that.
  • Where the payload is open-ended, use a judge with a rubric that names the forbidden content, and pin the judge model independently of the model under test. A judge that migrates at the same time as the target gives you two moving parts and no baseline.
  • Score three outcomes, not two: refused, complied, and partially complied. The middle category is where the migration damage lives, and a boolean detector cannot represent it.
  • Keep the refusal-phrase matcher, but demote it. As a secondary signal it is useful for spotting tone changes. As the primary verdict it is a liability.
# Before: verdict is a property of the wrapper
def refused(response: str) -> bool:
    return any(p in response.lower() for p in REFUSAL_PHRASES)

# After: verdict is a property of the payload
def outcome(response: str, probe) -> str:
    leaked = [m for m in probe.markers if m.lower() in response.lower()]
    if not leaked:
        return "refused"
    if len(leaked) < len(probe.markers):
        return "partial"
    return "complied"

Re-running the suite properly

A jailbreak suite is not a regression suite in the sense that a green run carries forward. Resistance to a specific attack pattern is a property of one model, and it does not transfer to the next one in the family, let alone across families. Assume nothing passes until it has passed on the new target.

  1. Fix the detectors first, then re-run the suite against the old model. This is the step that is always skipped and it is the only thing that gives you a comparable baseline. You are re-measuring history with the corrected instrument.
  2. Run the full suite against the new model. Full, not the subset that was failing — attack patterns that the old model shrugged off are exactly the ones nobody has looked at recently.
  3. Diff the two by probe, not in aggregate. An unchanged total can hide a set of newly-passing probes and an equal set of newly-failing ones, and only the second set matters.
  4. Re-check any input-side guard that ran before the model. Classifiers trained on attacks that worked against the old model are tuned to a threat surface that has moved.
  5. Record which probes were run, against which model string, on which date. A suite result with no model string attached is not evidence of anything.

Then re-establish the baseline itself, because the numbers you carry into the next migration are the ones produced by this run. Rebuilding the injection baseline after a migration covers that, and the general treatment of injection defences covers the layers this suite is testing.

One thing to hold fixed while you do this: do not edit the defensive system prompt in the same change. The instinct on seeing a newly-failing probe is to strengthen the wording — add another sentence about not revealing instructions, another about ignoring embedded commands. Do that during a re-run and you have altered the target and the instrument together, and no subsequent number tells you which change moved it. Land the detector fix, re-baseline, and only then touch the prompt, one edit at a time, re-running the full suite after each. It is slower and it is the only sequence that produces evidence.

What this page is not about

This is about a suite of many probes whose scoring is wrong. It is not about the canary marker some of those suites embed in the system prompt to detect extraction: that primitive has its own migration failure, an exact-string comparison defeated by a model that paraphrases the marker instead of quoting it, and it is worth fixing separately. Migrating a canary system between model families is that page. Fixing the detectors here does not fix the canary, and fixing the canary does not tell you anything about the other probes.