mroberts b536a587c1
build / Build and push image (push) Successful in 3m34s
Drop the Vikunja skill from the image
The image is published to a public registry, and the skill named the
employer as its Vikunja root project and pointed at tasks.mroberts.dev.
Neither is a secret, but neither belongs in an artefact anyone can pull.

It is a host-side planning skill with nothing to do inside a sandbox, so
removing it costs the sandbox no capability.
2026-08-03 16:14:32 -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
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.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%