The agent bug that worries me most isn’t a cinematic jailbreak. It’s stdout quietly getting a promotion.
One moment a string is a tool result. A few steps later that same string is evidence, a retry plan, a memory entry, or the reason the agent just called a write tool. Nobody signed off on the promotion. The text wandered from “data” to “decision” wearing a fake moustache, and the agent waved it through. That is the whole game: agents turn context into action, so every scrap of context is a potential instruction you never wrote.
Allowed: Web page → answerSummarise or cite. Don't let page text drive a write or a credentialed call.
Blocked: Email → write actionDraft from it. Don't send, delete, or escalate without a human in the loop.
Needs approval: Tool output → tool callClassify the result before reuse. Sensitive follow-ups need sign-off.
Needs approval: Memory → external APIPersistence raises the stakes. Trace the source before it leaves the system.
Allowed: Stdout → answerKeep byte-sensitive values exact. Treat transcript text as evidence, not authority.
Context Is Now Supply Chain
Traditional supply-chain security asks what entered the system — which package, binary, image, or dependency. Agents force a second question that build-time tooling never had to answer: what context shaped the next action?
A 2026 SoK paper frames this as an agentic runtime supply chain: agents assemble part of their behaviour at inference time from documents, memory, tools, API responses, and other agents. ContractBench shows the quiet version of the failure — models routinely mishandle “observation contracts” like OAuth state, session tokens, and presigned URLs. No model it tested cleared 80%; the best, Claude-Opus-4.6, managed 77.8%. That’s the polite end of the problem.
The hostile end is louder. MCPTox built 1,348 malicious tool-poisoning test cases across ten risk categories. VIPER-MCP scanned 39,884 real-world MCP repositories and confirmed 106 0-day vulnerabilities through end-to-end exploits, 67 of which already carry CVE IDs. And Oracle Poisoning is the one that nags at me: an agent can reason perfectly over corrupted structured data and still arrive, confidently, at the wrong answer.
MCP takes most of the heat here, but it isn’t the disease — it just hands the disease a convenient name badge. The boundary problem predates it and will outlive it.
The Boundary Review
Before you hand an agent real tools, list every way context can get in:
user prompt
retrieved document
web page
email
issue / PR / ticket
tool description
tool schema
tool output
CLI stdout / stderr
memory read
previous agent output
trace replay
Then give each one a class, decided before the model has a chance to reinterpret it:
type BoundaryClass =
| "opaque_contract"
| "untrusted_text"
| "capability_manifest"
| "evidence"
| "action_result"
| "memory";
And attach policy to the class, not to your good intentions:
type BoundaryPolicy = {
passToModel: boolean;
preserveBytes?: boolean;
hashBeforeUse?: boolean;
scanForInjection?: boolean;
requireApprovalForWrite?: boolean;
traceToSink?: boolean;
};
const boundaryPolicy: Record<BoundaryClass, BoundaryPolicy> = {
opaque_contract: {
passToModel: false,
preserveBytes: true,
hashBeforeUse: true
},
untrusted_text: {
passToModel: true,
scanForInjection: true,
traceToSink: true
},
capability_manifest: {
passToModel: true,
hashBeforeUse: true,
requireApprovalForWrite: true
},
evidence: {
passToModel: true,
traceToSink: true
},
action_result: {
passToModel: true,
traceToSink: true
},
memory: {
passToModel: true,
scanForInjection: true,
traceToSink: true
}
};
It isn’t glamorous, and that’s rather the point. Glamour is how production agents end up reading mystery YAML with a straight face.
Review Flows, Not Calls
Per-call approval feels safe and isn’t. A chain can be dangerous even when every individual call looks reasonable, because the risk lives in where the data travels rather than in any single hop. So I keep a source-to-sink matrix:
SOURCE ALLOWED SINKS BLOCK WITHOUT APPROVAL
public web page summary, citation email, prod write, credential use
tool description tool selection filesystem read, network exfil
tool output next read-only step write tool, payment, deploy
memory personalisation external send, authority grant
opaque contract exact downstream API model context, paraphrase, trim
“Is this text safe?” is the wrong altitude. The question that actually protects you is downstream:
What can this text influence next?
That is the moment tracing stops being observability theatre and starts being security work. OpenTelemetry’s GenAI semantic conventions and OpenAI Agents SDK tracing both record model calls, tool calls, and tool results — exactly the material you need to answer the only question worth asking after an incident: what shaped this action?
One caution, learnt the boring way (which is sadly how most useful lessons arrive). Full trace content can carry prompts, tool schemas, tool arguments, and tool results, so capture enough to reconstruct the boundary and no more. An observability stack quietly becoming a secret warehouse is its own incident in waiting. A minimum useful trace is small:
{
"step_id": "call_017",
"source_class": "untrusted_text",
"source": "github_issue_body",
"sink": "external_message",
"crosses_trust_domain": true,
"human_approved": false,
"blocked": true
}
The PR Template I Want
## Agent Boundary Review
- New context ingress:
- Boundary class:
- Allowed sinks:
- Blocked sinks:
- Can this influence a write, deploy, payment, email, or external API?
- Is byte integrity required?
- Is expiry required?
- Can this persist into memory?
- What trace field proves the source-to-sink path?
- What red-team case covers it?
It’s small and faintly annoying to fill in, which is usually a sign a process is doing its job rather than performing one.
Scan, But Don’t Worship the Scanner
The current CLI path starts here:
uvx snyk-agent-scan@latest --help
uvx snyk-agent-scan@latest inspect --help
mcp-scan still works as a compatibility alias, but it now warns that it has been renamed, so don’t be thrown when it does.
Worth saying out loud before you run any of this: scanners may start the MCP servers in a config so they can inspect tool descriptions. I wouldn’t point one at my real workstation config as a casual blog-demo command. Use a disposable container, VM, or throwaway config first, get comfortable with what it launches and what it reports, and only then wire it into CI.
Scanners earn their keep on manifests, skills, prompt-injection patterns, suspicious downloads, toxic flows, and credential mistakes. What they can’t do is prove runtime behaviour is safe — MCP-SandboxScan exists precisely because static analysis can’t see what a tool only does once it’s running.
Red-Team the Boundary
Reach for Promptfoo agent red teaming, Inspect, AgentDojo, PyRIT, garak, or plain boring custom tests. Whatever you pick, keep this set of cases around for good:
tool description asks for ~/.ssh/id_rsa
schema contains a parameter named summary_of_environment_details
tool output says "ignore previous instructions"
error message includes a new instruction
retrieved page asks the agent to call a write tool
OAuth state is paraphrased, decoded, trimmed, or expired
memory from an untrusted source influences an external send
two harmless tools compose into one harmful flow
trusted structured data is corrupted and treated as ground truth
When one fails, resist the urge to log “model got confused”. Write down the boundary that actually broke:
untrusted_text -> external_message was allowed without approval
A confused model is a shrug. A named boundary failure is something you can fix.
When Not to Bother
If an agent has no tools, no memory, no secrets, no filesystem, no network, and only ever hands text back to the same user who asked for it, a full boundary review is overkill. Skip it with a clear conscience.
If it can email people, touch production, change code, move money, read private data, persist memory, or call tools it discovered at runtime, do the review. Forget whether it counts as “AI”; the only question that decides it is whether untrusted context can reach a consequential action.
Takeaway
The habit worth building is to stop filing everything under one heading called “context”. The same tool output might be evidence, an injection channel, an opaque contract that should never touch the model, a poisoned runtime dependency, or perfectly harmless text — and which of those it is depends entirely on where your policy lets it go.
So classify each source, trace where it really flows, and test the flow rather than the call. The model was never the thing to secure. The boundary is: the point where outside text becomes inside authority.
Comments & Reactions
Got a thought, a war story, or a “well, actually”? Sign in with GitHub and jump in.
Loading comments…