Skip to content

Security: cosyte/claude-containers

SECURITY.md

Security

Reporting a vulnerability

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.

What this project's security model actually claims

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 plain runc. 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, MKNOD and SETFCAP are removed.
  • Credential isolation. The shared claude-auth credential master is root-owned; the agent reaches only its own per-container session token. By default the git deploy key lives in a root-owned ssh-agent and the agent gets a signing socket only, so it cannot read the private key bytes; only an explicit CLAUDE_BROKER_GIT_KEY=0 opts 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-launch and claude-compose-gen refuse a GIT_SSH_KEY whose public half is in SSH_AUTHORIZED_KEYS, and name any other container on the host that accepts this git key (or pushes with a key this SSH_AUTHORIZED_KEYS accepts). In the container, sshd refuses the git key whatever any authorized_keys file says: the entrypoint lists it in sshd's root-owned RevokedKeys file 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 the 1/true/yes/on spellings 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=strict is 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. Under strict, a path that reaches a running agent session in a container whose ruleset was not applied IS in scope. strict still contains only as well as the ruleset underneath it: a container whose IPv4 lockdown applied and whose IPv6 pass did not starts, and says IPv6 UNRESTRICTED in 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-launch and claude-compose-gen mount it into that container only, and claude-compose-gen --mount refuses another container's state or config volume. A generated or launched configuration that gives one container another's /state is in scope. The shared /cache is 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 /cache is 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-verify bypasses it by design.

Known and accepted, so not vulnerabilities in this project:

  • ANTHROPIC_API_KEY is refused: the entrypoint hard-fails if it is set. That is deliberate billing protection, not a bug.
  • Anyone who can read the claude-auth Docker volume can act as you. It holds live OAuth credentials. Protect it with host permissions.
  • The SSH port publishes on 0.0.0.0 by default, so it is LAN-reachable. Auth is pubkey-only; set CLAUDE_SSH_BIND=127.0.0.1 to 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.com is 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.

Supported versions

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.

There aren't any published security advisories