10 min read
The web application firewall existed because application code could not be trusted to validate its own inputs. It sat in front, it saw every request, it had a policy, and it logged what it decided. It bounded the application's failures without making the application itself secure.
Agents are at that point now. A coding agent with a shell and a GitHub token can do anything the token can do, and the model deciding whether to do it is the same model that just read a file containing instructions from a stranger. Prompt injection is a property of the architecture rather than a bug in the prompt.
So a category appeared. AgentFence, Vigils, Harden, agent-wall, a dozen small projects with "firewall" in the name, plus sandboxed MCP runners. They differ in polish and agree on the shape: a local proxy between the agent and its tools that decides allow, deny or ask on every call, before the call runs.
Take AgentFence as the reference implementation, because its README is precise about the mechanics and the pattern is shared.
It is one Go binary. In proxy mode it spawns your MCP server as a subprocess and relays JSON-RPC over stdio in both directions, intercepting every tools/call. An allowed call is forwarded. A denied call gets a JSON-RPC error and the server never sees it. An ask call hits an approver, with a timeout and an auto-deny in non-interactive contexts. A second mode gates remote MCP servers over streamable HTTP. A batch mode replays recorded tool calls against the policy in CI and fails the build on a violation.
The policy is YAML, deny by default, with per-tool rules and constraints on arguments.
version: 1
defaults:
decision: deny
tools:
filesystem.read:
decision: allow
paths:
allow: ["src/**", "docs/**", "package.json"]
deny: [".env", "**/*.pem", "secrets/**"]
filesystem.write:
decision: ask
paths:
allow: ["src/**", "tests/**"]
shell.exec:
decision: ask
commands:
deny: ["rm -rf", "git push --force", "curl * | sh"]
github.create_issue:
decision: allow
github.delete_repo:
decision: deny
redaction:
patterns:
- name: generic_secret_assignment
regex: '(?i)(api[_-]?key|secret|token)\s*[:=]\s*\S+'Three features separate a firewall from a permission prompt.
Redaction. Secrets in tool arguments are masked before they reach the audit log and, in some tools, before they reach the model's context. The demo output shows the marker in place of the value. Without this, your audit log is a secrets file.
Argument validation. Path globs, URL allow-lists, shell command patterns. A rule for filesystem.write says which paths it may write to, as well as whether it may write at all. This is the syntax-aware command permissioning pattern the harness study found inside the harnesses themselves, moved out into a component you can put in front of any harness.
Tamper-evident audit. Append-only JSONL, hash-chained so a removed entry is detectable, Ed25519-signed so a forged one is too, with rotation and external sinks. When an incident review asks what the agent did on Tuesday, this is the only artifact that answers.
A coding harness with shell, filesystem and GitHub tools, some local and some over MCP. The firewall sits between the harness and every tool.
The boundary that matters most is the third one. The old setup put the GitHub token in the agent's environment and hoped the agent would use it well. The firewall setup gives the proxy the token and gives the agent a tool called github.create_issue. The agent cannot leak a token it never had.
The July 2026 MCP revision removed sessions and made every request self-describing: protocol version, client identity and capabilities travel in _meta, and the method and tool name travel in the Mcp-Method and Mcp-Name headers.
For a policy proxy that is close to ideal. It can make the cheap decisions from headers alone, without parsing a body, and the full decision from one request, without a session table to know who the caller is. It also sits behind a load balancer as easily as the servers it protects. We wrote up the migration separately; the short version for the firewall is that your proxy no longer needs to be sticky.
The roadmap goes further on the caller side. Agent identity is a named priority: DPoP so a stolen token cannot be replayed from another process, workload identity federation so an agent running as a cloud workload has its own identity rather than a borrowed human's, and standard token exchange between them. When that lands, the proxy can bind a policy to an agent identity instead of to whoever happened to hold the key.
Choose when: You run MCP servers and want enforcement you can read and version-control.
Avoid when: Your harness bypasses MCP for its main tools and you cannot wrap them.
Choose when: One harness, one team, and its built-in model is good enough.
Avoid when: You run several harnesses or need one audit trail.
Choose when: Several teams, several harnesses, shared governance.
Avoid when: One laptop and one repository.
For most teams the answer is two of the three. Keep the harness's native permissions on, since they are free, and put a policy proxy in front of the tools, because it is the only layer that still works when the harness is swapped or the model is talked into something. Add the meta-harness layer when there is a fleet.
Actor: Anyone who can get text in front of the agent: a dependency's README, an issue comment, a web page the agent fetched, a file in a cloned repository
Asset: Everything the agent's credentials reach: source, secrets, the ability to push, open pull requests, call APIs
Entry point: A tool result that contains instructions phrased as if from the operator
Impact: Credential exfiltration or a malicious change pushed under the agent's identity
The controls assume the model will follow the hostile instruction and make sure that following it has no effect, which is why none of them is a better prompt.
A firewall you cannot operate from a terminal will not be operated. AgentFence's README documents a command set that covers the whole lifecycle, and the commands show what "policy as a first-class artifact" means in practice.
Scaffold and validate. agentfence init --pack filesystem,github,shell writes a starter policy from curated templates for the three tool families most agents touch. agentfence validate --policy policy.yaml checks the syntax. agentfence policy test --policy policy.yaml --tests cases.yaml runs fixture-based tests against the policy, so a rule change is tested the way code is tested, before it is deployed.
Run in front of a server. The proxy wraps a local MCP server as a subprocess:
agentfence proxy --policy policy.yaml --audit-log audit.jsonl -- npx my-mcp-server --stdioFor a remote server, agentfence proxy-http --upstream https://mcp.example.com --listen 127.0.0.1:8787 --policy policy.yaml --audit-log audit.jsonl gates streamable HTTP and SSE, forwarding the traffic that carries no decision transparently.
Gate in CI. agentfence check --policy policy.yaml --call recorded-calls.jsonl --fail-on deny,ask --no-interactive --summary gate.json replays a recorded set of tool calls against the policy without a live agent, exits non-zero on any decision you name, auto-denies anything that would have asked a human, and writes a machine-readable summary for the pipeline. --dry-run evaluates without enforcing, which is how you introduce a stricter policy without breaking the build on day one.
Explain a decision. agentfence explain traces a single call through the policy and says which rule matched and why. When an engineer asks why their agent was blocked, this is the answer, not a log search.
Manage the audit chain. agentfence audit keygen --private key.pem --public key.pub makes an Ed25519 pair. Runs with --tamper-evident --sign-key key.pem write hash-chained, signed events. agentfence audit verify --log audit.jsonl --pubkey key.pub checks the chain and the signatures offline; audit anchor writes a signed checkpoint that a third party can hold, and verify accepts it with --anchor and --anchor-pubkey. audit summarize --top 20 gives the activity report. Rotation is --audit-max-size and --audit-keep; an external sink is --audit-sink syslog://... or http://....
Install. A checksum-verifying install script, Homebrew, Scoop and winget, a container image on GHCR, or go build. Releases ship with checksums, cosign signatures and SBOMs, which is the supply-chain posture you would hope for from a tool whose job is supply-chain posture.
The policy schema is small. A version, a defaults.decision, and a tools map keyed by tool name or wildcard (filesystem.*, github.delete_*), each with a decision of allow, deny or ask, optional constraints on path, argument, url and shell_command, and optional redact patterns per field. Each audit event carries a schema version, a session id, a monotonic sequence number, an RFC 3339 timestamp, the call id, the tool, the decision, a human-readable reason, a typed reason_code such as rule_match, path_denied or approval_timeout, the redacted arguments, and a mode of dry_run when applicable.
That level of detail is the difference between a security tool and a demo, and it gives the rest of the category a documented shape to be judged against.
Deny by default is the right starting point and the wrong stopping point. A policy that denies everything is a policy that gets switched off on Thursday. The craft is in getting from deny-everything to a policy engineers stop noticing, without opening a hole.
Start with reads. Allow filesystem.read on the source tree and documentation, deny it on .env, key files and anything under a secrets directory, and leave everything else at the default. Most agent work is reading, and a read policy that is right removes most of the friction immediately.
Then writes, scoped. filesystem.write on src/** and tests/** as ask for the first week, then allow once you have looked at a week of audit events and seen nothing you would have denied. Writes outside those paths stay denied. The ask period exists to collect evidence rather than to make a human click.
Shell is where the patterns live. Deny the destructive shapes outright: rm -rf, force pushes, pipes from the network into a shell. Allow the build and test commands the repository uses, by pattern, so pytest -x is fine and pytest -x; curl ... is not; the syntax-aware parsing in the proxy is what makes that distinction reliable. Everything else asks.
Network egress is allow-listed by destination, and the list is short. Package registries, your own APIs, the provider endpoints. A skill or a tool that needs a new destination gets it added by a human after reading why.
Then watch asks per hour and denies per hour for a week. An ask that always gets approved becomes an allow, and one that never gets approved becomes a deny. A deny on legitimate work is the policy telling you a scope is too narrow, so widen the scope rather than the default. A deny on anything else is the policy doing its job, and each one is worth reading, because it is either a bug in a skill or an injection.
The failure mode to design against is ask fatigue. A proxy that asks twenty times an hour trains the approver to click yes, and a proxy the approver clicks yes on is a proxy with no policy. Every ask should be rare enough that a human reads it. If it is not, the rule is wrong.
A firewall cannot stop an exfiltration that fits an allowed channel. If the agent may write to the repository and push to a branch, then data can leave in a commit. The mitigations are scope, small allow-lists, review of what is pushed, and canary secrets whose appearance anywhere fails a test. The firewall makes the channel narrow and audited; it does not make the channel safe.
It cannot stop the model from being persuaded. Prompt injection still happens, every time. The firewall's promise is narrower and more useful: when the model is persuaded, the action it is persuaded to take has no effect, and the attempt is on the record.
It does not replace the sandbox. A policy proxy decides which calls are allowed; a container or an operating-system sandbox limits what an allowed call can reach. Harnesses already ship sandboxes, Docker and Apptainer in OpenHands, native OS sandboxing in Codex and Gemini CLI, and the meta-harness layer adds cross-harness sandboxing with network interception. Keep the sandbox on and add the proxy in front of it.
It covers what passes through it. A harness that spawns its own shell outside the MCP path, or a skill that reaches a tool the proxy does not wrap, is outside the policy. The inventory question before deploying a firewall is: what are all the paths by which this agent can cause a side effect, and does each one pass the proxy?
Everything above polices what is called. The MCP roadmap's agent identity work is about who is calling, and it closes the last gap in the "API key in .env" story.
Today the proxy holds a token and decides per call. But the token itself is a bearer credential: anything that obtains it can use it from anywhere. Three roadmap items change that. DPoP (demonstrating proof of possession) binds a token to a key the caller holds, so a token copied out of a process is useless from another process. Workload identity federation gives an agent running as a cloud workload an identity of its own rather than a borrowed human's. Standard token exchange lets that workload identity be traded for the narrowly scoped token a particular tool needs, at the moment it needs it.
For the firewall this means the policy can be bound to an agent identity rather than to whoever holds a key, and the audit log can say which workload made a call rather than which token was presented. The roadmap does not give dates, and the 2026-07-28 release already hardened the authorization layer with issuer validation, credential binding to the issuing server, and Client ID Metadata Documents replacing dynamic registration. Build the proxy now; design the identity binding so that when those pieces land, the policy keyed on identity is a configuration change rather than a rewrite.
The policy proxy is the cheapest security control you can add to an agent this year.