Add sandbox template image with Claude configuration and plugins
build / build (push) Canceled after 0s

Carries CLAUDE.md, AGENTS.md, hooks and skills verbatim from the host, plus a
manifest of the 10 marketplaces and 17 plugins to reinstall at build time. The
plugin directories themselves are not committed: ~/.claude/plugins is 831 MB and
sits alongside credentials and transcripts, so the image is reproduced from the
manifest instead and the build needs no access to the host.

The Gitea registry is behind Cloudflare, which rejects request bodies over 100 MB
against a base image with a 325 MB layer, so the workflow pushes chunked through
regctl rather than docker push.

sbx v0.37.0 and v0.37.1 cannot consume the result: layers stacked on the base are
silently dropped (docker/sbx-releases#366). The image builds and pushes correctly
and is a no-op at runtime until that is fixed, so README points at
'ai:sbx setup' as the mechanism that works today.
This commit is contained in:
2026-07-31 08:18:30 -05:00
commit c89e4f0568
22 changed files with 2726 additions and 0 deletions
+305
View File
@@ -0,0 +1,305 @@
# Hand-off: claude-sbx template
Build a custom Docker Sandboxes template carrying Malcolm's Claude Code
configuration — plugins, skills, hooks, agents, and memory files — and publish it to
the Gitea container registry at `git.mroberts.dev`.
Consumed by the `ai:sbx` mise task in
[`mroberts/ai-sandbox`](https://git.mroberts.dev/mroberts/ai-sandbox) via
`AI_SBX_TEMPLATE`.
## Why this exists
Docker Sandboxes deliberately ignore the host `~/.claude`. The agent runs as a
separate `agent` user with `HOME` pointing elsewhere, so even a read-only mount is not
picked up — confirmed by Docker staff in
[forum thread 151158](https://forums.docker.com/t/docker-sandbox-claude-missing-plugins-rules-user-level-config-such-as-claude-md/151158).
Three mechanisms exist, and only the third covers plugins:
| Mechanism | Covers | Verdict |
| --- | --- | --- |
| `sbx skills import` | `~/.claude/skills` only | Works, but misses everything under `~/.claude/plugins/` |
| Mixin kit | tools, env vars, network rules, startup commands | No plugin support |
| **Custom template** | anything bakeable into an image | **This repo** |
Plugins carry commands, hooks, agents, and MCP servers. Only an image moves those.
## The central constraint
**CI cannot read `~/.claude`.** It lives on the workstation. So the repo must not
depend on it.
There were two options:
1. Commit a curated copy of `~/.claude` — rejected. `~/.claude/plugins` alone is
**831 MB**, and the directory also holds `.credentials.json`, `history.jsonl`, and
2.4 GB of conversation transcripts under `projects/`. Committing any of it risks
pushing secrets to a registry.
2. **Commit a manifest and reinstall plugins at image build time** — chosen. The repo
stays small, the image is reproducible from source, and no host state or secret is
involved.
A local-only build script already exists at `~/.config/ai-sbx/template/` (Dockerfile
plus an allowlist-based `build`). It works, but requires the workstation. **This repo
supersedes it.** Read it for the allowlist and safety guards before deleting.
## What to build
### 1. Plugin manifest
Encode the current host state. 17 enabled plugins across 8 marketplaces:
```
caveman@caveman
chrome-devtools-mcp@claude-plugins-official
clangd-lsp@claude-plugins-official
claude-mem@thedotmack
codex@openai-codex
context-mode@context-mode
csharp-lsp@claude-plugins-official
frontend-design@claude-plugins-official
gopls-lsp@claude-plugins-official
hookify@claude-plugins-official
ponytail@ponytail
rust-analyzer-lsp@claude-plugins-official
security-guidance@claude-plugins-official
superpowers@claude-plugins-official
typescript-lsp@claude-plugins-official
guards@mroberts
reviews@mroberts
```
Marketplace sources, taken from the git remotes of
`~/.claude/plugins/marketplaces/*`:
| Marketplace | Remote |
| --- | --- |
| `anthropics-claude-plugins-official` | `https://github.com/anthropics/claude-plugins-official.git` |
| `caveman` | `[email protected]:JuliusBrussee/caveman.git` |
| `chrome-devtools-plugins` | `[email protected]:ChromeDevTools/chrome-devtools-mcp.git` |
| `claude-code-plugins` | `https://github.com/anthropics/claude-code.git` |
| `context-mode` | `https://github.com/mksglu/context-mode.git` |
| `mroberts` | `https://git.mroberts.dev/mroberts/claude-plugin.git` |
| `openai-codex` | `[email protected]:openai/codex-plugin-cc.git` |
| `ponytail` | `[email protected]:DietrichGebert/ponytail.git` |
| `superpowers-marketplace` | `[email protected]:obra/superpowers-marketplace.git` |
| `thedotmack` | `[email protected]:thedotmack/claude-mem.git` |
**Rewrite the SSH remotes to HTTPS.** CI has no SSH key, and every one of these is a
public GitHub repo, so `https://github.com/OWNER/REPO.git` works without credentials.
Two things need resolving before the manifest is correct:
- `claude-plugins-official` is **not a git repo** on the host, yet 11 enabled plugins
claim it as their marketplace. Determine whether it is bundled with Claude Code or
an alias for `anthropics-claude-plugins-official`, and whether it needs adding at
all.
- `superpowers@claude-plugins-official` is enabled, but a separate
`superpowers-marketplace` remote also exists. Confirm which one actually serves it.
### 2. Static configuration
Commit these, sourced from the host but reviewed before committing:
```
CLAUDE.md global memory
AGENTS.md agent instructions
settings.json see the caveat below
hooks/ 60K
skills/ 48K
agents/ currently empty on the host
commands/ currently empty on the host
```
Do **not** commit `plugins/`, `projects/`, `transcripts/`, `history.jsonl`,
`file-history/`, `.credentials.json`, `cache/`, `backups/`, or `session-env/`.
### 3. Dockerfile
```dockerfile
FROM docker/sandbox-templates:claude-code-docker
USER root
COPY --chown=agent:agent claude/ /home/agent/.claude/
USER agent
RUN claude plugin marketplace add <url> && claude plugin install <name>@<marketplace>
```
Base image variants are `docker/sandbox-templates:<variant>`; `-docker` variants
include a full Docker Engine inside the sandbox and are what `sbx` uses by default.
Keep `claude-code-docker` unless the sandbox never needs to build containers.
Install tools as `root`; anything landing in the home directory must run as `agent`,
or it installs under `/root/` and is invisible at runtime.
### 4. Build tools and language toolchains
The agent needs to actually build the projects it works on, so the template carries
toolchains as well as configuration.
**Start from what the base image already has.** All `docker/sandbox-templates:*`
variants are Ubuntu-based, run as a non-root `agent` user with sudo, and most include
Git, the Docker CLI, and Node.js, Python, Go, and Java. Check before installing —
duplicating a toolchain wastes layer space and creates version ambiguity.
```bash
docker run --rm docker/sandbox-templates:claude-code-docker \
bash -lc 'node -v; python3 -V; go version; java -version; git --version; docker -v'
```
**The `USER` rule is not cosmetic.** System packages need `root`; anything installing
into the home directory must run as `agent`, or it lands in `/root/` and is invisible
at runtime. This bites `rustup`, `nvm`, `pyenv`, and `mise`.
```dockerfile
FROM docker/sandbox-templates:claude-code-docker
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential pkg-config protobuf-compiler \
&& rm -rf /var/lib/apt/lists/*
USER agent
RUN curl https://mise.run | sh
```
**Prefer `mise` for language toolchains.** Repositories here already carry
`mise.toml`, so installing `mise` once in the image lets each project pin its own
versions, and `mise install` in the working directory resolves them at runtime.
That keeps the image generic instead of accumulating one toolchain per project.
Note `mise` must be **2026.7.17 or later** if the sandbox is ever to consume remote
`git::` task includes — earlier versions drop them silently.
**Three places tools can come from,** in increasing order of how often they change:
| Where | Runs | Use for |
| --- | --- | --- |
| Template `RUN` | image build | Everything shared across all repositories |
| Kit `commands.install` | once at sandbox creation | Per-project tools, without rebuilding the image |
| Kit `commands.startup` | every sandbox start | Background services, cache warming — must be idempotent |
`ai:sbx` already passes `--kit PATH` through to `sbx create`, repeatable, so a
project needing something unusual does not force a template rebuild.
**Networking will bite.** The default policy is `Balanced` — default-deny with common
development sites allowed. `apt`, `npm`, `crates.io`, and PyPI generally work; private
registries and internal proxies do not. Anything CareEvolution hosts internally needs
an explicit rule:
```bash
sbx policy allow network -g nexus.internal.example.com
```
Rules baked into a kit travel with the project, which is usually better than a global
rule on each developer's workstation.
**Docker inside the sandbox** is available on `-docker` variants: a full Engine runs
in the microVM with a 50 GB sparse block volume at `/var/lib/docker`, so the agent can
build and run containers without touching the host daemon. Override the size with
`DOCKER_SANDBOXES_DOCKER_SIZE` before starting. Use a non-`-docker` variant if the
agent never builds images — it is lighter and non-privileged.
### 5. Gitea Actions workflow
`.gitea/workflows/build.yml`. Gitea also honours `.github/workflows/`, but the
explicit path avoids ambiguity.
Requirements:
- Build and push `git.mroberts.dev/mroberts/claude-sbx:<tag>` on tag push, and a
`:main` or `:edge` tag on pushes to `main`.
- Authenticate with the automatic `${{ secrets.GITEA_TOKEN }}` and
`${{ github.actor }}`. No PAT should be needed — the repo has `has_packages: true`.
- Layer the plugin install so marketplace clones cache. It is the slow step.
- Tag immutably. `AI_SBX_TEMPLATE` pins one ref, and a moving tag means an unreviewed
image runs on the workstation with the developer's credentials.
Verified on the forge:
```
git.mroberts.dev Gitea 1.27.0
mroberts/claude-sbx private=false empty=true has_actions=true has_packages=true
```
**Confirm at least one Actions runner is registered and online before writing the
workflow.** `has_actions: true` only means the feature is enabled. If no runner
exists, either register one or fall back to building from the workstation with
`docker build --push`.
## Consuming the result
```toml
# ~/.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 the image must be pushed — a local build is invisible to it.
For a private registry, also run:
```bash
sbx secret set --registry git.mroberts.dev
```
Per-repository overrides, already supported by `ai:sbx`:
```bash
mise run ai:sbx -- setup --template git.mroberts.dev/mroberts/claude-sbx:v2
mise run ai:sbx -- setup --stock-template
```
## Known risks
**`settings.json` may be overwritten.** Reported in the forum thread above: a
`settings.json` baked into a template is visible under `docker run` but gone under
`--template`; Claude appears to rewrite it at startup. Since plugin *enablement* lives
there, plugin files may land while the plugins stay disabled. The suggested workaround
is `claude --settings <file-or-json>`. **Verify this early** — it determines whether
the whole approach delivers working plugins or just inert files.
**Plugin installs may require authentication.** Whether `claude plugin install`
works unauthenticated in CI is unverified. If it needs a logged-in session, the
workflow needs a token, or the image must ship pre-fetched plugin directories instead.
**Image size.** Plugins are ~831 MB on the host, of which ~211 MB is
`marketplaces/` source checkouts and ~609 MB is `cache/`. Claude Code loads from
`cache/`, so the marketplace checkouts may be droppable — unverified, and dropping
them may break plugin resolution. Measure the built image and trim deliberately.
**Runtime behaviour of daemon-backed plugins.** `claude-mem` and `context-mode` run
local services and MCP servers. Under a microVM with default-deny networking they may
misbehave. Test them explicitly rather than assuming a successful build means a
working sandbox.
**Hooks execute code.** Carrying the full host hook set into the sandbox erodes part
of the isolation the sandbox exists to provide. Worth a deliberate decision about
which hooks belong inside.
## Acceptance
1. `docker build` succeeds from a clean checkout with no access to `~/.claude`.
2. The workflow pushes on tag and the image is pullable from `git.mroberts.dev`.
3. `AI_SBX_TEMPLATE` set to the pushed ref, then
`mise run ai:sbx -- setup --replace` in a repository produces a sandbox where
`claude plugin list` shows the expected plugins **enabled**, and a known skill and
hook both fire.
4. No credential, transcript, or history file is present in any image layer. Check
with `docker history` and by extracting the layer, not by inspecting the build
context alone.
## Environment notes
Host is CachyOS, not the Ubuntu 24.04 Docker documents as supported. Two things were
needed to get `sbx` working at all, both worth repeating on any Linux workstation:
- Install the **`docker-sbx`** AUR package, not `docker-sandbox-bin`. The latter ships
only the CLI binary, omitting the microVM kernel, rootfs, and
`containerd-shim-nerdbox-v1`. Without those, `sbx` has no VM to boot and falls back
to mounting filesystems on the host, failing with `operation not permitted`.
- `sudo usermod -aG kvm $USER`, then re-login. Sandboxes are microVMs.
`sbx` must be **0.37 or later**: `--clone` replaced `--branch`, and `ai:sbx` uses
`--clone`.