CtrlK
BlogDocsLog inGet started
Tessl Logo

creating-sbx-kits

Write a Docker Sandbox kit — a spec.yaml declaring setup steps, network permissions and credentials. Use when authoring or fixing a kit, choosing between kind mixin and kind sandbox, migrating a spec from schemaVersion 1 to 2, or a kit fails sbx kit validate.

72

Quality

90%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

SKILL.md
Quality
Evals
Security

Creating sbx kits

A kit is a spec.yaml, and it decides what a sandbox is configured and permitted to do. What is installed belongs in an image — reach for creating-sbx-templates when the answer is a Dockerfile.

Kits stack at run time, because --kit repeats. That turns N agents × M task types into N bases plus M mixins, so write a mixin unless you are defining a whole agent. Of 198 published specs, 145 are kind: mixin.

Start from a skeleton

assets/mixin-spec.yaml is a commented v2 mixin. assets/sandbox-spec.yaml defines a whole agent. Copy one and strip what you do not need.

assets/sandbox-aware/ is a finished mixin rather than a skeleton: it carries no setup and no permissions, only agentInstructions telling the agent inside how a sandbox differs from a laptop — HTTPS rather than SSH for git, the 403 body that names its own rule, host.docker.internal, binding 0.0.0.0, which workspace mode it is in. Stack it onto anything with --kit.

Every field, with its type and its traps, is in references/spec-fields.md.

It loads or it doesn't

Both decoders set KnownFields(true) deliberately, so an unrecognised key is a hard error rather than a silent drop that leaves you believing the kit was honoured. 39 of 198 published specs — 19% — do not load at all.

Validation is therefore binary and cheap. Spend it after every edit:

sbx kit validate ./my-kit/

Without sbx on the machine, the same answer is still reachable: the grammar is enforced by the spec package in github.com/docker/sbx-kits-contrib, so a few lines calling spec.LoadFromDirectory then spec.ValidateArtifact reports the identical decode errors, deprecations and not-implemented warnings.

Then iterate against a real sandbox, which is the only way to see policy fire:

sbx run --kit ./my-kit/ claude
sbx policy log                 # what got blocked, and by which rule
sbx rm <sandbox>               # a clean slate; kits cannot be removed in place

The four decisions

Which kind. mixin layers onto an existing agent and declares no sandbox: block. sandbox defines a whole agent and needs sandbox.image, unless it inherits one through extends.

Mixins are base-agnostic by design, and the validator enforces it three ways. A mixin that sets extends is a hard error — inheritance is a kind: sandbox move, single-parent. requires.agent pins a mixin to one base agent and is rejected on any other kind, so leave it unset unless the mixin genuinely only works under one agent. And mixins: inside a spec parses, warns not implemented, and has no runtime effect: the field is accepted so kits can be written against the published v2 docs, but composition is still repeated --kit at the CLI, in order, last one winning a conflict.

Order therefore carries meaning in a stack. environment.variables is last-wins, so a kit composed later silently overrides an earlier kit's value of the same name — NODE_PATH set by two mixins is one mixin's NODE_PATH.

That is what keeps a library of them flat. Layering belongs to the image, where FROM does it properly — see creating-sbx-templates. A kit carries what is permitted, which does not multiply across a family: one typescript mixin's registry.npmjs.org allow works under every agent.

What runs, and when. setup.install runs once at creation, as a shell string. setup.startup runs on every start, as argv, so wrap a pipeline in sh -c. Two independent authors discovered that asymmetry the hard way.

They also run as different users: install defaults to uid 0, startup to 1000. A file written by install without a chown is root-owned when the agent meets it.

That split is the trap most mixins hit, and it is the kit-layer twin of the USER root / USER agent rule in creating-sbx-templates. Anything installing to a per-user default — ~/.cache, ~/.local, ~/.npm — lands in /root/ and the agent never sees it. Install to a fixed system path and export the variable that points at it, the way PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright moves a browser cache out of ~/.cache. Leave that directory writable by uid 1000 if the tool fetches more at run time. For a language runtime, a virtualenv under /opt/ with its entry points symlinked into /usr/local/bin does the same job.

Finish an install by proving it ran. The completion message reports the exit code and nothing else, so a step that installed nothing still reads as success. End with a probe — <tool> --version >/dev/null — and the failure lands at creation instead of in front of the agent.

Put volatile state in startup and make it idempotent, because it replays. binfmt_misc registration is the standard example. Keep install short: it runs in a throwaway provisioning container before the Docker socket mounts and before files land, so docker pull fails there, and slow work is SIGKILLed at exit 137.

When an install genuinely needs minutes, detach it and gate the agent on a flag file. install writes the real installer to a script, launches it with setsid … & so it is not the hook's child, and the script touches a flag when it finishes. The entrypoint points at a wrapper that polls for that flag before execing the real binary. Creation returns straight away, and the agent waits only if it wins the race. Published multi-minute kits are built this way.

What it may reach. permissions.network.allow takes a list of domains. Ordinary git work needs six GitHub hosts, not one:

permissions:
  network:
    allow:
      - github.com
      - api.github.com
      - raw.githubusercontent.com
      - objects.githubusercontent.com
      - codeload.github.com
      - release-assets.githubusercontent.com

