Skip to content

89luca89/clampdown

Clampdown

clampdown icon

Run AI coding agents in hardened container sandboxes.

AI coding agents run arbitrary code on your machine. clampdown confines them to a hardened container sandbox: filesystem access is restricted to your project, network egress is limited to the APIs the agent needs, and every tool container the agent spawns gets the same enforcement.

clampdown architecture

Blue = sidecar. Yellow = agent. Green = tool containers. Red text = untrusted egress. Green text = trusted egress. Border color = namespace trust level (see legend in diagram).


Container overview

clampdown runs three container types with different privilege levels:

Sidecar Agent Tool (nested)
Purpose Container runtime + firewall AI agent process Tools the agent spawns
Base image FROM scratch (no shell, no libc) Alpine User-chosen
Capabilities 16 (SYS_ADMIN, NET_ADMIN, ...) 0 (cap-drop=ALL) 10 default (effective set empty)
Seccomp ~85 blocked ~133 blocked ~133 blocked + inherited sidecar
Landlock No (incompatible with mount()) workdir RW, rootfs RO Derived from bind mounts
Secrets Registry credentials (opt-in) None (sk-proxy dummy key) None
Network egress N/A Deny + allowlist only Allow, private CIDRs blocked
Rootfs Read-only Read-only Read-only
Runs as root (userns-mapped) Non-root Non-root (hook-enforced)
SELinux container_engine_t container_t container_t
AppArmor unconfined confined confined
  • The sidecar is privileged enough to run podman and enforce firewall rules, but has no shell and no libc so it cannot be repurposed. A seccomp-notif supervisor intercepts all mount/umount operations in real time, blocking attempts to unmount masked paths or remount protected files -- closing the gap where Landlock cannot be applied.
  • The agent never holds real API keys -- it gets a dummy key and talks to a local auth proxy that injects the real credentials. Even if the agent connects to the upstream API directly, the dummy key produces a 401.
  • Tool containers have open egress to the public internet but hold no secrets: they cannot reach private networks, and credentials are never forwarded unless explicitly opted in.

Threat model

clampdown treats the agent as an untrusted process. Not because current models are malicious, but because the attack surface is real: prompt injection can hijack an agent's actions, jailbreaks can override its instructions, and even well-behaved models run arbitrary code that may do things you didn't ask for. If the agent can curl, cat ~/.ssh, or podman run --privileged, the question is when -- not whether -- something goes wrong.

Every defense in clampdown enforces from outside the agent's process. Filesystem restrictions are kernel-level Landlock rulesets applied before the agent binary starts. Network policy is iptables chains installed by the sidecar, in a network namespace the agent shares but cannot configure. Seccomp profiles are inherited from the container runtime -- the agent cannot modify or remove them. OCI hooks validate every container the agent creates before its entrypoint runs, and there is no flag to skip them.

None of these controls depend on the agent cooperating. A fully compromised agent -- one that ignores its system prompt entirely and tries to escape -- hits the same kernel-enforced walls as one following instructions. That's the point.


Security model

Nested container enforcement

The agent runs inside a zero-capability container: cap-drop=ALL, read-only rootfs, no-new-privileges, and a seccomp profile that blocks ~133 syscalls including all known kernel exploit primitives (io_uring, userfaultfd, BPF, perf_event, splice/tee). When the agent needs to run a tool -- a compiler, a test runner, a shell command -- it sends a podman run request to the sidecar's API socket. The sidecar intercepts every container creation through two OCI hooks that run synchronously before the container process starts:

precreate (seal-inject): rewrites the container entrypoint to sandbox-seal, which applies Landlock and cleans up unsafe file descriptors before execing the real command. The hook also assigns a non-root UID mapped outside the container's user namespace, mounts hidepid=2 on /proc, and injects masked paths over sensitive kernel interfaces (/proc/kcore, /proc/sysrq-trigger, and nine others).

createRuntime (security-policy): validates the final OCI config against 17 security checks -- blocking privileged mode, disallowed capabilities, host namespace sharing, unsafe bind mounts, dangerous devices, RW re-mounts of protected paths, /proc mount type, and supplementary groups. A container that fails any check is killed before its entrypoint runs.

Both hooks apply to every podman run the agent issues. There is no opt-out.

Sidecar mount supervisor

