clampdown

agent
SUMMARY

Run AI coding agents in hardened container sandboxes.

README.md

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 four container types with different privilege levels:

Sidecar Auth Proxy Agent Tool (nested)
Purpose Container runtime + firewall API key injection AI agent process Tools the agent spawns
Base image FROM scratch (no shell, no libc) FROM scratch Alpine User-chosen
Capabilities 16 (SYS_ADMIN, NET_ADMIN, ...) 0 (cap-drop=ALL) 0 (cap-drop=ALL) 10 default (effective set empty)
Seccomp ~85 blocked ~133 blocked ~133 blocked ~133 blocked + inherited sidecar
Landlock No (incompatible with mount()) RO rootfs, TCP 443+53 only workdir RW, rootfs RO Derived from bind mounts
Secrets Registry credentials (opt-in) Real API keys None (sk-proxy dummy key) None
Network egress N/A TCP 443, 53 (Landlock) Deny + allowlist only Allow, private CIDRs blocked
Rootfs Read-only Read-only Read-only Read-only
Runs as root (userns-mapped) Non-root Non-root Non-root (hook-enforced)
SELinux container_engine_t container_t container_t container_t
AppArmor unconfined confined confined confined
Core dumps Allowed Disabled (ulimit core=0:0) Disabled Allowed
  • 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
    20 syscalls in real time -- blocking unmounts of masked paths, execution of unknown
    binaries, deletion of protected files, and unauthorized firewall modification --
    closing the gap where Landlock cannot be applied.
  • The auth proxy holds the only copy of real API keys. It runs in a FROM scratch
    container with no shell, no writable filesystem, no capabilities, and core dumps
    disabled. The agent gets a dummy key (sk-proxy) and a base URL pointing at the
    proxy. Even direct connections to the upstream API produce 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 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 20 syscalls covering mount operations, exec verification, protected-path
file operations, process targeting, and firewall modification. A supervisor goroutine
evaluates each call against policy and either continues the syscall or blocks it.

This blocks attempts to unmount masked paths (exposing .env contents), remount
protected paths read-write, create a new procfs to access /proc/1/mem, execute
binaries not present on the rootfs at startup (SHA-256 hash-verified allowlist),
delete or replace protected files, modify iptables rules, or use ptrace to read
the supervisor's memory. Legitimate operations (podman creating containers, netavark
configuring bridge rules) pass through unimpeded.

The exec allowlist walks the rootfs at startup and SHA-256 hashes every
executable. On each execve/execveat from sidecar PID namespace processes, the
supervisor verifies the binary against the startup snapshot. Unknown binaries are
blocked with EACCES. Nested container processes (different PID namespace) skip the
check -- their exec is governed by Landlock and the workload seccomp profile.

The firewall lock blocks netfilter socket creation and iptables rule replacement
except from the netavark->xtables-nft-multi chain (podman's network manager). Since
podman exec from the host enters via setns (not fork), it does not inherit the
filter -- legitimate firewall changes from the launcher are exempt.

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+.

API key isolation

The agent never receives real API credentials. A separate auth proxy container
holds the real keys and injects them into upstream requests.

Agent (sk-proxy) ──> Auth Proxy (real key) ──> Upstream API

The proxy runs in its own FROM scratch container: no shell, no writable
filesystem, no capabilities (cap-drop=ALL), core dumps disabled, Landlock
restricting TCP to ports 443 and 53 only. Even if the agent connects to the
upstream API directly (port 443 is allowed for infrastructure domains), it
sends the dummy key sk-proxy and gets a 401.

Keys are resolved from two sources -- the host environment and .clampdownrc.
Neither is forwarded into the agent container. The proxy logs every API request
(method, path, status, model, sizes, duration) for the audit trail.

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

All containers run detached. clampdown claude starts a session and attaches
to it. Detach with ctrl+] — the session keeps running in the background.
All containers use --restart=unless-stopped: if the agent crashes, it
restarts automatically. Only clampdown stop stops a session.

# Start a session and attach (default workflow)
clampdown claude

# Detach: press ctrl+]
# The session keeps running. The launcher exits.

# List all sessions (running and stopped)
clampdown list

# Reattach to a running session
clampdown attach -s <session-id>

# Stop a running session (all containers)
clampdown stop -s <session-id>

# Remove a stopped session's containers and temp files
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

Reviews (0)

No results found