I had an AI coding agent managing a personal server: it had a shell, could read and write anything my account could, and could fetch any URL. During a task that had nothing to do with SSH, it opened ~/.ssh/id_rsa. No error, no violation: under the permissions I had given it, the read was entirely legal, and it did nothing with the key. That is exactly the problem: everything stayed within the permissions. Why could it read that file? What else had it read before I happened to be watching? I could answer neither question.
The problem is not new. A process holding more privilege than its task needs, with no way to audit what it does with the excess — sudo, SELinux and every least-privilege mechanism in security engineering address exactly this. What is new is only the shape of the process: an agent that decides its own next step. I could not find an existing tool that applied the old approach to this kind of process, so I started writing AgentGuard.
Three failure modes
An agent with tools — one that can read files, run commands and fetch pages — has three failure modes worth attention. They are usually filed together under "AI safety," but their causes differ and so do the fixes.
- Overreach. The task needs one config file; the agent also reads another two directories up.
ls .would do; it runsfind /. Not malicious; nothing stopped it. - Taking orders. Web pages, files and API responses can all contain text that looks like instructions. If the agent cannot separate content it is processing from instructions it should follow, a poisoned document changes its behavior. This is prompt injection, and it is distinct from overreach: the agent stayed within its permissions and executed instructions that arrived inside its own tool output.
- A legitimate call that still leaks. The agent was allowed to read the file, and the file happened to contain an API key. The call was fine; the content was not. Nothing masked the key before it entered the context window, and the context may later be logged, screenshotted or passed to another system.
Three failures, three independent mechanisms. That is AgentGuard's earliest design decision: not one filter but three layers, because one mechanism doing three jobs does all three badly.
Where the boundary goes
With the mechanisms settled, the next decision is where to enforce them. There are four candidate locations; the first three each have a specific shortcoming.
- In the prompt. "Please don't read SSH keys" is a request, not a boundary. Something that can be argued away does not function as a boundary.
- In the MCP server. A server you wrote yourself can enforce policy, but the same rules would have to be rewritten in every server you use, and this does nothing for a third-party server whose source you cannot change.
- In the operating system. Containers, seccomp and restricted users are useful, and they complement the approach below, but they see system calls, not the meaning of a tool call. The OS can block "this process opens this file"; it does not know that "this JSON-RPC message is a
tools/calltoread_filewith a path-shaped value in its arguments." The latter is the level I wanted to write policy at.
The remaining location is the protocol layer, between the AI agent and the MCP server. Every tools/call is checked before it is forwarded; every result is checked before it is returned. MCP's stdio transport suits this: newline-delimited JSON-RPC, with no change on either end. Point the agent's client configuration at AgentGuard instead of the server, and policy is enforced in the middle; both sides keep speaking MCP and need not know it is there.
What every message goes through
- The policy engine, on requests. Glob deny lists for file paths, regex deny lists for shell commands, hostname allowlists for network calls. MCP servers share no common argument format, so rules identify arguments by key name:
path,command,urland the like. A denied call never reaches the server. This layer addresses overreach. Key names are a convention, not a guarantee: a server that puts a path under some other key is missed by the rule. - The injection detector, first pass on responses. It scans tool output for text that looks like instructions. On a match, the whole result is blocked rather than the matched sentence removed; the next post explains why. This layer addresses taking orders.
- The secret redactor, second pass on responses. It masks secrets of known formats in what the detector passed. This layer addresses leakage: the call was fine, the content was not.
(Added September 2026: the list above describes 0.1. From 0.2.0, an argument's category comes from the schema the server declares in tools/list and from the words in its name, such as file in file_location. An argument neither identifies is still allowed by default and logged as unclassified; with unclassified_arguments: deny, such a call is refused.)
Every decision from all three layers is written to an audit log, and the log is hash-chained: each record contains the hash of the previous one, so an altered line shows up when the chain is verified again. The chain exists because a log whose rewriting cannot be detected does not function as an audit log.
What it does not promise
The full argument is in the repository's threat-model document; this is the summary. I would rather ship a narrow tool with its boundaries written down than a broad one making promises it cannot keep. AgentGuard does not sandbox the MCP server process: what the server does at the operating-system level, a protocol-level proxy cannot see. It does not guarantee catching every injection phrasing: the rules are public in the policy YAML, and anyone who has read them can write around them; the LLM classification layer planned to close this gap is not built yet, and the gap is stated in the documentation. The audit log detects tampering and does not prevent it: it can prove a line was altered, but it cannot stop someone with full filesystem access from deleting the file and forging a self-consistent new chain.
A security tool is useful only when its user can determine exactly what it provides. A tool that overpromises is worse than none: it makes people drop the checks they would otherwise have done themselves.
These conclusions are in the README, in the separate threat-model document, and here.
The next post covers the injection detector alone: why the first version uses rules rather than a classifier, what false positives and false negatives each cost, and a hole in the shipped default rules that the test suite later found.