· via dev.to (home feed)
TaintGuard gates Claude Code tool calls based on what a session has already done
A dev.to post introduces TaintGuard, a Claude Code mod that gates tool calls on session history rather than per-tool permissions, closing a prompt-injection gap where MCP mail access meets Bash.

An inbox, a shell and one context window
A developer writing on dev.to recounts connecting Claude Code to Gmail through a Model Context Protocol (MCP) server so the agent could triage mail, while keeping Bash available in the same session to run tests. Each capability looked acceptable on its own. Combined, an email telling the agent to run a one-liner to verify an account could hand the sender the contents of the inbox. The author's answer is TaintGuard, a Claude Code mod that records what kinds of content have entered a session and restricts later tool calls based on that history.
The trifecta behind the risk
The post frames the danger with a term from Simon Willison: the "lethal trifecta". An agent that can reach private data, that ingests content written by others, and that can communicate outward can be talked into sending the first through the second. Every MCP server adds at least one ingredient — mail, Drive, Slack and Notion hold private material, browser servers read pages written by strangers, and Bash or WebFetch offers a way out.
The pattern is not hypothetical. According to the post, Invariant Labs disclosed an attack in May 2025 against the official GitHub MCP server: text planted in a public repository issue steered an agent, asked to review open issues, into reading the user's private repositories and publishing what it found in a pull request on the public one. Invariant concluded the weakness is not in the server code and cannot be patched there, because every call was one the user's own token permitted.
Meta's "Agents Rule of Two", also cited, converts the trifecta into a design rule: within a session an agent should hold at most two of three properties — processing untrustworthy input, accessing sensitive systems or private data, and changing state or communicating externally. Tasks that need all three call for human supervision.
Per-tool permissions answer the wrong question
Existing controls, the author argues, express policy per tool. Docker's MCP Toolkit enables or disables individual tools in a profile; Claude Code rules allow, ask or deny, keyed on tool names. The answer is identical no matter what the session has already done. Allow accepts the risk even in the session where the agent just read attacker-influenced mail; deny forfeits Bash in ordinary coding sessions that touch nothing sensitive; ask shows the same prompt whether the context is clean or holds a fresh injection. Gateways add a second gap: they sit between the agent and its MCP servers, so built-in tools like Bash never pass through them, and a curl posting .env straight to an attacker's host stays invisible.
How the mod works
TaintGuard is a Claude Code mod — TypeScript hook handlers running inside the agent, terminal version 2.1.287 or later — rather than an external command hook, so it keeps the taint set in session state, appends a line to tool results and surfaces its own approval prompt in every permission mode, including bypassPermissions.
The policy has three parts. Labels name risk categories; the default preset defines private, untrusted and secret. Sources are tool calls that add labels — reading a message on a mail server adds private and untrusted, a successful WebFetch adds untrusted, reading .env or an SSH key adds secret. Sinks pair a label condition with tools to block and an action: deny, ask or audit. Every call, built-in or MCP, from the main agent or a subagent, passes through a single hook.
Ordering matters. Sinks are evaluated against the taint set as it stood before the call, so a tool that is both a source and a sink, such as sending mail after reading it, is judged on earlier taint only. Mail reads taint on attempt, so a read that errors partway still marks the session; WebFetch taints on success, since a failed fetch returns no third-party text. Taint only grows within a conversation — reloads, restarts and compaction retain or add labels, with the set rebuilt from conversation history and every label held until that finishes — while explicit user commands clear it, and clears are recorded in an audit log.
The lineage is older, the post notes: Perl's taint mode long refused data from outside a program reaching shell or file operations, and research systems such as CaMeL, from Google and ETH Zurich researchers, and Microsoft's Fides planner track trust through control and data flow. Those approaches need to own the agent's planning loop. TaintGuard sits at the tool-call boundary of an agent it did not write, seeing which tools ran but not which bytes the model carried between them, so it taints the whole session.
Why it matters
Agent risk is compositional: it emerges from what a session combines, not from any single tool. As MCP adoption makes it routine for one agent to hold mail, documents, shell and web access at the same time, per-tool allow and deny lists cannot express "Bash was fine before the mail read and suspect after it". History-aware gating trades precision for safety — once an email is in context, every later outbound call is treated as suspect even if nothing from it is used — but it offers a workable middle ground between disabling tools outright and rubber-stamping every session.
- #prompt-injection
- #mcp
- #security
- #claude-code
- #ai-agents