The v1.0.1 tag claimed this work but did not contain it: the edits were still in the working copy when main was moved and pushed. This is that change. The run against v1.0.1 reached the registry and failed with 'unauthorized'. The automatic token cannot write packages without the job asking for it, so the job now requests packages: write and nothing wider. actions/checkout is pinned to a commit rather than a tag, at v4.4.0 rather than the current v7.0.1, which declares node24 that act_runner does not provide. regctl is pinned to a release instead of tracking latest. Every expansion moves into env so nothing interpolates into a shell body. Clean under actionlint and zizmor --persona=auditor. .shush.toml preserves the comments those linters require, which the comment hook was otherwise removing.
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.
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 |
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.
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, authenticating with the automatic GITEA_TOKEN.
Pulls are unaffected by the Cloudflare limit, which caps uploads only.
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.