Report privately: do not open a public issue.
Use GitHub private vulnerability reporting
(Security → Report a vulnerability), that is the channel that reaches the
maintainer directly. If it is unavailable to you, email hello@cosyte.com with
SECURITY: claude-containers in the subject.
Please include the version or commit, your host OS and Docker/runc versions, the flags or env vars in play, and the smallest reproduction you can manage. A first response should come within a few working days; this is a small project, so please allow for that.
Report to the right party: a flaw in Claude Code itself, in the Claude mobile app, or in the Anthropic API belongs to Anthropic, not here. This repo is the container that runs Claude Code, not Claude Code.
Read this before filing: several plausible-sounding reports are documented non-goals rather than defects.
A container is not a security boundary against a fully weaponized agent.
This is the load-bearing statement. The stack runs an autonomous agent that
executes shell commands, often with --dangerously-skip-permissions. Everything
below shrinks the blast radius of a prompt-injected or misbehaving agent. None
of it is a claim that a determined attacker who achieves code execution inside
the container cannot get further. The single highest-leverage control is a
patched host runtime: the launcher warns when host runC is older than
1.2.8 / 1.3.3 (CVE-2025-31133 / -52565 / -52881).
What the stack does enforce, and therefore what a bypass of is a valid report:
- No
--privileged, and no host Docker socket mount, ever. Every session runs on plainrunc. A path that obtains host root is in scope. - Capability floor.
--security-opt no-new-privileges, plus (default) dropping all Linux capabilities and re-adding only the minimal set for sshd and privilege-dropping:NET_RAW,MKNODandSETFCAPare removed. - Credential isolation. The shared
claude-authcredential master is root-owned; the agent reaches only its own per-container session token. By default the git deploy key lives in a root-ownedssh-agentand the agent gets a signing socket only, so it cannot read the private key bytes; only an explicitCLAUDE_BROKER_GIT_KEY=0opts out to a readable key file, and a broker that fails to come up installs no key file rather than falling back. The mounted key sits in/etc/claude, a root-owned mode-700 directory, so the agent cannot open it off the mount even when the host file's owner is the agent's own UID; a brokered key the agent could still read stops the boot. A path by which the unprivileged agent reads the master credential or the raw private key is in scope. - The git key is never an SSH login key. Every session can sign with the git
key through its agent, so a container that accepted it for SSH logins could be
entered from every session.
claude-launchandclaude-compose-genrefuse aGIT_SSH_KEYwhose public half is inSSH_AUTHORIZED_KEYS, and name any other container on the host that accepts this git key (or pushes with a key thisSSH_AUTHORIZED_KEYSaccepts). In the container, sshd refuses the git key whatever anyauthorized_keysfile says: the entrypoint lists it in sshd's root-ownedRevokedKeysfile at every boot. A path by which a session logs in over SSH, to its own container or another, with the key in its agent is in scope. - Egress lockdown (
CLAUDE_EGRESS_LOCKDOWN=1) is enforced in netfilter, default-deny, IP-pinned, applied at boot before the agent starts and while it is still unprivileged, so the agent cannot disable its own rules. A path that does is in scope. Note that on the1/true/yes/onspellings it fails open by design: if setup fails it logs loudly and leaves egress unrestricted rather than bricking connectivity, so on those spellings lockdown is a request, not a guarantee, and a report that it did not apply is not a vulnerability.CLAUDE_EGRESS_LOCKDOWN=strictis the fail-closed spelling: the same firewall, but a ruleset that cannot be applied makes the container refuse to start the agent and exit nonzero. Understrict, a path that reaches a running agent session in a container whose ruleset was not applied IS in scope.strictstill contains only as well as the ruleset underneath it: a container whose IPv4 lockdown applied and whose IPv6 pass did not starts, and saysIPv6 UNRESTRICTEDin its boot log. - Allowlist refresh (
CLAUDE_EGRESS_REFRESH_INTERVAL=<seconds>) re-resolves and re-commits the same allowlist on an interval, as root, with the same privilege ownership as the boot pass. A path by which the unprivileged agent alters what a refresh commits, stops a refresh, or influences what it resolves (the state it reads is root-owned and mode 700) is in scope. Note that a refresh retains rather than narrows, always: a lookup that comes back empty, a restore that will not commit and a failed fetch of a published range all leave the ruleset already in force exactly as it is, and are logged. A report that a refresh failed to tighten an allowlist is therefore not a vulnerability; narrowing a live container's egress on a transient DNS failure would be the worse outcome and is the behaviour being deliberately avoided. - Private state per container. Each container gets its own
claude-state-<name>volume at/state(CLAUDE_STATE_DIR) for an app's databases, keys and tokens;claude-launchandclaude-compose-genmount it into that container only, andclaude-compose-gen --mountrefuses another container's state or config volume. A generated or launched configuration that gives one container another's/stateis in scope. The shared/cacheis the opposite by design: every container mounts it and runs as the same UID, so anything in it is readable from every container. Private state left there is named in the boot log, not hidden; a report that/cacheis readable across containers is not a vulnerability. - Secret guard installs a git pre-commit hook blocking obvious secret
material. It is a backstop against an agent committing a credential, not a
DLP control;
--no-verifybypasses it by design.
Known and accepted, so not vulnerabilities in this project:
ANTHROPIC_API_KEYis refused: the entrypoint hard-fails if it is set. That is deliberate billing protection, not a bug.- Anyone who can read the
claude-authDocker volume can act as you. It holds live OAuth credentials. Protect it with host permissions. - The SSH port publishes on
0.0.0.0by default, so it is LAN-reachable. Auth is pubkey-only; setCLAUDE_SSH_BIND=127.0.0.1to limit it. - All containers share one SSH host key (stable fingerprint), acceptable for a single-owner host: the private host keys are root-only in every container, so only root in one container could pose as another. Use distinct keys if that matters to you.
- Egress is open by default. Lockdown is opt-in.
- An IP-pinned allowlist goes stale as CDNs rotate addresses, and
statsig.anthropic.comis not publicly resolvable so it cannot be pinned.
The full threat model: including the weaponized-agent supply-chain exfil case the package-provisioning tier is built against: is in docs/package-provisioning-security.md.
There are no releases. main is the supported version; fixes land there. The
legacy/sysbox-broker-2026-07-12 branch is a frozen historical artifact,
retained for reference and not maintained or patched: see
docs/legacy-sysbox-broker.md.