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.

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.

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

(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.