The sidecar cannot use Landlock (mount() internally triggers Landlock path hooks). To compensate, the sidecar entrypoint installs a seccomp-notif BPF filter that intercepts mount, umount, and process-targeting syscalls. A supervisor goroutine evaluates each call against a set of protected paths discovered from /proc/self/mountinfo at startup.

This blocks attempts to unmount masked paths (exposing .env contents), remount protected paths read-write, create a new procfs to access /proc/1/mem, or use ptrace/process_vm_readv to read the supervisor's memory. Legitimate mount operations (podman creating containers) pass through unimpeded.

The filter is inherited by all processes in the sidecar. For agent and nested containers, the workload seccomp profile already blocks mount/umount entirely (stricter than the supervisor's USER_NOTIF), so the supervisor effectively only handles sidecar-level operations.

Landlock filesystem isolation

Landlock is a Linux kernel LSM that enforces filesystem access control at the kernel level, beneath and independent of container bind mounts and Unix permission bits.

sandbox-seal applies a Landlock ruleset inside the agent process just before it execs the agent binary. The rules cannot be lifted after exec -- Landlock policies are inherited across exec and can only be made more restrictive, never relaxed.

Agent policy:

  • Read-write + execute: your working directory (the project being edited)
  • Read + execute: system directories (/usr, /bin, /lib, /etc, ...)
  • No access: everything else -- host home, other projects, sensitive kernel paths

Tool containers launched by the agent get a derived policy based on their mount list: writable access is granted only to paths explicitly bind-mounted into the container.

Landlock V3 (kernel >= 6.2) is a hard requirement. The launcher refuses to start if Landlock is absent. Kernel >= 6.12 is recommended for full feature coverage (V4 TCP connect at 6.7, V5 IoctlDev at 6.10, V6 IPC scoping at 6.12). V7 audit logging requires 6.15+.

Network isolation

The agent container shares the sidecar's network namespace. All egress is controlled by iptables rules the sidecar installs at startup -- not by application-layer filtering that the agent could bypass.

Agent (default: deny): outbound connections are blocked except for an explicit allowlist of domains required by the agent (API endpoints, auth, telemetry). The allowlist is resolved to IPs at session startup and installed as iptables ACCEPT rules. DNS queries are rate-limited to 10 requests per second.

Tool containers (default: allow): outbound is open to the public internet, but all RFC 1918 addresses, loopback, link-local, and IPv6 ULA ranges are permanently blocked. Tool containers cannot reach the host, the sidecar, or other containers by internal IP.

Private CIDRs are blocked for both the agent and tool containers regardless of policy. Both defaults are configurable (--agent-policy, --pod-policy), and rules can be adjusted at runtime without restarting the session -- see Runtime network control.

For the full technical reference -- seccomp profile tables, iptables chains, Landlock access sets, capability lists, OCI hook pipeline, and masked path inventory -- see DIAGRAM.md.


Requirements

Requirement Version Notes
Linux kernel >= 6.2 Hard requirement (Landlock V3). Runs natively or in a VM (see below).
Kernel >= 6.12 Recommended. V4 TCP connect (6.7), V5 IoctlDev (6.10), V6 IPC scoping (6.12). V7 audit logging (6.15).
Container runtime any recent rootless podman (preferred), Docker, or nerdctl
Go >= 1.23 Build-time only

Platform support

Platform Runtime Status
Linux (native) podman, docker Full support. All security layers active.
Linux / macOS podman machine Full support. Containers run in a Linux VM with full Landlock.
Linux / macOS docker + colima Full support. Containers run in a Linux VM with full Landlock.
macOS Docker Desktop Not supported. Its fakeowner filesystem breaks Landlock enforcement.

VM backends (podman machine, colima) run containers inside a Linux VM. All security layers (Landlock, seccomp, iptables, OCI hooks) execute inside the VM kernel, identical to native Linux. The VM boundary adds an extra isolation layer — a kernel exploit inside the VM cannot reach the host.

# podman machine (any platform)
podman machine start

# colima + docker (Linux / macOS)
colima start
docker context use colima

Verify Landlock is active (Linux or inside the VM):

cat /sys/kernel/security/lsm   # must contain "landlock"

Install

git clone https://github.com/89luca89/clampdown
cd clampdown
make all      # builds sidecar image, agent images, and launcher binary
make install  # copies binary to ~/.local/bin/clampdown

make all builds four container images (clampdown-sidecar, clampdown-proxy, clampdown-claude, clampdown-opencode) and the clampdown launcher binary. Images are rebuilt only when their source changes (stamp files).


Quick start

Store API keys in ~/.config/clampdown/clampdownrc (created once, used by all sessions):

# ~/.config/clampdown/clampdownrc
ANTHROPIC_API_KEY=sk-ant-...

Per-project overrides go in .clampdownrc at the root of each project. Project values take precedence over the global file:

# /path/to/project/.clampdownrc
ANTHROPIC_API_KEY=sk-ant-...   # project-specific key

Then run:

clampdown claude

# OpenCode (supports multiple providers)
clampdown opencode

# Run against a specific directory (defaults to $PWD)
clampdown claude --workdir /path/to/project

# Pass flags through to the agent
clampdown claude -- --model claude-opus-4-5

Alternatively, pass keys via environment:

ANTHROPIC_API_KEY=sk-ant-... clampdown claude

The first run pulls base images and builds a per-project container storage cache under ~/.cache/clampdown/. Subsequent runs start faster.


Agents

Agent Command Supported provider keys
Claude Code clampdown claude ANTHROPIC_API_KEY
OpenCode clampdown opencode ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, GEMINI_API_KEY, GROQ_API_KEY, DEEPSEEK_API_KEY, MISTRAL_API_KEY, XAI_API_KEY, OPENROUTER_API_KEY, OPENCODE_API_KEY

Keys are passed to the auth proxy, not the agent. The first matching key (from host environment or .clampdownrc) activates the proxy for that provider. The agent receives a dummy key (sk-proxy) and a base URL pointing at the local proxy.


Options

All options can be set on the CLI, in config.json, or (for environment-variable-mapped options) via env.

clampdown [options] <agent> [-- agent-flags...]

Network

Flag Default Env Description
--agent-policy deny SANDBOX_AGENT_POLICY Agent egress default: deny (allowlist only) or allow
--agent-allow -- SANDBOX_AGENT_ALLOW Extra domains for the agent allowlist (comma-separated)
--pod-policy allow SANDBOX_POD_POLICY Tool container egress default: allow or deny

The agent's egress allowlist includes the domains required by the agent (API endpoints, auth, telemetry). Private CIDRs (RFC 1918, loopback, link-local, IPv6 ULA) are always blocked for both agent and tool containers, regardless of policy.

Filesystem

Flag Default Description
--allow-hooks off Allow agent to modify .git/hooks/ (read-only by default)
--protect -- Additional paths to protect read-only (repeatable; trailing / = directory)
--mask -- Additional paths to mask - prevent read (repeatable; trailing / = directory)
--unmask -- Remove paths from the default mask list (e.g. --unmask .env)

Protected paths are always read-only inside the agent and nested containers, regardless of flags: .git/config, .gitmodules, .clampdownrc, .devcontainer, .envrc, .idea, .mcp.json, .vscode. User --protect paths get the same enforcement. Protected paths propagate into nested containers via recursive bind mounts; explicit RW re-mounts are blocked by the security-policy hook.

Masked paths are hidden entirely (replaced with /dev/null): .env, .envrc, .npmrc, .clampdownrc. The agent sees the path exists but reads empty content.

Use --unmask to selectively restore access (e.g. --unmask .env for projects that need it). Persistent via config.json: {"unmask_paths": [".env"]}.

Resources

Flag Default Env Description
--memory 4g SANDBOX_MEMORY Memory limit for agent and tool containers
--cpus 4 SANDBOX_CPUS CPU limit

Credentials

Credentials are opt-in. Nothing is forwarded by default.

Flag Description
--gitconfig Forward ~/.gitconfig read-only into tool containers
--gh Forward ~/.config/gh read-only into tool containers (GitHub CLI auth)
--ssh Forward SSH_AUTH_SOCK into tool containers

Registry

Flag Default Env Description
--registry-auth off SANDBOX_REGISTRY_AUTH Forward host registry credentials to the agent
--require-digest warn SANDBOX_REQUIRE_DIGEST Image digest enforcement: warn or block

Other

Flag Default Description
--runtime auto Container runtime: podman, docker, nerdctl
--tripwire off Kill session on protected path modification (restores on exit either way)
--log-level info debug, info, warn, error

Tripwire monitors protected host paths via inotify and kills the session immediately if any are modified -- the last line of defense against a full container escape. It also snapshots files before launch and restores them on exit (restoration happens regardless of the flag).

Off by default because IDEs, other terminal sessions, and git operations in the same workdir trigger false positives. Enable it for high-security use cases where the workdir is not touched by anything else during the session:

clampdown claude --tripwire

Session management

# List all running sessions
clampdown list

# Stop a session and clean up its containers
clampdown delete -s <session-id>

# View audit events from a session (merged, sorted)
clampdown logs -s <session-id>

# Remove all cached container storage for the current project
clampdown prune

Audit log

Every session produces a structured audit trail for postmortem analysis. All components emit lines prefixed with clampdown: <RFC3339> <source>: so events can be filtered, merged, and sorted chronologically across containers.

What gets logged:

Source Events
session START (agent, workdir, pid), STOP (reason), SIGNAL
security-policy PASS/BLOCKED per nested container (image, command, check count)
seal-inject PASS per nested container (Landlock policy summary)
proxy Every API request (method, path, status, model, sizes, duration)
firewall Rule changes (allow/block/reset with targets and ports)
image Image pushes into the session
launcher Tripwire TAMPER events
# View audit events from a running session (merged, sorted)
clampdown logs -s <session-id>

# Include the full agent conversation (large)
clampdown logs -s <session-id> --dump-agent-conversation

The agent's full conversation (tool calls, responses, errors) is also captured in the container logs. Use --dump-agent-conversation to include it -- the output is cleaned of ANSI escapes and TUI noise, with runtime timestamps preserved for correlation with audit events.

The audit log is persisted to ~/.cache/clampdown/<project>/state/audit-<session>.log when the session ends. This file survives clampdown delete (container removal) -- only clampdown prune removes it.


Runtime network control

Network rules can be adjusted while a session is running:

# Allow the agent to reach a host on a specific port
clampdown network agent allow -s <session-id> example.com --port 443

# Block a host
clampdown network agent block -s <session-id> example.com --port 443

# Remove all dynamic agent rules (returns to startup state)
clampdown network agent reset -s <session-id>

# Same commands for tool containers (pod)
clampdown network pod allow -s <session-id> db.internal --port 5432
clampdown network pod reset -s <session-id>

# Show current rules for a session
clampdown network list -s <session-id>

Targets can be hostnames, IP addresses, or CIDRs. Hostnames are resolved to IPs at the time the rule is applied.


Pushing images into a session

Tool containers pull from the sidecar's isolated registry. To make a local host image available inside the sandbox:

clampdown image push -s <session-id> myimage:latest

Images already present in the sidecar (same image ID) are skipped.


Configuration

config.json

Persistent defaults live in $XDG_CONFIG_HOME/clampdown/config.json (typically ~/.config/clampdown/config.json). Any CLI option can be set here.

{
  "agent_policy": "deny",
  "agent_allow": "registry.mycompany.com",
  "gitconfig": true,
  "gh": true,
  "memory": "8g",
  "cpus": "8",
  "require_digest": "block"
}

.clampdownrc

KEY=VALUE files for API key configuration. Two locations are merged; project overrides global:

  • ~/.config/clampdown/clampdownrc -- global
  • $workdir/.clampdownrc -- per-project
# ~/.config/clampdown/clampdownrc
ANTHROPIC_API_KEY=sk-ant-...

# myproject/.clampdownrc
ANTHROPIC_API_KEY=sk-ant-...   # project-specific key

Keys from .clampdownrc are passed to the auth proxy, not to the agent container. Lines starting with # are comments. Values may be quoted with " or '.


Building from source

make all               # sidecar + proxy + agent images + launcher
make test              # all unit tests (no podman required)
make test-integration  # integration tests (requires podman + internet)
make sidecar           # sidecar image only
make proxy             # auth proxy image only
make claude            # claude agent image only
make opencode          # opencode agent image only
make launcher          # launcher binary only
make install           # install launcher to ~/.local/bin/
make clean             # remove built images and binaries

See CONTRIBUTING.md for contribution guidelines.


Future work

More agents

  • Gemini CLI
  • OpenAI Codex.

License

GNU General Public License v3.0