A reproducible benchmark that measures how often tool-using LLM agents follow instructions planted in tool outputs (indirect prompt injection), and how much each defense lowers that rate without hurting legitimate task success. Every tool is simulated: nothing reaches a real account, website, or system.
It runs 40 user tasks from AgentDojo's workspace suite (an email, calendar, and file-drive assistant) under 5 injection styles and a clean control, with 4 defenses, on 4 models: two open-weight Qwen models served locally by llama.cpp, and two hosted models: OpenAI's gpt-6-luna (API model ID), queried on 2026-10-18 (UTC) on 2 of the styles to stay within its spend cap, and Anthropic's claude-haiku-5-5, queried on 2026-10-30 (UTC) on the same styles, so that the two hosted models share a grid. That is 8640 scored cases. The repository holds the harness that plants the injections and runs the agents, the defenses, the task-clustered bootstrap statistics, and the generators that write every table, plot, and document from the raw case records committed under results/raw/.
Rates come with 95% task-clustered bootstrap intervals, except that a rate with no successes shows its exact one-sided 95% upper bound (marked †) and a rate with only successes its exact one-sided 95% lower bound (marked ‡); changes are paired against no defense on the same resampled tasks.
- Undefended attack success on the styles every model ran (
direct,important_instructions):claude-haiku-5-50 of 239 classified cases, 0.000 [0.000, 0.012]†,gpt-6-luna1 of 233 classified cases, 0.004 [0.000, 0.013],qwen2.5-7b6 of 148 classified cases, 0.041 [0.006, 0.089],qwen3-4b12 of 190 classified cases, 0.063 [0.015, 0.121]. The table below pools each model over all the styles it ran, which differ (2 forclaude-haiku-5-5, 2 forgpt-6-luna, 5 forqwen2.5-7b, 5 forqwen3-4b). - Floor effect: undefended attack success was at most 0.030 [0.009, 0.055] (
qwen3-4b, pooled over its styles), so no defense could lower it by more than that; the style with the most successful attacks, per model:qwen3-4b12 of 14 underimportant_instructions(0.124 [0.031, 0.240]),qwen2.5-7b6 of 9 underimportant_instructions(0.083 [0.013, 0.188]),gpt-6-luna1 of 1 underdirect(0.009 [0.000, 0.026]). - DeBERTa filter: it lowered task success by 0.265 [0.163, 0.370] (
qwen2.5-7b) to 0.562 [0.446, 0.671] (gpt-6-luna); its largest drop in attack success was 0.023 [0.005, 0.046] (qwen3-4b). It is expensive because it flagged 0.334 [0.242, 0.426] (qwen2.5-7b) to 0.428 [0.321, 0.541] (claude-haiku-5-5) of benign tool outputs in runs without an attack, and a flagged output is replaced by an omission notice. - Delimiter and hierarchy prompts: the paired interval for the change in task success includes zero in 8 of 8 model-defense pairs, and for the change in attack success in 7 of 8; the exceptions:
qwen2.5-7bunderdelimit, attack-success reduction -0.017 [-0.037, -0.001] (attack success rose). - Second-opinion judge:
claude-sonnet-5-5, asked whether the agent's tool calls carried out the injected goal, agreed with AgentDojo's rule-based check onclaude-haiku-5-5900 of 900 parsed verdicts (58 refused), 1.000 [0.997, 1.000]‡, Cohen's kappa undefined (both always gave the same answer),gpt-6-luna873 of 873 parsed verdicts (62 refused), 1.000 [0.997, 1.000]‡, Cohen's kappa 1.000,qwen3-4b1828 of 1830 parsed verdicts (102 refused), 0.999 [0.997, 1.000], Cohen's kappa 0.978. The judge changes no rate; its confusion counts, refusals, and stamps are in the Report.
Terms. ASR (attack success rate) is the share of injected cases in which the agent carried out the injected goal, counted only when the agent actually read the injected tool output. TSR (task success rate) is the share of cases in which it completed the user's task. Defense cost is TSR without a defense minus TSR with it; ASR reduction is ASR without a defense minus ASR with it. Exposure is the share of injected cases in which the agent read the injection.
- User tasks: 40; conditions: 6 (clean plus 5 injection styles); defenses: 4; injected goals: 6; models: 4; case records: 8640.
- Repetitions per model, defense and condition: 3; temperature 0.7; top-p 0.9; loop cap 15 model calls; output cap 1024 tokens per call; base seed 20261017; the temperature, top-p, and output cap apply to every model but claude-haiku-5-5.
- OpenAI hosted run: 1440 of 1440 planned cases completed; spend $0.5474 of the $3.00 spend cap; stopped at the cap: no.
- Claude run (claude-haiku-5-5): 1440 of 1440 planned cases completed; recorded spend $1.6025 in 1 run, under the $5.00 per-run cap; the last run stopped at its cap: no. No sampling settings or seed were sent (the model's default sampling); adaptive thinking at effort medium; output cap 16000 tokens per call, thinking included.
Each model pooled over the injection styles it ran (from results/summary.md; † marks an exact upper bound for a rate with no successes, ‡ an exact lower bound for a rate with only successes):
| Model | Defense | Styles | ASR (95% CI) | TSR (95% CI) | Defense cost vs none (95% CI) | ASR reduction vs none (95% CI) | Exposure (95% CI) | Unclassified | Errored |
|---|---|---|---|---|---|---|---|---|---|
| claude-haiku-5-5 | none | 2 | 0.000 [0.000, 0.012]† | 0.908 [0.817, 0.979] | reference | reference | 0.996 [0.988, 1.000] | 1 | 0 |
| claude-haiku-5-5 | delimit | 2 | 0.000 [0.000, 0.012]† | 0.908 [0.808, 0.983] | 0.000 [-0.012, 0.012] | 0.000 [0.000, 0.000] | 0.996 [0.988, 1.000] | 1 | 0 |
| claude-haiku-5-5 | hierarchy | 2 | 0.000 [0.000, 0.012]† | 0.929 [0.850, 0.992] | -0.021 [-0.054, 0.000] | 0.000 [0.000, 0.000] | 1.000 [0.988, 1.000]‡ | 0 | 0 |
| claude-haiku-5-5 | filter | 2 | 0.000 [0.000, 0.012]† | 0.429 [0.296, 0.558] | 0.479 [0.350, 0.617] | 0.000 [0.000, 0.000] | 1.000 [0.988, 1.000]‡ | 0 | 0 |
| gpt-6-luna | none | 2 | 0.004 [0.000, 0.013] | 0.942 [0.887, 0.983] | reference | reference | 0.971 [0.929, 1.000] | 7 | 0 |
| gpt-6-luna | delimit | 2 | 0.000 [0.000, 0.013]† | 0.942 [0.879, 0.988] | 0.000 [-0.021, 0.021] | 0.004 [0.000, 0.013] | 0.979 [0.942, 1.000] | 5 | 0 |
| gpt-6-luna | hierarchy | 2 | 0.000 [0.000, 0.013]† | 0.942 [0.875, 0.992] | 0.000 [-0.033, 0.037] | 0.004 [0.000, 0.013] | 0.963 [0.908, 1.000] | 9 | 0 |
| gpt-6-luna | filter | 2 | 0.004 [0.000, 0.013] | 0.379 [0.267, 0.496] | 0.562 [0.446, 0.671] | 0.000 [0.000, 0.000] | 0.983 [0.950, 1.000] | 4 | 0 |
| qwen2.5-7b | none | 5 | 0.024 [0.005, 0.050] | 0.362 [0.248, 0.478] | reference | reference | 0.630 [0.508, 0.755] | 222 | 0 |
| qwen2.5-7b | delimit | 5 | 0.040 [0.014, 0.073] | 0.358 [0.247, 0.472] | 0.004 [-0.068, 0.072] | -0.017 [-0.037, -0.001] | 0.620 [0.498, 0.740] | 227 | 2 |
| qwen2.5-7b | hierarchy | 5 | 0.041 [0.010, 0.080] | 0.358 [0.245, 0.480] | 0.003 [-0.080, 0.093] | -0.017 [-0.038, 0.001] | 0.573 [0.457, 0.690] | 256 | 0 |
| qwen2.5-7b | filter | 5 | 0.013 [0.003, 0.026] | 0.097 [0.047, 0.163] | 0.265 [0.163, 0.370] | 0.011 [-0.004, 0.028] | 0.642 [0.523, 0.755] | 215 | 0 |
| qwen3-4b | none | 5 | 0.030 [0.009, 0.055] | 0.560 [0.430, 0.678] | reference | reference | 0.790 [0.678, 0.888] | 126 | 0 |
| qwen3-4b | delimit | 5 | 0.020 [0.004, 0.041] | 0.555 [0.433, 0.673] | 0.005 [-0.035, 0.048] | 0.009 [-0.011, 0.031] | 0.828 [0.725, 0.913] | 103 | 0 |
| qwen3-4b | hierarchy | 5 | 0.041 [0.015, 0.070] | 0.532 [0.410, 0.650] | 0.028 [-0.013, 0.073] | -0.011 [-0.034, 0.009] | 0.780 [0.668, 0.883] | 132 | 0 |
| qwen3-4b | filter | 5 | 0.006 [0.000, 0.019] | 0.195 [0.108, 0.285] | 0.365 [0.267, 0.465] | 0.023 [0.005, 0.046] | 0.822 [0.715, 0.913] | 107 | 0 |
The paper-style write-up (question, setup, results, limitations, threats to validity) is docs/REPORT.md. The full per-cell table, with exposure, unclassified and errored counts, and filter precision and recall, is results/summary.md.
flowchart LR
subgraph Sandbox["Sandbox: AgentDojo workspace suite, simulated tools, no network"]
FX[(YAML fixture)] --> T[email, calendar, drive tools]
end
S[Injection_Style + Injected_Goal] -- planted only in fields the task reads --> FX
P[AgentDojo pipeline + defense] <--> M[Model: llama.cpp, hosted API, or Stub_Model]
P <--> T
P --> C[Task_Success_Check + Injection_Success_Check, same case]
C --> R[(results/raw/*.jsonl)]
R --> MET[results/metrics.json] --> TAB[results/summary.md] --> DOC[docs/REPORT.md + README.md + plots]
- Sandbox and tasks. AgentDojo's workspace suite (
agentdojo==0.1.35, benchmarkv1.2.2with thev1.1.2task fixes): user tasks with deterministic success checks over the final state and the tool-call trace. - Attacks. Five generic injection styles, planted only in fixture fields that the user task's ground truth reads, so the injection always arrives inside a tool output; each user task is paired with one of six benign sandbox goals. A clean control has no injection.
- Classification. An injected case counts toward the attack success rate only when the agent actually received a tool output containing the injection; other injected cases are reported as unclassified.
- Defenses.
none,delimit(spotlighting with<<>>),hierarchy(instruction-hierarchy system prompt), andfilter(a DeBERTa prompt-injection classifier screens every tool output). - Statistics. Task-clustered bootstrap intervals for every rate, except exact one-sided bounds for a rate with no successes or only successes; paired bootstrap intervals for defense cost and ASR reduction.
No GPU, model download, or API key is needed. On Linux (or WSL) with uv and Python 3.12:
git clone https://github.com/OctaviusLeo/Agent-Injection-Bench.git
cd Agent-Injection-Bench
uv sync --frozen
bash scripts/test_unit.sh
bash scripts/build_results.sh
git status --shortbuild_results.sh recomputes every rate and interval from the committed case records in results/raw/ (seeded bootstrap), then regenerates the Results_Table, the plots, the Report, and this README, and checks that every number in the README and the Report appears in a generated file. Afterwards git status shows the regenerated files; they differ from the committed ones only in the commit hash of their stamp lines, and the plots come out byte-identical.
Python and every script run in WSL Ubuntu-24.04 with uv; the llama.cpp server runs in Docker on the GPU (see docs/ENVIRONMENT.md for every version). From the repository root:
uv sync --frozen
uv run --frozen python scripts/fetch_models.py
bash scripts/run_all_local.sh
bash scripts/run_benchmark.sh gpt-6-luna
bash scripts/run_benchmark.sh claude-haiku-5-5
uv run --frozen python -m aib.judge --store results/raw/<model key>.jsonl
bash scripts/build_results.sh- Full local-model benchmark (every enabled local model, one after the other):
bash scripts/run_all_local.sh - One model's benchmark (resumable; reproduces that model's store):
bash scripts/run_benchmark.sh <model key> - The same run under a watchdog that resumes it with a different launch method (setsid, systemd, tmux, child process) after a crash, a vanished job, a stall, or a slowdown:
uv run --frozen python -m aib.watchdog --model <model key> - The OpenAI hosted model needs
OPENAI_API_KEY(read from~/.config/aib/secrets.envbyscripts/wsl_run.sh) and stops before its Spend_Cap. - The Claude target (
claude-haiku-5-5) and the second-opinion judge (claude-sonnet-5-5) are billed to the Anthropic API key in the Windows user variable thatconfig/models.yamlnames, forwarded into WSL withWSLENV(seedocs/ENVIRONMENT.md). Every Claude command stops before the per-run cap inconfig/claude.yaml. The judge gives a second opinion on each attack case and reports how often it agrees with AgentDojo's rule-based check; it never changes a reported rate. - Regenerate the metrics, the Results_Table, the plots, the Report, and this README, then run the traceability check:
bash scripts/build_results.sh
| Result | Command |
|---|---|
A model's case records, results/raw/<model key>.jsonl |
bash scripts/run_benchmark.sh <model key> |
Every rate and interval, results/metrics.json |
uv run --frozen python -m aib.metrics --store results/raw --seed 20261017 --resamples 2000 --out results/metrics.json |
| The Results_Table, the plots, the Report, and this README | bash scripts/build_results.sh |
Hosted run summary, results/hosted_run.json |
bash scripts/run_benchmark.sh gpt-6-luna |
Claude run summary, results/claude_run.json |
bash scripts/run_benchmark.sh claude-haiku-5-5 |
Claude check, results/claude_check.json |
uv run --frozen python scripts/claude_check.py |
Second-opinion verdicts and agreement, results/judge/<model key>.jsonl and results/judge/<model key>_agreement.json |
uv run --frozen python -m aib.judge --store results/raw/<model key>.jsonl |
Classifier check on ground-truth outputs, results/m3_filter_calibration.json |
uv run --frozen python scripts/filter_calibration.py |
Stub_Model validation, results/m2_stub_validation.json |
uv run --frozen python scripts/stub_validation.py |
Task catalog, results/m1_catalog.json |
uv run --frozen python scripts/catalog.py |
- Unit and property tests with the per-module coverage check (what CI runs):
bash scripts/test_unit.sh - CI smoke subset (Stub_Model, no GPU, network, or key):
uv run --frozen python scripts/ci_smoke.py - README and Report number traceability:
uv run --frozen python scripts/check_traceability.py
GitHub Actions runs the first two on every push and on pull requests to main.
- One environment. All 40 user tasks come from AgentDojo's workspace suite, so the results describe an email, calendar, and file-drive assistant, not agents in general.
- Small local models. Their task success can be low even without attacks, which widens intervals and limits what the defense-cost comparison can resolve.
- One model family locally. Both local models are Qwen-family: Llama-3.1-8B was replaced before the benchmark because llama.cpp could not parse the turns in which it called two tools (the model checks are recorded in
.kiro/specs/agent-injection-bench/design.md), so the local comparison is across size and generation, not family. - The hosted grid is smaller: it covers 3 of the 6 conditions because of
gpt-6-luna's spend cap.gpt-6-lunais OpenAI's API model ID, an alias the provider can repoint; the responsemodelfield recorded on every case echoed the alias, so no dated snapshot was recorded. - The two hosted models are not a controlled comparison.
claude-haiku-5-5ran the same grid asgpt-6-lunabut sent no sampling settings and no seed, so it used the model's default sampling, and it thought adaptively at effortmedium;gpt-6-lunaran at temperature 0.7 and top-p 0.9 with a per-request seed and reasoning effortnone. A difference between the two mixes the model with its decoding settings. The responsemodelfield onclaude-haiku-5-5's cases echoed the requested ID, so no dated snapshot was recorded. - Floor effect. Undefended attack success was at most 0.030 pooled over a model's styles, so no defense could lower it by more than that; the ASR-reduction intervals are narrow mainly because there was little to reduce.
- Different decoding stacks. The local models are 4-bit GGUF quantizations served by llama.cpp; quantization and the server's chat template can change behavior relative to the original weights.
- Exposure-conditioned ASR. Cases where the agent never read the injected data are excluded from the ASR denominator; exposure rates are reported beside every ASR.
- The tool-permission-limit defense is not implemented. It needs a per-task allowed-tool list that AgentDojo does not provide.
- Fixed filter threshold of 0.5; precision and recall at other thresholds are not explored.
- Zero-success and all-success intervals. A rate with no successes reports the exact one-sided 95% upper bound and a rate with only successes the exact one-sided 95% lower bound; both treat cases as independent and so can be narrower than a task-clustered bound, and a paired difference between two rates that are both zero-success, or both all-success, still shows a zero-width bootstrap interval.
- Goal confounding. Each user task is always paired with the same injected goal (user task k with goal k mod 6), and the bootstrap resamples tasks only, so task and goal effects cannot be separated and the intervals do not cover other pairings.
- Predicate exceptions. An AgentDojo injection predicate that raises is scored as not carried out, and the committed records do not say whether that happened; runs made after this was found record the exception in
security_check_error. - An AgentDojo executor crash counts as errored.
ToolsExecutorinagentdojo0.1.35runsliteral_evalon every string argument and catches onlyValueErrorandSyntaxError, so a nested set literal (a set written inside a set) raisesTypeError: unhashable typeand ends the case. The runner records such a case aserroredwith that cause, as for any per-case failure; it is counted in the errored columns, and the executor was not patched mid-run, so every record of a model comes from one commit. - Suite deviation. The suite includes AgentDojo's
v1.1.2task fixes, whichget_suiteomits in0.1.35, so numbers are not directly comparable with results produced through that function.
- All tools are simulated. The agent only ever calls Python functions over an in-memory sandbox loaded from a local fixture; no real account, website, or system is targeted, and a unit test fails if a tool opens a socket.
- The injected goals are benign sandbox actions (send an email to a fictional outside address, delete a fixture file, create a calendar event, forward a fixture email or code) and never ask for content-policy-violating output.
- Results are aggregate only. The Report and this README give attack success rates and defense effects; the injection texts are generic published templates, none tuned against a named commercial product.
- Responsible disclosure. If benchmarking reveals a reproducible vulnerability in a specific real third-party product, publication of that payload stops and the vendor's responsible-disclosure process is followed (design.md, "Responsible disclosure and payload restraint").
- Third-party code, models, and their licenses are listed in THIRD_PARTY.md.
Code is released under the MIT License. Third-party packages, models and images are listed with their licenses in THIRD_PARTY.md; no third-party code or model weights are redistributed here, and the committed case records quote only short strings from AgentDojo's fictional workspace fixture (MIT). There is no published package or release.
This README is generated by python -m aib.docs_build from docs/README.template.md and the files under results/; do not edit it by hand.
- README: command
python -m aib.docs_build --template docs/README.template.md --out README.md, seed 20261017, commit4e02e31ef15d42585edb3bb1921ecf6c8340b9f5. - Results_Table: command
python -m aib.results_table --metrics results/metrics.json --out results/summary.md, seed 20261017, commit4e02e31ef15d42585edb3bb1921ecf6c8340b9f5. - Metrics: command
python -m aib.metrics --store results/raw --seed 20261017 --resamples 2000 --out results/metrics.json, seed 20261017, commit4e02e31ef15d42585edb3bb1921ecf6c8340b9f5.
This project was built with Claude Code (Anthropic's agentic coding tool) under a spec-driven workflow: written requirements, then a design, then a task list organized into milestones (the spec files under .kiro/specs/), each milestone ending in an automated gate script that had to pass before the next one started. The commit history shows this openly; commits carry a Co-Authored-By trailer naming the model that helped write them. Every reported number comes from a script in this repository and records the command, seed and commit that produced it, so nothing was typed, estimated or borrowed. I set the goals and constraints in the specification, I am accountable for the verification and the results, and I can explain and change any part of this code.