Allow only github.com and clones fail in ways that look like a broken image. Add registry.npmjs.org, or pypi.org and files.pythonhosted.org, per ecosystem. A blocked request returns HTTP 403 with the rule named in the body.

Anything running apt-get needs four more. apt-get update refreshes every configured source, so one unreachable source fails the whole run with exit 100 even when your package lives elsewhere.

- archive.ubuntu.com      # amd64 main
      - security.ubuntu.com     # amd64 security
      - ports.ubuntu.com        # arm64 serves from here instead
      - download.docker.com     # pre-added by every -docker template

ports.ubuntu.com is what keeps a kit working on Apple silicon, and download.docker.com is inherited from the base image rather than anything your kit asked for.

Pin releases rather than resolving them. Asking a releases/latest endpoint which version to fetch spends an unauthenticated API quota shared across every caller on that IP — 60 requests an hour, then 403. The install works until it abruptly doesn't, on no change of yours. Pin the version and a per-arch SHA256 instead, branching on dpkg --print-architecture for the right tarball and checksum.

How credentials arrive. A kit declares how to inject a secret, never where the value comes from — that is the user's sbx secret set. Name the exact authentication host in inject[].domain; a wildcard forces TLS interception across CDN subdomains and corrupts binary downloads.

Only 54 of 198 published specs declare credentials: at all. It is a minority practice worth adopting, because the alternative is a real key inside the box.

Tell the agent what changed

Every kit gets agentInstructions. A kit that installs a tool and says nothing has added a capability the agent will never use, because nothing else announces it — and one that changes how an existing tool behaves has laid a trap. Write the runbook form: the failure, the symptom, the way out. Name the command, the path, and the variable that has to be set.

Scope it to what this kit did. Facts about the environment itself — that git pushes over HTTPS, that a 403 body names its rule, that a host service is host.docker.internal — belong in one mixin stacked everywhere, not copied into each kit. A mixin's instructions land in kits-memory/<kit-name>.md, one file per kit, so five kits repeating the same paragraph spend that context five times and drift apart the first time sbx changes. assets/sandbox-aware/ is that mixin; keep it in the --kit list by default and let every other kit describe only its own contribution.

Write permissions, not caps

caps: is the single most common way a public kit breaks. It is a genuine v1 field that no documentation page mentions — an earlier v2 draft spelling that leaked, since Caps.Network is the internal form both grammars normalise into. Under schemaVersion: "1" it loads with a deprecation warning; under "2" it is fatal. It breaks 27 published specs.

The same applies to kind: agent, which appears widely in public repos with three different body shapes and is documented only as a legacy row. Write kind: sandbox.

Shipping an executable

setup.files writes a file with a mode, which is how a kit adds a command without an image rebuild. The worked example is an Agent Client Protocol adapter, which lets a host editor drive the sandboxed agent over stdio: a kind: mixin kit drops one launcher into /home/agent/.local/bin/ that pins the adapter version and execs it.

setup:
  files:
    - path: /home/agent/.local/bin/claude-acp
      mode: "0755"
      content: |
        #!/bin/bash
        set -e
        if [ -z "$CLAUDE_CODE_EXECUTABLE" ]; then
          export CLAUDE_CODE_EXECUTABLE=claude
        fi
        exec npx -y @agentclientprotocol/claude-agent-acp@0.51.0 "$@"

The launcher defaults an environment variable to the sandboxed binary rather than hard-coding it, so a caller can override. A host tool then drives it over stdio with sbx exec -i. Say so in agentInstructions, since nothing else tells the agent the command exists. npx -y needs registry.npmjs.org in permissions.network.allow.

Migrating from v1

The grammars fork at schemaVersion. A v1 key inside a v2 spec is a decode error, not a translation, so migrate every key or none.

Docker's own build-an-agent.md tutorial is written in v1. Followed verbatim under schemaVersion: "2" it produces a spec that is rejected at decode. The rename table in references/spec-fields.md is the translation.

Distributing one

sbx kit pack ./my-kit/ -o my-kit.zip
sbx kit push ./my-kit/ docker.io/you/my-kit:v1

Kit sources are allowlisted, defaulting to docker.io/ only, so a git-URL kit needs sbx settings set kit.allowedSources before it will install.

Pin anything you consume. A git-URL kit is an active code load at sandbox creation — whatever sits on that branch runs — so reference it as git+https://host/repo.git#ref=v0.1.0. Treat a kit you publish like a base image: tag releases, and let consumers opt into updates rather than receiving them silently.

Last verified

Verified against a research corpus and the sbx docs of 2026-08-07 (re-fetched unchanged on 2026-08-10). The mixin constraints, the mixins: no-op, the install/startup user defaults, the scheme sugar, and the ports/volumes/resources rules were read on 2026-08-10 from the spec package Docker publishes in docker/sbx-kits-contrib, which is the code that enforces them; the launcher, apt-host and detached-install patterns come from that repo's published kits. Deny-across-composed-kits is from the docs mirror. Claims appearing only in that repo's prose were left out where the code or the mirror did not agree. The kit system is experimental: the format, the CLI and the workflow all change. sbx reached a second kit schema within four months of launch, so record which schemaVersion you wrote against and re-check before relying on a version-bound claim. sbx kit validate is the ground truth.

Repository
slurpyb/sbx-agent
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.