docs: replace stale .sh paths with claude_bottle/*.py equivalents
test / run tests/run_tests.py (push) Successful in 13s

Cleans up references to the pre-refactor bash layout (cli.sh,
lib/*.sh, scripts/*.sh) across README, Dockerfile, the pipelock PRD,
and research notes. Refreshes line numbers in the oauth-token note
against the current cli/start.py.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 00:27:25 -04:00
parent 4ebfcec2f7
commit cc5e772519
7 changed files with 48 additions and 45 deletions
+3 -3
View File
@@ -17,7 +17,7 @@ FROM node:22-slim
# image, those features fail in surprising ways once the user does any # image, those features fail in surprising ways once the user does any
# real work. ca-certificates is already in the slim base; listed for # real work. ca-certificates is already in the slim base; listed for
# clarity in case the base ever drops it. socat is the privileged # clarity in case the base ever drops it. socat is the privileged
# forwarder for the in-container ssh-agent (see lib/ssh.sh): the agent # forwarder for the in-container ssh-agent (see claude_bottle/ssh.py): the agent
# runs as root and rejects non-root connections, so socat sits between # runs as root and rejects non-root connections, so socat sits between
# node and the agent socket. # node and the agent socket.
RUN apt-get update \ RUN apt-get update \
@@ -38,7 +38,7 @@ USER node
WORKDIR /home/node WORKDIR /home/node
# Pre-create the skills directory so PRD 0002's host->container skill # Pre-create the skills directory so PRD 0002's host->container skill
# copier (scripts/lib/skills.sh) drops files into a path owned by the # copier (claude_bottle/skills.py) drops files into a path owned by the
# `node` user. `skills_copy_into` also `mkdir -p`s defensively, but # `node` user. `skills_copy_into` also `mkdir -p`s defensively, but
# baking it into the image avoids a permission-confusion footgun if a # baking it into the image avoids a permission-confusion footgun if a
# future change to the launcher copies in as a different user. # future change to the launcher copies in as a different user.
@@ -58,7 +58,7 @@ RUN cat > "$HOME/.claude.json" <<JSON
JSON JSON
# Default to an interactive claude session. In the v1 launcher, # Default to an interactive claude session. In the v1 launcher,
# `scripts/start.sh` runs the container detached and uses `docker exec` # `claude_bottle/cli/start.py` runs the container detached and uses `docker exec`
# to attach a TTY, but this CMD makes `docker run -it claude-bottle` also # to attach a TTY, but this CMD makes `docker run -it claude-bottle` also
# do something useful for ad-hoc debugging. # do something useful for ad-hoc debugging.
CMD ["claude"] CMD ["claude"]
+2 -2
View File
@@ -9,7 +9,7 @@ Spins up an isolated container for running Claude Code with a curated set of ski
Each container is a bottle; Claude is the genie inside. The genie has Each container is a bottle; Claude is the genie inside. The genie has
broad powers within the bottle — read, write, run anything — but it broad powers within the bottle — read, write, run anything — but it
cannot escape to the host. You uncork one bottle per agent cannot escape to the host. You uncork one bottle per agent
(`./cli.sh start <agent>`), many bottles run in parallel, and each (`./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 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 of skills, env vars, and a starting prompt. When the session ends the
bottle is destroyed and the genie does not persist. bottle is destroyed and the genie does not persist.
@@ -97,7 +97,7 @@ as `CLAUDE_BOTTLE_OAUTH_TOKEN`:
export CLAUDE_BOTTLE_OAUTH_TOKEN="<token>" export CLAUDE_BOTTLE_OAUTH_TOKEN="<token>"
``` ```
`cli.sh` automatically forwards it to every container as `cli.py` automatically forwards it to every container as
`CLAUDE_CODE_OAUTH_TOKEN` via `docker run -e` — no manifest wiring `CLAUDE_CODE_OAUTH_TOKEN` via `docker run -e` — no manifest wiring
required, and the value is never written to disk or placed on argv. required, and the value is never written to disk or placed on argv.
@@ -43,12 +43,12 @@ The feature works when all of the following are observable:
The feature is **done** when all of the following ship: The feature is **done** when all of the following ship:
- `cli.sh start` brings up a per-agent pipelock sidecar on a `--internal` - `cli.py start` brings up a per-agent pipelock sidecar on a `--internal`
Docker network and points the agent's `HTTPS_PROXY` at it. Docker network and points the agent's `HTTPS_PROXY` at it.
- A per-agent pipelock YAML config is generated from a bottle-level - A per-agent pipelock YAML config is generated from a bottle-level
`egress.allowlist` field, plus baked-in defaults for Claude Code's `egress.allowlist` field, plus baked-in defaults for Claude Code's
required hosts so basic bottles work out of the box. required hosts so basic bottles work out of the box.
- The existing `cli.sh` y/N preflight shows the resolved allowlist before - The existing `cli.py` y/N preflight shows the resolved allowlist before
launch. launch.
- When the agent container exits, the pipelock sidecar and the internal - When the agent container exits, the pipelock sidecar and the internal
network are torn down cleanly (no orphaned containers or networks). network are torn down cleanly (no orphaned containers or networks).
@@ -76,7 +76,7 @@ The feature is **done** when all of the following ship:
- Pipelock sidecar container lifecycle (start, attach to network, - Pipelock sidecar container lifecycle (start, attach to network,
receive config, stop on agent exit). receive config, stop on agent exit).
- `HTTPS_PROXY` / `HTTP_PROXY` injection into the agent container. - `HTTPS_PROXY` / `HTTP_PROXY` injection into the agent container.
- Preflight integration: the existing y/N plan in `cli.sh` lists the - Preflight integration: the existing y/N plan in `cli.py` lists the
resolved allowlist. resolved allowlist.
### Out of scope ### Out of scope
@@ -95,30 +95,31 @@ The feature is **done** when all of the following ship:
### New services / components ### New services / components
Two new files under `lib/`: Two new modules under `claude_bottle/`:
- **`lib/pipelock.sh`** — pipelock-specific logic. Generates the - **`claude_bottle/pipelock.py`** — pipelock-specific logic. Generates
per-bottle YAML config from the manifest's `egress` block plus baked-in the per-bottle YAML config from the manifest's `egress` block plus
defaults; copies the YAML into the sidecar via `docker cp`; starts and baked-in defaults; copies the YAML into the sidecar via `docker cp`;
stops the sidecar container; resolves the allowlist for display in the starts and stops the sidecar container; resolves the allowlist for
preflight. display in the preflight.
- **`lib/network.sh`** — Docker network plumbing. Creates the per-agent - **`claude_bottle/network.py`** — Docker network plumbing. Creates the
`--internal` network (named `claude-bottle-net-<slug>` with the same per-agent `--internal` network (named `claude-bottle-net-<slug>` with
slug-and-suffix scheme used for container names), attaches the agent the same slug-and-suffix scheme used for container names), attaches
and sidecar to it, removes it on teardown. Kept separate from the agent and sidecar to it, removes it on teardown. Kept separate
`lib/docker.sh` so a future PRD can add non-pipelock network controls from `claude_bottle/docker.py` so a future PRD can add non-pipelock
without entangling them with pipelock specifics. network controls without entangling them with pipelock specifics.
This split mirrors the existing per-concern lib/ pattern This split mirrors the existing per-concern module pattern
(`manifest.sh`, `env_resolve.sh`, `skills.sh`, `ssh.sh`). (`manifest.py`, `env_resolve.py`, `skills.py`, `ssh.py`).
### Existing code touched ### Existing code touched
- **`cli.sh`** — wire the new lifecycle into `start`: create the - **`claude_bottle/cli/start.py`** — wire the new lifecycle into the
internal network, launch the pipelock sidecar, then launch the agent `start` subcommand: create the internal network, launch the pipelock
container with `HTTPS_PROXY` / `HTTP_PROXY` set to the sidecar's sidecar, then launch the agent container with `HTTPS_PROXY` /
service name. Add the resolved allowlist to the preflight y/N output. `HTTP_PROXY` set to the sidecar's service name. Add the resolved
Tear down sidecar + network in the existing exit trap. allowlist to the preflight y/N output. Tear down sidecar + network in
the existing exit handler.
- **`README.md`** — public-facing description should mention that - **`README.md`** — public-facing description should mention that
agent containers route HTTP egress through pipelock by default, and agent containers route HTTP egress through pipelock by default, and
document the new `egress.allowlist` bottle field. document the new `egress.allowlist` bottle field.
@@ -128,9 +129,10 @@ This split mirrors the existing per-concern lib/ pattern
the image. This keeps the image agnostic to whether a sidecar is in use the image. This keeps the image agnostic to whether a sidecar is in use
(useful if a future bottle definition opts out of the proxy for testing). (useful if a future bottle definition opts out of the proxy for testing).
`lib/docker.sh` may grow one or two helpers if there is a clean place `claude_bottle/docker.py` may grow one or two helpers if there is a
for shared primitives, but the network-specific helpers live in clean place for shared primitives, but the network-specific helpers
`lib/network.sh`. Decide during implementation; not a contract. live in `claude_bottle/network.py`. Decide during implementation; not a
contract.
### Data model changes ### Data model changes
@@ -174,9 +176,9 @@ bottle share the same allowlist.
- **Pipelock binary** is pulled from - **Pipelock binary** is pulled from
`ghcr.io/luckypipewrench/pipelock@sha256:<digest>`. The digest is `ghcr.io/luckypipewrench/pipelock@sha256:<digest>`. The digest is
pinned in `lib/pipelock.sh` (or a sibling `.env`-shaped constants pinned in `claude_bottle/pipelock.py` (or a sibling constants module)
file) and bumped deliberately, mirroring the claude-code version and bumped deliberately, mirroring the claude-code version pinning
pinning pattern in `Dockerfile`. pattern in `Dockerfile`.
- No new host-side runtimes. The pipelock image is the only new - No new host-side runtimes. The pipelock image is the only new
external artifact. external artifact.
@@ -189,10 +191,10 @@ bottle share the same allowlist.
which features used by this PRD are Apache-2.0-core. v1's plan which features used by this PRD are Apache-2.0-core. v1's plan
(proxy + 48 default DLP patterns + subdomain entropy + sidecar (proxy + 48 default DLP patterns + subdomain entropy + sidecar
topology) is expected to be core-only, but this should be confirmed. topology) is expected to be core-only, but this should be confirmed.
- **Where to put the digest pin.** A constant in `lib/pipelock.sh` is - **Where to put the digest pin.** A constant in
the lowest-friction option; a separate `lib/versions.sh` (or similar) `claude_bottle/pipelock.py` is the lowest-friction option; a separate
may be cleaner once there are multiple pinned dependencies. Decide `claude_bottle/versions.py` (or similar) may be cleaner once there
during implementation. are multiple pinned dependencies. Decide during implementation.
- **Per-agent overrides.** The PRD scopes egress to the bottle. If a - **Per-agent overrides.** The PRD scopes egress to the bottle. If a
later use case calls for tightening (not loosening) the allowlist for later use case calls for tightening (not loosening) the allowlist for
one agent within a bottle, revisit. Out of scope for v1. one agent within a bottle, revisit. Out of scope for v1.
@@ -34,7 +34,7 @@ Three pieces in combination give a 100% guarantee:
- `get_status(job_id)` — check running/done - `get_status(job_id)` — check running/done
- `get_output(job_id)` — read results - `get_output(job_id)` — read results
3. **Non-interactive container run mode**`cli.sh run <agent> "<task>"` passes the task to `claude --print` inside the container and captures output. Currently `cli.sh start` is interactive only; this mode does not yet exist. 3. **Non-interactive container run mode**`cli.py run <agent> "<task>"` passes the task to `claude --print` inside the container and captures output. Currently `cli.py start` is interactive only; this mode does not yet exist.
## Proposal ## Proposal
@@ -42,7 +42,7 @@ Build host-dispatch-to-container in two deliverables:
**Deliverable 1: Non-interactive run mode for claude-bottle** **Deliverable 1: Non-interactive run mode for claude-bottle**
Extend `cli.sh` with a `run <agent> <task>` subcommand. Starts the container, writes the task prompt to a file inside it (same `docker cp` pattern used for `--append-system-prompt-file`), invokes `claude --print` with the prompt, streams stdout back to the host, and exits when Claude finishes. Results committed and pushed from inside the container as usual. Extend `cli.py` with a `run <agent> <task>` subcommand. Starts the container, writes the task prompt to a file inside it (same `docker cp` pattern used for `--append-system-prompt-file`), invokes `claude --print` with the prompt, streams stdout back to the host, and exits when Claude finishes. Results committed and pushed from inside the container as usual.
**Deliverable 2: MCP server wrapping claude-bottle** **Deliverable 2: MCP server wrapping claude-bottle**
+1 -1
View File
@@ -234,7 +234,7 @@ which does not work on macOS Desktop.
Moderate. The script itself is well-understood and can be lifted nearly Moderate. The script itself is well-understood and can be lifted nearly
verbatim from Anthropic's devcontainer repo. The integration points in verbatim from Anthropic's devcontainer repo. The integration points in
`cli.sh` are: `cli.py` are:
1. Pass `--cap-add NET_ADMIN --cap-add NET_RAW` in the `docker run` invocation. 1. Pass `--cap-add NET_ADMIN --cap-add NET_RAW` in the `docker run` invocation.
2. `docker cp` an `init-firewall.sh` script into the container (alongside 2. `docker cp` an `init-firewall.sh` script into the container (alongside
@@ -20,11 +20,12 @@ that does not route through `ANTHROPIC_BASE_URL` at all.
## How the token reaches claude today ## How the token reaches claude today
1. `cli.sh:526528` — host's `CLAUDE_BOTTLE_OAUTH_TOKEN` is exported into 1. `claude_bottle/cli/start.py` (around line 237238) — host's
the launcher process as `CLAUDE_CODE_OAUTH_TOKEN`, then forwarded with `CLAUDE_BOTTLE_OAUTH_TOKEN` is exported into the launcher process as
`CLAUDE_CODE_OAUTH_TOKEN`, then forwarded with
`docker run -e CLAUDE_CODE_OAUTH_TOKEN` (no `=value`, so the value `docker run -e CLAUDE_CODE_OAUTH_TOKEN` (no `=value`, so the value
never lands on argv — good). never lands on argv — good).
2. `cli.sh:603605` — claude is launched via 2. `claude_bottle/cli/start.py` (around line 318325) — claude is launched via
`docker exec -it <container> claude …`, which inherits the container `docker exec -it <container> claude …`, which inherits the container
PID 1's env, including the token. PID 1's env, including the token.
3. claude runs as `node` (UID 1000) with `--dangerously-skip-permissions`. 3. claude runs as `node` (UID 1000) with `--dangerously-skip-permissions`.
+2 -2
View File
@@ -273,7 +273,7 @@ reach, and avoids the `--best-effort` issue on macOS Docker Desktop.
The claude-bottle manifest model would need one new piece of plumbing: a The claude-bottle manifest model would need one new piece of plumbing: a
per-agent pipelock ACL YAML generated from the manifest's `allowlist` per-agent pipelock ACL YAML generated from the manifest's `allowlist`
and `ssh` entries, analogous to what the smokescreen section of and `ssh` entries, analogous to what the smokescreen section of
`network-egress-guard.md` already sketches. The `cli.sh` changes required `network-egress-guard.md` already sketches. The `cli.py` changes required
are the same pattern: `docker network create --internal`, `docker run` for are the same pattern: `docker network create --internal`, `docker run` for
the proxy container, then `docker run` for the agent with `HTTPS_PROXY` the proxy container, then `docker run` for the agent with `HTTPS_PROXY`
injected. injected.
@@ -408,7 +408,7 @@ The reasoning:
subdomain-entropy DNS exfil detection, MCP scanning, and request subdomain-entropy DNS exfil detection, MCP scanning, and request
redaction. The integration shape for claude-bottle is identical: a redaction. The integration shape for claude-bottle is identical: a
separate container on an internal Docker network, with the agent's separate container on an internal Docker network, with the agent's
`HTTPS_PROXY` pointing at it. The `cli.sh` changes are the same pattern. `HTTPS_PROXY` pointing at it. The `cli.py` changes are the same pattern.
2. **The DLP layer is the most direct answer to the content-tripwire gap.** 2. **The DLP layer is the most direct answer to the content-tripwire gap.**
The `secret-exfil-tripwire-encodings.md` note concluded that no The `secret-exfil-tripwire-encodings.md` note concluded that no