c8a35beb12
test / run tests/run_tests.py (push) Successful in 15s
A short apothecary-bottle SVG with a cream cartoon robot inside — sized roughly to the robot so it works as a favicon-shaped icon. README gains a centered logo above the title and a Trademarks section disclaiming affiliation with Anthropic and framing the "claude" in the project name as descriptive use. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
151 lines
5.5 KiB
Markdown
151 lines
5.5 KiB
Markdown
<p align="center">
|
|
<img src="docs/logo.svg" alt="claude-bottle logo" width="140">
|
|
</p>
|
|
|
|
# claude-bottle
|
|
|
|
[](https://gitea.dideric.is/didericis/claude-bottle/actions?workflow=test.yml)
|
|
|
|
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 — see `docs/research/stronger-isolation-alternatives.md`
|
|
for the broader v2 discussion.
|
|
|
|
Linux hosts can opt into [gVisor](https://gvisor.dev/) per bottle for
|
|
a userspace syscall barrier between the agent and the host kernel:
|
|
|
|
```jsonc
|
|
{
|
|
"bottles": {
|
|
"default": { "runtime": "runsc" }
|
|
}
|
|
}
|
|
```
|
|
|
|
When `runtime` is set to `"runsc"`, claude-bottle verifies the runtime
|
|
is registered with Docker before launch and passes `--runtime=runsc`
|
|
to the agent container. Default is `"runc"` (Docker's default). gVisor
|
|
is not available on macOS.
|
|
|
|
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.
|
|
|
|
```sh
|
|
./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>`.
|
|
|
|
## Egress
|
|
|
|
Agent containers route HTTP / HTTPS traffic through a per-agent
|
|
[pipelock](https://github.com/luckyPipewrench/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`:
|
|
|
|
```jsonc
|
|
{
|
|
"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:**
|
|
|
|
```sh
|
|
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`:
|
|
|
|
```sh
|
|
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.
|