unkin-agent 71c1999e27
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
Abort on invalid allowlist lines; refuse launch dirs exposing $HOME or the allowlist
2026-10-23 22:46:24 +11:00
2026-10-23 22:29:18 +11:00
2026-10-23 22:29:18 +11:00
2026-10-23 22:29:18 +11:00
2026-10-23 22:29:18 +11:00

agent-sandbox

Runs Claude Code in a rootless podman sandbox. The only thing visible from the host is the directory you launch it from, and the only way out of the sandbox is a per-session router that serves the hostnames on your allowlist.

Usage

cd ~/src/some-repo
agent-sandbox [claude args...]       # e.g. agent-sandbox --continue
agent-sandbox --exec bash            # a shell in the sandbox instead of claude
  • $PWD is mounted read-write at the same path. Files you create there are owned by your host user. Nothing else from the host is mounted, and SELinux labels on your files are never changed (label=disable).
  • It refuses to launch from $HOME, any parent of it, or any directory containing the allowlist.
  • Claude's home is the agent-sandbox-home podman volume (login, config and nested container images live here). All sessions share it.
  • podman works inside the sandbox (nested rootless). The host podman socket is never exposed.
  • You can run several sessions at once, from different directories.
  • Environment overrides: AGENT_SANDBOX_IMAGE, AGENT_SANDBOX_ROUTER_IMAGE, AGENT_SANDBOX_ALLOWLIST, AGENT_SANDBOX_HOME_VOLUME.

Allowlist

~/.config/agent-sandbox/allowed-hosts is copied from the packaged default on first run. It is read at the start of each session.

api.anthropic.com      exactly this host (ports 80 and 443)
.example.com           example.com and every subdomain (ports 80 and 443)
db.example.com:5432    this exact host on TCP 5432 too (ssh, postgres, ...)

You don't need a proxy setting. Allowlisted names resolve to loopback addresses inside the sandbox, and all other names return NXDOMAIN.

How it works

Each session gets its own --internal podman network with no route out, and a pod on that network that holds the sandbox's netns, DNS and hosts file.

  • router (on the internal net and the shared egress net): dnsmasq answers for allowlisted names only and never forwards a query. nginx routes 443 by SNI and 80 by Host header to allowlisted names only. Each host:port entry has its own router port, which forwards to that host and port only.
  • forwarder (in the pod, read-only, no capabilities except NET_BIND_SERVICE): socat listeners on the loopback IPs that forward to the router.
  • claude (in the pod, non-root keep-id user, no setuid binaries, no NET_ADMIN, NET_RAW or SYS_ADMIN): has no capability over the netns. It can't change addresses, routes, nftables, /etc/hosts or /etc/resolv.conf, and can't see or signal the forwarders.
  • Nested containers share claude's netns by default. They get the same DNS and forwarders, and they can't change the netns either. A nested container on its own network (--network pasta|bridge) has no egress.

Threat model

  • Confined: direct IP egress, other hostnames, DNS exfiltration (dnsmasq never forwards), SNI or Host spoofing (the destination is always the name the client sent, and it must be on the allowlist), the host filesystem beyond $PWD, and the host podman socket.
  • Not confined: anything you allowlist is still an exfiltration channel. git.unkin.net and api.anthropic.com can carry your data out. $PWD is writable, so anything there, including .git/hooks, runs later on the host if you trust it.
  • Router-side DNS resolves allowlisted names on the host's resolvers, so internal names like vault.service.consul work.
  • The home volume persists across sessions and is shared by all of them.

Development

make build        # local images (localhost/agent-sandbox{,-router}:dev)
make test lint pre-commit
make rpm          # dist/agent-sandbox-*.rpm
make patch|minor|major

The in-repo launcher runs the local :dev images. The RPM's launcher pins the images published by the release pipeline for its tag: artifactapi.k8s.syd1.au.unkin.net/docker-internal/agent-sandbox{,-router}.

S
Description
Rootless podman sandbox for Claude Code with allowlisted egress
Readme 286 KiB
v0.1.0 Latest
2026-10-24 20:40:16 +11:00
Languages
Go 91.4%
Makefile 3.4%
Dockerfile 3.1%
Shell 2.1%