Add sandbox template image with Claude configuration and plugins
build / build (push) Canceled after 0s
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:
+305
@@ -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`.
|
||||
Reference in New Issue
Block a user