Removes the per-sandbox setup cost AI_SBX_TOOLS pays on every setup, and makes the sandbox a terminal environment worth working in. Neovim comes from the upstream tarball rather than apt: Ubuntu's build is far behind what a LazyVim config needs. Upstream publishes no checksums, so NEOVIM_SHA256 is taken from the release asset and verified at build time. The version is pinned and labelled because a config that works on the host and breaks in the sandbox on a version skew is expensive to diagnose. The launcher is fetched from ai-sandbox v1.11.0's tasks/ai/workspace rather than vendored here. The task installs its own copy into stock-image sandboxes and skips that when the image supplies one, so two copies could drift and make behaviour depend on which image you are on. A bash -n guard rejects a forge error page served with a 200.
claude-sbx
A Docker Sandboxes template carrying Malcolm's Claude Code configuration — plugins,
skills, hooks and memory files — published to the Gitea container registry at
git.mroberts.dev.
Consumed by the ai:sbx mise task in
mroberts/ai-sandbox via
AI_SBX_TEMPLATE.
docs/HANDOFF.md is the original brief. Where the two disagree, this file is current:
several of the brief's assumptions turned out to be wrong once tested.
Status: blocked upstream
sbx v0.37.0 and v0.37.1 cannot consume custom templates at all. Every layer
stacked on the base image is silently dropped — the sandbox boots with base content
only, sbx create exits 0, and sbx inspect reports the correct image. Verified here
across all three delivery paths: sbx template load from a tar, sbx template save
snapshots, and a registry pull.
This is docker/sbx-releases#366 —
the erofs snapshotter stopped building the merged fsmeta.erofs at the top of the
chain. Confirmed by others on macOS, Windows, Ubuntu and WSL2, across Homebrew,
winget, apt and template load. v0.35.0 is unaffected, and templates saved under
0.37.1 materialize correctly when launched under 0.35.0.
Until it is fixed, this image builds and pushes correctly but produces a sandbox with
none of its contents. Use mise run ai:sbx -- setup instead — it installs the same
configuration and plugins into a stock sandbox at runtime, and works on 0.37.x today.
The pipeline itself is verified. CI builds and publishes on every push to main and
every tag, and the published image pulls and contains what it should: 17 enabled
plugins, mise, the hooks and skills, and no credentials or transcripts in any layer.
What is unverified is the only thing that matters at runtime — whether a sandbox
created from it sees any of that — and it will not until #366 is fixed.
What the image contains
| Path | Source |
|---|---|
claude/CLAUDE.md, claude/AGENTS.md |
~/.claude, verbatim |
claude/hooks, claude/skills |
~/.claude, verbatim |
plugins.json |
generated from ~/.claude/plugins/known_marketplaces.json and settings.json |
| mise | https://mise.run, with shims on PATH via .bashrc |
tmux |
Ubuntu apt, 3.6a |
nvim |
upstream release tarball into /usr/local, pinned and checksummed |
/usr/local/bin/ai-sbx-workspace |
fetched from ai-sandbox at the tag in AI_SANDBOX_REF |
The pinned versions are recorded in image labels
(dev.mroberts.claude-sbx.neovim, dev.mroberts.claude-sbx.ai-sandbox-ref), so a
sandbox that disagrees with the host config can be diagnosed from the image alone.
Neovim comes from upstream rather than apt because Ubuntu's build is far behind what
a LazyVim config needs. Upstream publishes no checksums, so NEOVIM_SHA256 was taken
from the release asset itself and is verified at build time.
With tmux and Neovim in the image, AI_SBX_TOOLS can drop back to bun.
Credentials, transcripts, history.jsonl, projects/, file-history/ and
session-env/ are never copied. ~/.claude/plugins/ is not copied either — the build
reinstalls plugins from the manifest, which keeps the repository small and the image
reproducible from source without any access to the host.
agents/ and commands/ are absent because both are empty on the host.
Building
docker build -t git.mroberts.dev/mroberts/claude-sbx:v1 .
Requires no access to ~/.claude. The build installs 10 marketplaces and 17 plugins
and fails if the resulting enabled-plugin count does not match the manifest.
The launcher is fetched from the ai-sandbox tag named by AI_SANDBOX_REF, rather
than vendored here. The task installs its own copy into sandboxes running a stock
image and skips that when the image supplies one, so two copies that could drift would
make behaviour depend on which image you happen to be on. Build against a different
release with:
docker build --build-arg AI_SANDBOX_REF=v1.12.0 -t git.mroberts.dev/mroberts/claude-sbx:v2 .
The default is v1.11.0, the ai-sandbox release that introduced the launcher. A tag
without tasks/ai/workspace fails the build at the fetch rather than shipping a
broken launcher.
Pushing
docker push fails against this registry: git.mroberts.dev is behind Cloudflare,
which rejects request bodies over 100 MB, and the base image has a 325 MB compressed
layer. Push chunked instead:
mise use -g "github:regclient/regclient[exe=regctl,matching=regctl-linux-amd64]"
regctl registry set git.mroberts.dev --blob-chunk 50000000 --blob-max 50000000
docker save git.mroberts.dev/mroberts/claude-sbx:v1 -o image.tar
regctl image import git.mroberts.dev/mroberts/claude-sbx:v1 image.tar
Plain ubi:regclient/regclient installs regbot rather than regctl; the matching
filter above picks the right asset. .gitea/workflows/build.yml does the same on tag
pushes and on main.
Pulls are unaffected by the Cloudflare limit, which caps uploads only.
CI authentication
The workflow authenticates with a REGISTRY_TOKEN secret holding a personal access
token scoped to package: Read and Write, and nothing else. The automatic
GITEA_TOKEN does not work: the package registry rejects it as unauthorized, and
adding permissions: packages: write to the job changes nothing.
The job runs on runs-on: linux, matching the self-hosted runner's label. There is no
ubuntu-latest runner on this forge, and a workflow naming one is discarded silently —
no queued run, no error, nothing in the Actions tab.
Consuming
# ~/.config/mise/config.toml
[env]
AI_SBX_TEMPLATE = "git.mroberts.dev/mroberts/claude-sbx:v1"
The sandbox's Docker daemon pulls from the registry directly and does not share the host image store, so a local build is invisible to it.
Notes from building this
- Marketplace sources are already portable. The brief called for rewriting SSH
remotes to HTTPS. Unnecessary:
known_marketplaces.jsonrecords marketplaces as{source: github, repo: owner/name}or an HTTPS git URL, never as the SSH remote the checkout happens to use. claude-plugins-officialis not built in. A fresh sandbox knows no marketplaces at all, so it is added explicitly like any other, fromanthropics/claude-plugins-official.- Plugin installs need no authentication, but a marketplace on a non-GitHub host
needs a network policy rule —
git.mroberts.devis denied by default. - Baked plugin enablement does not survive. sbx recreates
~/.claude/settings.jsonat sandbox creation, droppingenabledPluginswhile leaving the plugin files. Re-enabling against a baked image costs ~1.3s per plugin because the marketplace clones are already present;ai:sbx configdoes this. ~/.claude/skillsis a mount point inside the sandbox, backed by sbx's shared skills store. It cannot be replaced by a copy; seed it withsbx skills import.