Skip to content
← All projects
2026Founder: designed by me, implemented by directing AI coding agents

Grit — a policy gateway for MCP tool calls

Grit sits between an MCP client and the tools it calls. Policy is keyed by (server, tool). Path arguments are fully resolved — following symlinks — and checked component-wise against allowed and denied roots. A workspace snapshot is taken before any tool that may mutate runs, so a bad call can be undone from what the filesystem actually shows. Credentials are AES-256-GCM encrypted at rest and decrypted only for the tool that declared them. Every decision lands in an append-only audit log.

RustMCPPolicy isolationPath containmentAES-256-GCMRollback
Deny by defaulta tool with no declared policy is refused, never given a default

Architecture

Architecture of Grit — a policy gateway for MCP tool calls, as a pipeline. 1. Authorise: look up the (server, tool) policy; undeclared means denied 2. Contain: resolve every path argument, check it against allowed and denied roots 3. Snapshot: capture the workspace if the tool may mutate 4. The host runs the tool: not Grit's business 5. Complete: size cap, injection scan, rollback on refusal, audit either way
  1. 01

    Authorise

    look up the (server, tool) policy; undeclared means denied

  2. 02

    Contain

    resolve every path argument, check it against allowed and denied roots

  3. 03

    Snapshot

    capture the workspace if the tool may mutate

  4. 04outside

    The host runs the tool

    not Grit's business

  5. 05

    Complete

    size cap, injection scan, rollback on refusal, audit either way

The problem

An agent talked into passing a bad path, a tool that writes more than it claimed, an injected instruction that redirects a file operation — these are the realistic failures of tool-using agents, and none of them are caught by asking the agent to behave. They need a layer that decides before the call and measures after it, from outside the agent.

Approach & decisions

  • A two-phase API — authorise before, complete after — because the interesting moment belongs to somebody else: Grit never executes the tool, the host does. A single function would have to snapshot too late or never roll back.
  • Paths are resolved first, then compared. Checking the string the tool was handed, rather than where it actually points, is exactly how a symlink walks a permitted call out of its root.
  • mutates defaults to true because the cost of being wrong is asymmetric: a read marked mutating wastes a snapshot; a write marked read-only means no rollback exists when it matters.
  • Snapshots are bounded — 5,000 files or 64 MB — and exceeding the bound is an error, never a partial capture, because a rollback from half a snapshot would appear to succeed.
  • In the secret store, a secret's name is authenticated as associated data under GCM, so swapping two ciphertexts in the store file fails closed instead of quietly handing a tool the production token it was not meant to have. Cryptographic failures return one opaque error: distinguishing a bad key from a bad tag is how oracles get built.
  • A real race surfaced only once CI was actually executing the suite: one test mutated the process-wide master-key variable while others were decrypting. Fixed by serialising those tests behind a mutex, and reproduced clean over eight consecutive runs before the fix was called done.

Results

  • Tests across policy, containment, snapshot-and-rollback and the crypto store, green in CI with secret and dependency scanning on every push.
  • Rollback is measured from the filesystem, not from anything the tool says about itself, so it holds regardless of how well-behaved the tool turns out to be.
  • The injection heuristics are documented as a signal, not a boundary: pattern-matching on prose is evaded by rephrasing. The containment is the policy layer; the patterns raise an alarm.

What this honestly is not

  • Not kernel sandboxing. No namespaces, no seccomp, no cgroups — a tool Grit permits could ignore the path it was handed and open something else directly. It checks arguments, not syscalls, and is built to sit inside a real sandbox rather than replace one.
  • Not network policy. allow_network is declared and recorded, not enforced.

Stack

Rustaes-gcmsha2walkdirzeroizeserdetracinggitleaks · osv-scanner · cargo-audit in CI