Skip to main content
Containment answers a different question from every other detector: not “does this look like an attack?” but “was the agent ever authorized to do this?” Opt-in and fail-closed — read the cost before enabling it in production.

Why detection alone has a ceiling

Nasr et al., The Attacker Moves Second (USENIX Security 2026), ran tuned, search-based attacks with detector-score feedback against twelve published defenses and bypassed all of them at over 90% attack success — including detectors architecturally similar to ours. Most had originally reported near-zero attack success on static benchmarks. The paper states plainly that stacking more detectors does not fix it. That result is not an argument against detection. It is an argument about where the answer comes from: This is the core idea behind CaMeL and FIDES: derive the policy from trusted input, then enforce it regardless of what the untrusted channel says.

Before you enable it: the objective must be trusted

This control is worth nothing if the attacker can write the objective.The entire security argument is that the envelope derives from input the attacker does not control. If your user_objective is itself lifted from attacker-influenced content — an inbound email, a ticket body, a chat message, a scraped page — the argument collapses completely and containment provides no protection at all.The objective must come from a genuinely trusted channel: a human typing it, or a system-owned template. CaMeL carries the same precondition. There is an executable test recording this (test_a_poisoned_objective_defeats_containment) so the limitation cannot quietly disappear.

How it works

user_objective is parsed into a capability envelope — which of the four capability axes the stated task authorizes. Every tool call is then checked against it.
The derivation function takes one argument, the objective string. It cannot see tool outputs. That signature is the security argument, and there is a test asserting the signature itself so a later refactor cannot quietly weaken it.
Containment is deterministic — no LLM. A judge that can be prompted is a judge an injection can argue with, and the whole value here is that the policy is not negotiable. The trade-off is that derivation is keyword-based and therefore imprecise, which is exactly why it is opt-in and why an unrecognized objective grants nothing rather than everything.

It catches what dataflow analysis structurally cannot

Consider an agent told to summarize tickets that instead deletes a file. Nothing leaked — there is no tainted value and no sink — so FLOW001 correctly reports nothing. Containment still blocks it: The two are complementary: containment bounds what is reachable; taint analysis inspects what happens inside those bounds. An objective that legitimately grants send_email still should not send your API key — that is FLOW001’s job.

Enabling it

By default only the irreversible axes are enforced — public_sink and destructive. Reading is comparatively safe, and containing it too would stop most legitimate agents dead. Pass enforced_axes to widen or narrow that.

What this costs you

Be clear-eyed about this before switching it on:
  • An agent whose objective does not name its side effects will be stopped. “Handle my inbox” does not authorize sending. If your objectives are terse or auto-generated, expect false positives.
  • Unrecognized objectives grant nothing. That is the correct direction for a containment control, and it is disruptive.
  • An unlabeled tool is not contained — same limitation as FLOW001. A tool with no capability labels has no capability to check, so labeling remains the one step worth being thorough about.
  • One authorized send authorizes other ordinary sends. Containment is axis-level. “Send the report to finance” grants public_sink, which also permits send_email to an attacker-chosen recipient — FLOW001 remains the control that inspects which value reaches that sink. The exception is below.
  • Derivation is keyword-based, not semantic. It reads verbs of transmission (send, publish, forward) and destruction (delete, cancel, transfer). Ambiguous nouns need a direction to count: “email the report to finance” authorizes a send; “delete all emails from newsletters” does not.
Run it in warn mode first and read the findings before you let it block anything.

High-consequence capabilities must be named

Axis granularity alone turned out to be too coarse, and an adaptive attack against our own control found it: “Book me a table tonight” legitimately grants public_sink — a reservation is an outbound action — and that grant then also authorized send_money, because both tools carry the same axis. Money movement, for free, off an unrelated dinner booking. So a tool whose name announces a high-consequence capability must have that capability named in the objective too, whatever the axis says: Covered tokens: money, fund, payment, transfer, wire, withdraw, crypto, wallet, password, credential, secret. The rule is inert for ordinary tools — send_email carries none of these — so it adds no false positives on the common case. Only tools that announce they move money or handle credentials have to be asked for by name.

Reading the finding

The aggregate threat_type for a containment-only finding is unauthorized_capability.

Next steps

Agent Trace Analysis

Submit a run and label your tools

Threat Detection

What each detector catches