didericis 713424214e fix(cli): install cleanup_all trap before pipelock_start to avoid orphan networks
Previously cleanup_all was defined AFTER network_create_internal /
network_create_egress / pipelock_start ran, so a failure during
pipelock_start (or in network_create_egress added by the prior commit)
would land in the cleanup_stage trap that knows nothing about networks.
The internal and egress networks would survive the failed launch and
accumulate as orphans on the host.

Move the cleanup_all definition + `trap … EXIT INT TERM` install ahead
of the resource creation, and gate the CONTAINER branch on
`-n "${CONTAINER:-}"` since CONTAINER is set earlier in the function
but the trap now runs in the early-failure window. pipelock_stop and
network_remove are already idempotent against missing resources.

Smoke test: with `CLAUDE_BOTTLE_PIPELOCK_IMAGE` pinned to a nonexistent
digest, `./cli.sh start implementer` now creates both networks, fails
at pipelock_start, and exits with both networks removed —
`docker network ls | grep claude-bottle` returns nothing.

Assisted-by: Claude Code
2026-05-08 01:18:40 -04:00
2026-05-07 22:45:36 -04:00
2026-05-07 22:45:36 -04:00
2026-05-07 22:45:36 -04:00
2026-05-07 22:45:36 -04:00

claude-bottle

Spins up an isolated container for running Claude Code with a curated set of skills and env vars.

Why "claude-bottle"?

Each container is a bottle; Claude is the genie inside. The genie has broad powers within the bottle — read, write, run anything — but it cannot escape to the host. You uncork one bottle per agent (./cli.sh start <agent>), many bottles run in parallel, and each one's powers are scoped to what the manifest grants it: a curated set of skills, env vars, and a starting prompt. When the session ends the bottle is destroyed and the genie does not persist.

Goals

  • Minimize risk of running claude with full permissions
  • Allow me to easily spin up agent tasks in parallel
  • Create isolated, well defined, easily updated, shareable agents

Non-goals

  • Communicating between agents directly
  • Self hosted VMs (v1 uses local Docker containers, not VMs)
  • Advanced agent auditing (lean on git history for auditing)

Quickstart

Requires Docker on the host and a long-lived Claude Code OAuth token in your shell env.

./cli.sh start <agent>   # builds the image on first run, drops you into claude

The container is removed automatically when the session ends. If the script is killed with SIGKILL the exit trap won't fire and the container may be left running; remove it with docker rm -f <container-name>.

Egress

Agent containers route HTTP / HTTPS traffic through a per-agent pipelock sidecar attached to a Docker --internal network. The sidecar enforces a hostname allowlist, runs DLP scanning (48 default credential patterns), and detects URL-embedded high-entropy secret leaks. Without the proxy the agent has no route off-box at all — the internal network has no default gateway. The sidecar and network are torn down with the agent on session exit.

The effective allowlist is the union of a baked-in default for Claude Code's required hosts (api.anthropic.com, claude.ai, ...) and the optional bottles.<name>.egress.allowlist field in claude-bottle.json:

{
  "bottles": {
    "default": {
      "env":    { },
      "ssh":    [ ],
      "egress": { "allowlist": ["github.com"] }
    }
  }
}

The resolved allowlist is shown in the y/N preflight before launch. See docs/prds/0001-per-agent-egress-proxy-via-pipelock.md for the design and docs/research/pipelock-assessment.md for the rationale.

Auth: OAuth token, not API key

claude-bottle authenticates claude inside the container with the same Pro/Max subscription you already use on the host, via a long-lived OAuth token. No ANTHROPIC_API_KEY is needed.

Why a token instead of mounting ~/.claude.json: on macOS, Claude Code stores OAuth credentials in the encrypted Keychain, not in ~/.claude.json. Mounting that file into a Linux container does not carry the credentials with it. Linux hosts keep credentials in ~/.claude/.credentials.json, but to keep the launcher portable claude-bottle uses the env-var path on every host.

One-time setup on the host:

claude setup-token   # browser login, prints a ~1-year OAuth token

Stash the token in your shell env (e.g. ~/.zshrc or a secret manager) as CLAUDE_BOTTLE_OAUTH_TOKEN:

export CLAUDE_BOTTLE_OAUTH_TOKEN="<token>"

cli.sh automatically forwards it to every container as CLAUDE_CODE_OAUTH_TOKEN via docker run -e — no manifest wiring required, and the value is never written to disk or placed on argv.

Inside the container, claude picks up CLAUDE_CODE_OAUTH_TOKEN and authenticates against your subscription. Caveats: the token is bound to your subscription tier (Pro/Max/Team/Enterprise), it does not work with claude --bare (which only reads ANTHROPIC_API_KEY), and if it leaks, regenerate via claude setup-token again. Reference: https://code.claude.com/docs/en/authentication.

S
Description
Lightweight, self-hosted sandbox for AI coding agents that protects against prompt-injected or misbehaving agents: all egress traffic is TLS-inspected and secret-scanned, and credentials are injected at the proxy so the agent never sees them. No third-party platform in the loop, no trust required.
Readme Apache-2.0 30 MiB
Languages
Python 99.1%
Shell 0.5%
Dockerfile 0.4%