airlockd, deployed from the bundle) that runs
detection and turns it all into a per-person record: see
the review plane below.
Per session, the agent runtime runs two containers: a sandbox where the
agent works, and an egress proxy that is the sandbox’s only way out.
Scoped isolation
The sandbox is attached to a single Docker network,airlock-net, created with
--internal. An internal network has no route to the internet and none to the
host, so the only peer the sandbox can reach is the proxy.
The proxy container is the only one attached to both airlock-net and the
default bridge network, which is what gives it egress.
The sandbox container
Started fresh for every run with--rm, so nothing persists in it:
- Your repo is mounted read-write at
/<reponame>, which is also the working directory and the container hostname. The name is lowercased, and any character outsidea-z 0-9 _ . -becomes-. HTTP_PROXY,HTTPS_PROXY(and lowercase variants) point at the proxy on port8888.NO_PROXYcoverslocalhost,127.0.0.1.AIRLOCK_REPOholds the in-container repo path.AIRLOCK_USER_IDandAIRLOCK_USER_EMAIL(fromairlock login) identify who is running this session.- Allowlisted host env vars and any MCP credentials are injected.
- A TTY is attached only when your terminal has one, so piping into
airlockworks.
The agent’s home is not yours
So the agent isn’t re-authenticating and re-picking a theme on every launch, two paths persist across runs:
This is an Airlock-owned home, deliberately not your
~/.claude. The
sandbox keeps its own credentials, isolated from the ones on your host.
The egress proxy
One proxy container per repo, namedairlock-proxy-<repo>, restarted on
each airlock run. This is also what makes allowlist changes take effect
immediately: edit .airlock.toml, start a sandbox, and the new policy is
live.
Identity is threaded in when the container starts: AIRLOCK_USER_ID and
AIRLOCK_REPO ride in as environment, and every audit line carries them. On
a shared machine, restart the sandbox after switching accounts. Attribution
follows whoever was signed in when the proxy started.
For each request, the proxy takes the hostname, checks it against policy
(exact match or a subdomain of a listed domain), and writes an audit entry
before acting on the verdict.
- Allowed
CONNECT— dials upstream and splices bytes. TLS stays end-to-end between the agent and the provider. - Allowed plain HTTP — forwarded upstream.
- Refused —
403naming the host and the policy that refused it.
The audit log
Every attempt, allowed or denied, is appended as one JSON object per line to:user_id comes from the identity threaded into the proxy’s environment at
start; for an unattributed run (no airlock login) it is simply omitted.
Denials are written immediately; allowed tunnels log on close, with
byte counts in both directions and duration.
The same events stream to the proxy container’s stdout as ALLOW / DENY
lines, so docker logs -f airlock-proxy-<repo> gives you a live view.
To see what a repo’s agents have been reaching:
The review plane
The egress log answers where did it connect. It cannot answer what did it read — by design, since TLS stays end-to-end and the proxy never sees plaintext. That answer comes from the agent’s own transcripts, which survive the container because the agent’s home is persisted.airlockd attributes every ingested event to its
token’s user (a token cannot write anyone else’s history), records a
server-side received_at beside the client timestamp so backdating is
visible, and raises decision flags at ingest. The
console is served by the same daemon, login-gated with
role-scoped reads. And where an agent workflow crosses an irreversible
boundary, the same machinery backs
action gates: verification before the action.

