Add a Gitea Actions test-status badge plus a short README "CI" section covering how to read the check and what to do when it's red. Capture the (out-of-tree) branch-protection rule on `main` in docs/ci.md so the gate that requires the test check is reproducible from the repo alone — covers both the Gitea UI path and the equivalent API call. Refs: PRD 0002 Assisted-by: Claude Code
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>.
CI
Every push to a PR (and every push to main) runs the full test suite
on Gitea Actions via .gitea/workflows/test.yml. The status badge at
the top of this README links to the workflow's run history; the same
check appears on each PR.
Reading the check:
- Green —
tests/run_tests.pyexited 0 on the runner. - Red — at least one test failed (or the workflow itself errored).
Click through to the run, expand the failing step, and read the
unittest output. Reproduce locally with
tests/run_tests.py(ortests/run_tests.py unitif you don't have Docker handy); integration tests skip cleanly when Docker isn't reachable. - Skipped tests — expected when the runner has no Docker daemon. They still leave the job green; if you actually want them executed, ensure Docker is available on the runner host.
Pushing a fix to a red PR re-triggers the workflow automatically — no
manual rerun needed. Branch protection on main requires this check
to be green before the merge button is enabled; see docs/ci.md
for how those rules are configured.
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.