(adr-py- uvx claude-sandbox [OPTIONS] [claude|codex|pi|shell] [AGENT_ARGS...] is the launcher. The verbs replace --agent NAME and --shell. The helper verbs of the in-container CLI (gh-auth, glab-auth, verify, pi-local, version, update) are forwarded into the container, so uvx claude-sandbox verify means the same on the host as inside.-front-door)=

23. A PyPI wheel as the front door: uvx claude-sandbox#

Date: 2026-09-13

Status#

Accepted

Documentation update (2026-09-14): the recommended host workflow is uv tool install claude-sandbox, then claude-sandbox. The uvx launcher remains available for one-off use and uvx claude-sandbox install remains the devcontainer installation recipe. The decision below records the original distribution design.

Refines ADR 8 (bash-only) and ADR 17 (reference at a pin, never copy).

Context#

Two consumers of the sandbox were paying a distribution tax that the code itself does not justify.

The guest devcontainer is the most common way the sandbox is used: a project’s own devcontainer, with the sandbox injected at postCreate. ADR 17 made that a reference at a pinned tag, which is right, but the reference was twelve lines of git plumbing per repository (clone, fetch, checkout, install --here), and each repository carried its own copy of the recipe.

The host launcher for the published image, container/claude-container, was fetched with curl and copied onto PATH. A copied script has no package manager: it went stale silently, and the notify-only versioning of ADR 17’s companion image design deliberately refuses a self-updater, so staleness was structural. Its version also had nothing to do with the image tag it pulled (:latest), so a fresh image and an old launcher, or the reverse, was the normal state.

Two alternatives were considered and set aside. A devcontainer feature would inject the sandbox with one JSON line and no runtime dependency, but features run at image build and the team found them slow to start; the launcher is host-side, where a feature does not help at all. A bootstrap curl | bash one-liner removes lines but not the staleness, and adds a second fetch path to audit.

Decision#

Publish one wheel, claude-sandbox, on PyPI, and make uvx claude-sandbox the front door for both consumers.

The wheel holds no sandbox logic. It bundles, verbatim, the launcher script, the install shim, .devcontainer/claude-sandbox/, the shipped conf and the statusline script, in the repository’s own layout, plus a one-module Python entry point that locates them and execs bash. The bash is what runs; the Python sets two environment variables and calls execvpe.

  • uvx claude-sandbox [OPTIONS] [claude|codex|pi|shell] [AGENT_ARGS...] is the launcher. The verbs replace --agent NAME and --shell.

  • uvx claude-sandbox install runs the installer inside a devcontainer. The entry point refuses it outside a container (CLAUDE_SANDBOX_HOST_INSTALL=1 overrides), because a one-word command can be typed on a host by accident where the twelve-line recipe could not.

  • The launcher refuses to run inside a container: an installed sandbox is started by the agents’ own names, and a bare container wants install. CLAUDE_SANDBOX_NESTED=1 overrides, for an engine inside a container.

One release number: the git tag. The wheel version comes from the tag by hatch-vcs (the same setuptools_scm mechanism the DLS python-copier template uses; _dist.yml, _pypi.yml and _release.yml are copied from it, and ci.yml wires them in the same way). PyPI normalises a prerelease tag such as 4.0.0-beta.1 to 4.0.0b1; image tags are the raw tag, so the entry point maps the wheel version back before pinning CLAUDE_SANDBOX_IMAGE (a wheel built between tags has no image and falls back to :latest). The hyphen in the tag is what keeps the install shim’s newest-stable filter from choosing a beta. CI bakes the tag into the image label on a tag build, and the entry point passes the same value to the launcher as CLAUDE_SANDBOX_LAUNCHER_VERSION, so uvx claude-sandbox==4.0.0 runs that launcher against that image and uvx claude-sandbox@latest moves both together (CLAUDE_SANDBOX_IMAGE still overrides). A copied script keeps its own VERSION= literal, :latest and the label comparison, which is what serves that path.

Host networking by default. The launcher now creates the container with --network=host; --bridge opts out. Pi’s relay to a local model needs the host’s loopback, and the repository’s own devcontainer already runs host-net. The agents’ posture is unchanged: the egress jail is built inside the container and only restricts, so RFC1918 stays blackholed and loopback crosses only through the configured relay ports whatever the container’s network mode (ADR 15).

The bash-only boundary holds. The packaging lives under packaging/pypi/ only: no root pyproject.toml, no lockfile, no src/, no test framework. It is the third and last Python exception in CLAUDE.md, alongside the docs toolchain and the socket fixture, with the same rule: it may not grow logic. Published as a wheel only; an sdist would be a second copy of the tree, which ADR 17 forbids.

Consequences#

  • The guest recipe is one line, uvx claude-sandbox==X.Y.Z install, and the pin is the version. The clone recipe stays documented for images without uv; both run the same installer.

  • claude-sandbox update after a wheel install points at uvx instead of cloning past the pin. The installer stamps installer next to version under /usr/libexec/claude-sandbox/ so the CLI can tell.

  • PyPI is a second trust root beside the GitHub tag. Trusted publishing (OIDC from the release workflow) means no token exists to leak; the pypi environment on the repository must be created once, and the PyPI project’s publisher set to it, before the first tag publishes.

  • The launcher’s pre-4.0 flags (--agent, --shell, --host-net) exit with the new spelling rather than fall through to the agent’s argv. No aliases: an unknown option would otherwise fall through to the agent and fail obscurely.

  • uvx caches the wheel: a plain uvx claude-sandbox does not move to a new release on its own. That is the deliberate-update property the notify-only design wanted, now provided by the package manager.

  • Releases are git tags from 4.0.0 on: the wheel, the image and the launcher report the tag, with nothing to bump in the tree.