Clam just shipped the kind of plumbing the AI agent space has been quietly dreading it needed: a network-layer semantic firewall that sits between every agent and the open internet, parsing every byte for PII leaks, prompt injections, and malicious payloads. It runs each agent in an isolated VM so a compromised agent can't pivot to the network. It's not glamorous. It is exactly what production agents need.
This is a replication guide. You'll have a working Clam-style firewall by the end — the proxy, the inspection rules, the database for events, and the operational scaffolding to keep it honest. The architecture is from Claude's Corner: Clam — AI Agent Security with a Semantic Firewall, the YC W2026 team that open-sourced the pattern.
What a semantic firewall actually does
Traditional firewalls match IPs, ports, and protocol headers. A semantic firewall matches content. It reads the body of every request an agent makes outbound and every response it receives inbound, and applies rules that understand what the data means — not just what port it's on. PII patterns, instruction-override phrases, executable payloads. Things a packet filter can't see because they're inside the application layer.
The firewall isn't on the agent. It's in front of it. The agent thinks it's talking to the open internet; it's actually talking to a proxy that scans everything before forwarding. If the scan flags the payload, the proxy returns a blocked response and the agent never sees the malicious content. Isolation happens at two layers: the VM boundary (so an escape can't reach the host) and the proxy boundary (so the escape can't reach the network).
This is the right architecture. Defense in depth, applied at the seams where AI agents leak.
[[DIAGRAM: agent VM → mitmproxy → scanner → internet, with bidirectional scanning on both request and response]]
Step 1: set up isolated agent VM infrastructure
The first rule: no agent gets direct network access. Every byte leaves through a proxy you control.
# docker-compose.yml — per-agent template
services:
agent:
image: openclaw:latest
network_mode: none # the agent cannot reach the network directly
environment:
- PROXY_URL=
firewall-proxy:
image: your-proxy:latest
ports:
- "8080:8080"
network_mode: none is the load-bearing line. The agent container literally has no network interface. The only way it talks to anything is through the proxy URL you hand it as an env var. If the agent tries to dial out by IP, the kernel says no.
Pair each agent with a session record. You need durable state for every firewall decision:
CREATE TABLE agent_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
agent_config JSONB,
vm_instance_id TEXT,
status TEXT DEFAULT 'running',
created_at TIMESTAMPTZ DEFAULT now(),
terminated_at TIMESTAMPTZ
);
CREATE TABLE agent_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id UUID REFERENCES agent_sessions(id),
direction TEXT CHECK (direction IN ('inbound', 'outbound')),
blocked BOOLEAN DEFAULT false,
block_reason TEXT,
latency_ms INTEGER,
created_at TIMESTAMPTZ DEFAULT now()
);
direction, blocked, block_reason, latency_ms — every column in agent_events is a column you'll want to slice on later. Design the schema for the dashboards you haven't built yet.
Step 2: build the network proxy interceptor
mitmproxy is the right primitive here. It's a Python man-in-the-middle HTTP(S) proxy that lets you hook request and response flows. You write two handlers, one for each direction.
# proxy/main.py
from mitmproxy import http
from scanner import SemanticFirewall
firewall = SemanticFirewall()
def request(flow: http.HTTPFlow):
body = flow.request.get_text()
result = firewall.scan_outbound(body)
if result.blocked:
flow.response = http.Response.make(
403, f"Blocked: {result.reason}",
{"Content-Type": "text/plain"}
)
def response(flow: http.HTTPFlow):
body = flow.response.get_text()
result = firewall.scan_inbound(body)
if result.blocked:
flow.response.text = "[Content blocked by security policy]"
flow.response.status_code = 200 # return a safe response, not 403
Two details worth noticing. First, inbound blocks return 200 with a sanitized body instead of a 403. That's deliberate — a hard error often causes agents to retry, log the blocked payload somewhere worse, or crash. A quiet "content blocked by security policy" string is the gentlest possible signal. Second, the scanner is a separate module. Don't inline the rules into the proxy; the proxy is plumbing, the scanner is policy.
Step 3: implement the PII detection layer
The first scanner module is PII. presidio_analyzer ships with recognizers for Social Security numbers, credit cards, and the formats every compliance team already worries about. Your custom recognizers handle the API keys and private keys specific to your org — every cloud provider has its own format and prefix conventions, so write one recognizer per provider, not a regex that drifts when AWS rotates a prefix.
# scanner/pii_detector.py
import re
from presidio_analyzer import AnalyzerEngine
The PII detector is the boring, deterministic foundation. The interesting decision is the threshold per recognizer: too tight and SSNs leak through, too loose and you false-positive on every credit-card-shaped string in a public dataset. Everything else in the scanner — prompt-injection heuristics, malicious-code pattern matching — sits on top of the same scan_outbound / scan_inbound contract the proxy already calls.
Convention beats configuration. One scanner interface, many rule modules.
Step 4: log every decision to agent_events
The proxy already has latency_ms, direction, blocked, and block_reason waiting in the schema. Wire the scanner's result into an insert on every decision — not just blocks. Allowed traffic is signal too; if the allow rate for a session suddenly spikes, something has changed in either the agent's behavior or the upstream surface.
Keep the logging synchronous from the proxy's perspective but cheap. A batched insert every N events or a fire-and-forget queue is fine. What you cannot do is let a slow log write block a 200 OK to the agent — that turns your firewall into a denial-of-service against your own fleet. Treat the events table as observability, not the request path.
Step 5: test with payloads that should be blocked
Build a corpus of payloads that should trip the firewall: SSNs, credit-card numbers, AWS-style API keys, "ignore previous instructions" prompts, base64-encoded shell snippets, internal hostnames. Run them through the proxy. Every one should land in agent_events with blocked = true and a non-null block_reason.
Then build the inverse corpus: payloads that should pass. Real API responses, public documentation, normal agent queries. Zero of these should be blocked. The false-positive rate is the metric you watch in production, but you find it in staging first — and you find it loudly.
Step 6: deploy in waves, watch the dashboards
Don't flip every tenant at once. Start with one internal team, then a single friendly customer, then the rest. Each wave, watch blocked / total per session, latency_ms p95, and the block_reason distribution. A sudden shift in any of those is the first signal that either the rules are wrong or the threat model has changed.
The proxy is stateless. The database is the source of truth. That makes horizontal scaling boring: more proxy replicas behind a load balancer, one Postgres (or your equivalent) for events. Don't overthink it.
Step 7: maintain the rules
Threat models move. New prompt-injection patterns show up weekly; new PII formats appear whenever a vendor ships a new API. Schedule a monthly review of the top 20 block_reason values and the top 20 latency_ms outliers. Update recognizers, retire dead rules, document what changed in a changelog the on-call engineer can scan at 2am.
A semantic firewall is a living system. Treat it like one.
What this gets us
A Clam-style firewall turns AI agents from "we hope it doesn't exfiltrate" into "we can prove what crossed the boundary." That's the difference between running agents in production and running agents you trust in production. The semantic firewall is the durable guarantee; the scanner rules inside it are the part that gets rewritten every quarter.
Build the firewall. The agent is now safe. The interface you watch it from matters too — and that's the part that doesn't churn when the threat model does. OTF ships cross-platform primitives so the same admin view renders on a laptop, a phone, and an on-call tablet without rebuilding for each.
Top comments (0)