Files
bot-bottle/README.md
T
didericis acbaffb98e
test / run tests/run_tests.py (push) Successful in 15s
docs: add Apache 2.0 LICENSE and link it from the README
Drops the canonical Apache 2.0 text at repo root and adds a License
section to the README with the copyright line. Apache 2.0 picked over
MIT for the patent grant and contributor language, both of which
matter for a security-adjacent tool aiming at corporate-evaluable
adoption. Also tidies the manifest example's GIT_AUTHOR_NAME to use
the project's online handle.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 02:02:42 -04:00

6.4 KiB

claude-bottle logo

claude-bottle

test

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

Security model

Each agent runs in its own bottle: its own container, its own internal Docker network, and its own pipelock sidecar. Bottles don't share state, don't talk to each other, and only get the env vars, skills, SSH identities, and egress hosts the manifest grants them — nothing more. Any one agent only has the access it needs to do its job.

The container is the boundary against an uncoordinated agent reaching the host: a misbehaving Claude Code session can't read files outside the bottle, can't reach the host's network without going through pipelock, and can't see other bottles. By default it is not a hardened boundary against a determined attacker with kernel-level escape capability — Linux hosts can opt into gVisor per bottle (see runtime in the manifest below); the broader v2 discussion lives in docs/research/stronger-isolation-alternatives.md.

The egress proxy and OAuth-token handling below are the load-bearing pieces of v1.

Quickstart

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

./cli.py 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>.

Manifest

Agents and the bottles they run in are declared in claude-bottle.json in your project root or $HOME (both files merge if present, with project entries overriding home entries on key conflict).

{
  "bottles": {
    "gitea-dev": {
      // Container runtime for the agent. Default "runc"; set to
      // "runsc" on Linux hosts to launch the agent under gVisor for
      // a userspace syscall barrier between the agent and the host
      // kernel. claude-bottle verifies the runtime is registered with
      // Docker before launch; gVisor is not available on macOS.
      "runtime": "runsc",

      "env": {
        "GITEA_TOKEN":     "?paste your Gitea API token",
        "GITHUB_TOKEN":    "${GH_PAT}",
        "GIT_AUTHOR_NAME": "didericis"
      },

      "ssh": [
        {
          "Host":         "gitea",
          "Hostname":     "gitea.dideric.is",
          "User":         "git",
          "Port":         30009,
          "IdentityFile": "/Users/didericis/.ssh/id_ed25519_gitea",
          "KnownHostKey": "gitea.dideric.is ssh-ed25519 AAAA..."
        }
      ],

      // Egress is forced through a per-agent
      // [pipelock](https://github.com/luckyPipewrench/pipelock) sidecar
      // on a Docker `--internal` network — without the proxy the agent
      // has no route off-box. The effective allowlist is the union of
      // baked-in defaults (api.anthropic.com, claude.ai, ...) and the
      // hostnames listed here. Pipelock also runs DLP scanning and
      // detects URL-embedded high-entropy secrets. The resolved
      // allowlist is shown in the y/N preflight before launch.
      "egress": {
        "allowlist": [
          "github.com",
          "registry.npmjs.org",
          "pypi.org"
        ]
      }
    }
  },

  "agents": {
    "gitea-helper": {
      "bottle": "gitea-dev",
      "skills": ["init-prd"],
      "prompt": "You help maintain Gitea-hosted projects."
    }
  }
}

Comments are illustrative; the file itself must be valid JSON. See claude-bottle.example.json for a working starting point. Pipelock's design lives in docs/prds/0001-per-agent-egress-proxy-via-pipelock.md and the rationale in docs/research/pipelock-assessment.md.

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

Trademarks

claude-bottle is an independent project and is not affiliated with, endorsed by, or sponsored by Anthropic, PBC. "Claude" and "Claude Code" are trademarks of Anthropic, PBC; the project name uses "claude" descriptively to indicate that the tool runs Claude Code inside a sandbox.

License

Copyright 2026 Eric Bauerfeld

Licensed under the Apache License, Version 2.0. See LICENSE for the full text.