If you only inspect each tool call in isolation, you can be perfectly accurate per-call and still miss the actual attack. Real agent compromise rarely shows up as a single malicious payload. It shows up as a sequence: a sequence of small reads, then a write to an outbound channel; a sequence of probing calls across different scopes; a sequence of calls whose risk score keeps inching up but never quite crosses the block line.

This is the gap session detection fills. Below are the sixteen patterns Clampd runs on every classify request, what each one fires on, and what we use them for.

The two buckets: flag-based and context-based

The patterns split into two layers.

Flag-based patterns read from a list of session flags accumulated across previous calls in the same session (e.g. bulk_read, schema_recon, denied). They run cheaply, with no JSON parse required. Eight of the sixteen patterns are flag-based.

Context-based patterns require a parsed session context: histograms of categories, rolling totals, recent risk trajectory. These are heavier (JSON parse, math) but they catch the patterns that flag-counting can't, like sawtooth evasion or volume anomalies.

The sixteen patterns

Names below match the function names in services/crates/ag-intent/src/session/patterns.rs. If you read the source, you'll find the actual thresholds (configurable per-pattern).

PatternLayerWhat it fires on
read_then_exfilflagBulk read followed by an outbound call. The classic exfiltration trail.
recon_then_attackflagSchema or capability probing followed by a destructive action against the same target.
scope_probingflagRepeated denied calls across different scopes. The agent is "trying doors."
rapid_fireflagBurst of calls inside a short window. Often a precursor to brute-force or scraping.
tool_diversity_spikecontextSudden expansion in the variety of tools the agent calls. Usually means new capability acquisition.
volume_anomalycontextOutbound data volume above the agent's rolling baseline.
permission_escalationflagCalls climbing the privilege ladder: read, then write, then admin.
cross_tool_bridgingcontextConnecting categories that don't normally talk: data from db:* ending up in net:* within the same session.
cross_agent_escalationflagAn agent acquires capabilities through a delegated agent that exceed its own.
unauthorized_delegationflagDelegation to an agent that wasn't approved as a downstream peer.
first_time_toolflagCalling a tool the agent has never called before. Light signal on its own; valuable as a multiplier.
escalating_riskcontextPer-call risk score trending up over the last N calls.
sensitive_data_flowcontextData tagged sensitive (PII, secrets) flowing toward an outbound category.
sensitive_chaincontextChain of sensitive accesses across multiple tools in the same session.
slow_dripcontextSmall amounts of data leaving over time. Below per-call volume threshold but adding up.
interleaved_evasioncontextSawtooth risk score: deliberately oscillating high and low to never trip the per-call block threshold.

Why sawtooth evasion deserves its own pattern

An attacker who knows your block threshold sits at, say, 0.85, can deliberately structure their requests to bounce between 0.7 and 0.4. Each individual call passes. The agent never gets blocked. The attack succeeds.

Single-call inspection has no answer for this. You need to look at the shape of the recent risk trajectory. interleaved_evasion looks for direction changes in the last 6 risk-score data points; if the trajectory shows N+ direction changes in a small window, that's not a normal workload, it's evasion.

// Detects when risk_trend shows a sawtooth pattern (up-down-up-down).
pub fn check_interleaved_evasion(ctx: &SessionContext) -> Option<PatternResult> {
    // N+ direction changes in 6 data points = sawtooth pattern
    ...
}

This pattern is one of the reasons single-stage WAFs miss a class of agent attacks they otherwise would catch. WAFs are stateless per-request by design.

How patterns feed the risk score

Each pattern that fires returns a PatternResult with a flag name and a risk_bonus. The risk bonus is added to the assessed risk for the current call. So a perfectly safe-looking call ("read one row") can become high-risk if multiple session patterns fire on it ("read one row" + slow_drip active + sensitive_chain active).

The flag is also persisted into the session for future calls. cross_tool_bridging firing on call N becomes a session flag that other patterns can read on call N+1.

This is intentional: it means an attack that took ten calls to set up still leaves a trail that the eleventh call sees. The session is the unit of detection, not the call.

Worked example: the slow-drip exfiltration

An attacker compromises an agent with read access to a customer database and write access to a webhook URL. Their goal is to exfiltrate the customer table. They know the agent's risk-block threshold and the per-call volume cap.

Single-call defence: the agent reads 50 rows at a time. Volume is below threshold. Each call passes.

What our session layer sees:

  1. Call 1: db:query:read. Volume: 50 rows. Per-call risk is low. first_time_tool fires once, contributing a small bonus. Allowed.
  2. Calls 2–8: more 50-row reads. volume_anomaly doesn't fire because the rolling avg is now 50 rows. But slow_drip starts watching the cumulative pattern.
  3. Call 9: net:webhook:send with 200 KB body. sensitive_data_flow fires: data tagged from db:* is leaving via net:* in the same session. cross_tool_bridging fires too. The bonuses combine on top of the per-call rules score.
  4. Call 10: another 50-row read. slow_drip now correlates the rolling outbound total against the rolling read total and contributes its own bonus.
  5. Call 11: another net:webhook:send. read_then_exfil fires (flag-based, looking at recent history). sensitive_chain fires (multi-tool chain of sensitive accesses). The accumulated session-derived bonus crosses the auto-suspend threshold, the kill cascade fires, the agent's tokens get revoked fleet-wide.

The slow-drip attack succeeded against per-call inspection. It didn't survive the session layer. (Per-pattern risk_bonus values vary by pattern and are tunable per org; the source of truth is services/crates/ag-intent/src/session/patterns.rs.)

What this isn't

Session patterns are not full behaviour modelling. They are an explicit, hand-built ruleset for known multi-step attack shapes. They catch what we know how to describe. They will miss attacks we haven't seen yet. We separately run a per-agent EMA risk score (the behavioural baseline layer) that catches "this agent is doing things it never used to do" without us having to enumerate the shape. Both layers feed the same final score.

Cross-agent: the special case

Two of the sixteen patterns specifically address agent-to-agent (A2A) workflows: cross_agent_escalation and unauthorized_delegation. These are critical because a delegation chain shifts the trust boundary: an agent that can't itself perform action X may legitimately delegate to an agent that can.

The patterns watch for two specific abuses:

Both pair with the tool descriptor hash check to make sure each agent in the chain is also calling the tool it actually got approved for.

What you can do without Clampd

Session detection isn't magic, it's just data we don't normally collect. Three suggestions if you're rolling something yourself.

Try Clampd in 60 seconds

One line of Python or TypeScript. Works with OpenAI, Anthropic, LangChain, CrewAI, Google ADK, and any MCP server. Self-hosted, source-available. The 16 session patterns are on by default; thresholds are tunable per-org.

pip install clampd npm install @clampd/sdk
Get Started → Why Clampd