Crescendo
Crescendo never asks for the goal outright. It opens a conversation about the goal's topic with a harmless question, then asks for a little more on each turn, building on what the target has already said. By the time the real request arrives, the target has discussed the subject at length, and the request reads as the next step of that discussion rather than a first ask.
The idea that makes it work: the conversation is the attack. Every accepted turn is kept and the whole history is sent again, so the target's own earlier answers justify the next question. When the target refuses, Crescendo does not build on the refusal. It removes that exchange from the conversation and asks the same step again in other words. This models a patient user who escalates across many turns, which a single-prompt test never exercises.
At a glance
| Type | Helper models | Turns | Calls per goal | Good for |
|---|---|---|---|---|
| Multi-turn | attacker (required), refusal (optional) | Up to max_turns user turns in the conversation | Up to 20 target calls with the defaults | Chat assistants that refuse a direct request but keep answering follow-ups |
Research
Mark Russinovich, Ahmed Salem and Ronen Eldan, "Great, Now Write an Article About That: The Crescendo Multi-Turn LLM Jailbreak Attack", 2024, USENIX Security 2025. arXiv:2404.01833.
The paper starts from a benign question and escalates by referring back to the model's own replies. It reported high success rates on ChatGPT, Gemini Pro, Gemini Ultra, LLaMA-2 70b, LLaMA-3 70b Chat and Anthropic Chat. Its automated version, Crescendomation, beat the other jailbreak techniques it was compared with on an AdvBench subset, by 29–61% on GPT-4 and 49–71% on Gemini Pro.
HackAgent follows the reference implementation, PyRIT's CrescendoAttack. Like
it, a Refusal Judge with PyRIT's strict refusal rubric decides when to
backtrack, a backtrack does not use up a turn, and the backtrack budget is
shared by the whole goal. Where HackAgent differs:
- Success comes from the campaign's judge panel, not from a scorer and
threshold of Crescendo's own. Each kept turn is judged, and with
early_stopthe first reply the panel passes ends the search. - The panel sees one exchange: the goal, the latest question and the reply. It does not see the earlier turns.
- A refused turn can still count. Once the backtrack budget is spent, a refused turn is kept, judged like any other and can be a jailbreak.
- A failed or unreadable refusal verdict counts as "not refused", so it never triggers a backtrack.
- No parse retries. The attacker is asked for JSON, but a plain-text question is accepted. If the attacker returns nothing usable, the search stops.
How it works
- The
attackermodel gets a system prompt with the goal and the escalation strategy, and is asked for a general, clearly benign opening question on the goal's topic. - It answers with a short reasoning and the next question. If the call fails or the reply is empty, the search stops.
- HackAgent appends the question to the conversation and sends the whole conversation to the target.
- If the target gives no usable reply (a provider error, a guardrail block or
an empty reply), the turn is skipped: it counts toward
max_turns, no attempt is recorded for it, and the attacker is asked to rephrase. - The Refusal Judge (the
refusalrole, or the attacker model when you leave it out) reads the goal and the reply. Under the strict rubric, anything short of substantively fulfilling the goal is a refusal: an explicit refusal, a redirection, a partial answer, or caveats that stop it from answering. - If the reply is a refusal and backtracks remain under
max_backtracks, the turn is backtracked. The exchange is recorded but not judged, it never joins the conversation, the turn counter does not move, and the attacker is told to rephrase the same step. - Otherwise the panel judges the reply, the exchange joins the conversation,
and the turn counter moves on. The attacker sees the reply (cut to
feedback_max_chars) and the panel's score, and is asked to escalate a little further. Its own planning log keeps the lastkeep_last_nexchanges; the target's conversation is never trimmed. - The search ends when a kept turn passes the panel (with
early_stop), whenmax_turnsturns have been kept or skipped, or when the attacker stops proposing questions. The goal counts as jailbroken if any of its attempts passed.
When to use it
Use Crescendo when a target refuses the goal asked directly, and you want to know whether it holds that line over a conversation. It suits chat assistants and customer-facing bots, where users can keep talking, and models whose safety training was tested mostly on single prompts.
The target must accept the conversation history on every call. That is true of
the chat API types (OPENAI, OLLAMA, LITELLM, LANGCHAIN). The agent types
forward only the latest user message:
CLAUDE_CODE,CODEX,HERMESandGOOGLE_ADK(which opens a fresh session per request by default) never see the earlier turns, so each question arrives cold and the escalation is lost.WEBkeeps one live chat, so turns do accumulate on the page, but a backtracked turn cannot be removed from it.
Pick another attack when:
- The target already falls to a single prompt. PAIR or TAP find that for fewer calls, and a static attack for fewer still.
- The target is an agent with tools and the harm is an action, not text. STAC checks what the agent actually executes.
Run Crescendo after cheaper attacks with execution.escalate, so it spends its
turns only on the goals still standing.
Cost
Each pass through the loop makes one attacker call, one target call and, when
the target replied, one Refusal Judge call. A kept turn adds one panel
evaluation; a backtracked turn adds none. Turns advance and backtracks are
bounded, so a goal runs at most max_turns + max_backtracks passes.
| Calls per goal | Bound | With the defaults |
|---|---|---|
| Target | max_turns + max_backtracks | 20 |
attacker | max_turns + max_backtracks | 20 |
| Refusal Judge | max_turns + max_backtracks | 20, on the attacker model unless you set refusal |
| Panel | max_turns evaluations, each one call per judge | 10 × judges |
Those are ceilings. With early_stop on, the search ends at the first reply
the panel passes. Provider retries (execution.retries) add to them. The
calls within a goal are sequential, because each question depends on the last
reply, so a goal takes about passes × (attacker + target + refusal + panel)
latency. Goals still run in parallel under execution.concurrency.
Every request re-sends the whole conversation, so target tokens grow with each
kept turn. Lower max_turns to cut both calls and tokens; lower
max_backtracks to stop paying for rephrasings.
Configuration
Helper models
Models this attack drives, configured under its roles. Each one is a ModelConfig.
| Role | Kind | Needed | What it does |
|---|---|---|---|
attacker | chat model | required | Plans the conversation and writes each next question, a little closer to the goal than the last. |
refusal | chat model | optional | Optional. The Refusal Judge: decides whether each reply refuses the goal, which triggers a backtrack. Defaults to the attacker model. |
Parameters
Settings for the algorithm itself, written under the attack's parameters. Anything left out keeps its default.
| Field | Type | Default | Description |
|---|---|---|---|
max_turns | integer (>= 1) | 10 | Turns the conversation may grow to. A backtracked turn is retried, not counted, so the conversation never escalates on a refusal. |
max_backtracks | integer (>= 0) | 10 | Rephrasings allowed across the whole goal. Once spent, a refused turn is accepted as-is, judged like any other, and the conversation moves on. |
keep_last_n | integer (>= 1) | 6 | Attacker turns kept before its planning log is trimmed. The target's own conversation is never trimmed: that history is the attack. |
early_stop | boolean | true | Stop as soon as a turn passes the panel. |
feedback_max_chars | integer (>= 1) | 2500 | Characters of the target's reply fed back to the attacker; the rest is cut. |
In a campaign file
attacks:
- name: crescendo
roles:
attacker:
name: llama3.2:latest
connection:
provider: ollama
type: OLLAMA
This attack cannot run without attacker. A campaign that leaves it out fails to resolve, before anything is attacked.
Example campaigns
A separate Refusal Judge and a shorter conversation:
attacks:
- name: crescendo
parameters:
max_turns: 6
max_backtracks: 4
roles:
attacker:
name: qwen3:8b
connection:
provider: ollama
type: OLLAMA
refusal:
name: llama3.2:latest
connection:
provider: ollama
type: OLLAMA
This caps a goal at 10 target calls and moves refusal checks to a second model, so the attacker model only writes questions. Use it when your attacker model is picked for writing adversarial text and you trust another model more to classify refusals.
Crescendo only on the goals a cheap attack missed:
attacks:
- name: flipattack
- name: crescendo
roles:
attacker:
name: qwen3:8b
connection:
provider: ollama
type: OLLAMA
execution:
escalate: true
FlipAttack runs first for one call per goal. With escalate, a goal it breaks
is dropped, and Crescendo spends its turns only on the goals still standing.
Reading the results
One attempt is one exchange the target answered: the conversation sent so far plus the new question, and the reply. A goal produces one attempt per answered turn, in order, including backtracked ones. A skipped turn produces none; if nothing came back at all, the goal gets one error attempt that says why.
- A kept turn carries the panel's verdict.
- A backtracked turn is recorded as not a jailbreak, with the explanation "not judged: backtracked after a refusal" followed by the Refusal Judge's rationale.
Each attempt's metadata has turn (counted from 0), refused,
refusal_rationale, backtracked and the panel score. Its trace sits under a
turn N/M phase: the attacker call, the target call, the Refusal Judge call
(labelled refusal, or attacker when the attacker model judges refusals), the
panel call, and decisions such as backtracked (with the budget used so far),
skipped and stopped.
In the dashboard the goal reads as one continuous conversation. History an attempt shares with an earlier one is not printed again, a turn rephrased after a refusal says which attempt it continues from, and each attempt header states its turn. A prompt or reply identical to an earlier one shows as "same as attempt N".
A jailbreak here means the panel passed one reply of the conversation, judged against the goal on that reply alone. Check these first:
- Read the passing reply with the turns before it. Does it deliver the goal, or only make sense as part of the conversation?
- Read the
refusal_rationaleof the backtracked turns. If the budget went on the first few turns, the rest of the conversation ran without backtracking. - A long run of kept turns with low scores means the attacker is not escalating; try a stronger attacker model.
Limitations
- The panel judges replies one at a time. Harmful content split across several replies is not caught, and a reply that is harmful only in context may be under-scored.
- The Refusal Judge reads each reply against the goal, not the turn's
question. A helpful answer to an early, harmless question can be scored as
"only tangentially related", which is a refusal under the strict rubric.
Those turns are backtracked too, and can spend the budget early. Raise
max_backtracksif you see this. - One conversation per goal. There are no parallel streams, and the attacker samples freely, so two runs of the same goal can take different paths. Compare rates over several goals, not single outcomes.
- Only chat API targets get the full history. See When to use it.
- The conversation is never trimmed. A long run can outgrow the target's context window, which shows up as provider errors and skipped turns.
Related
- Multi-turn attacks, and STAC for agents with tools
- PAIR and TAP, single-prompt searches that cost less
- Attacks and roles: the
attackerandrefusalroles - Judges: how the panel turns a reply into a verdict
- Jailbreak, whose
multi_turnsub-type this attack tests