mroberts 9be4122580
build / Build and push image (push) Successful in 30s
Record the verified CI path and its token requirement
The registry rejects the automatic token, so the workflow needs a personal access
token scoped to package read and write, and the runner label must match one the
forge actually has. Both cost a failed run to discover.
2026-07-31 09:06:05 -05:00

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

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.

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.json records marketplaces as {source: github, repo: owner/name} or an HTTPS git URL, never as the SSH remote the checkout happens to use.
  • claude-plugins-official is not built in. A fresh sandbox knows no marketplaces at all, so it is added explicitly like any other, from anthropics/claude-plugins-official.
  • Plugin installs need no authentication, but a marketplace on a non-GitHub host needs a network policy rule — git.mroberts.dev is denied by default.
  • Baked plugin enablement does not survive. sbx recreates ~/.claude/settings.json at sandbox creation, dropping enabledPlugins while leaving the plugin files. Re-enabling against a baked image costs ~1.3s per plugin because the marketplace clones are already present; ai:sbx config does this.
  • ~/.claude/skills is a mount point inside the sandbox, backed by sbx's shared skills store. It cannot be replaced by a copy; seed it with sbx skills import.
S
Description
sbx template for claude code with my plugins pre-configured
Readme
99 KiB
v1.0.0
Latest
2026-07-31 13:19:54 +00:00
Languages
JavaScript 82.9%
Shell 7%
PowerShell 5.5%
Dockerfile 4.6%