Build a worker Kit
Want workers in a plain container, with no Kit? Under the repository's entry in repos.yaml, set sandbox: {kind: docker, image: <your image>, network: <hosts>}; a sandbox needs a network policy or unrestricted_network: true. Sandboxed workers has the rest of the keys.
Build a Kit for Claude workers, push it to your registry, and point a repository at it. A Kit is an image that carries its own sandbox policy: which hosts its workload may reach, which credentials it holds, and its limits. Kraft publishes no worker image of its own.
Kraft runs a Kit you build for it. Docker's published Kits require capabilities Kraft does not enforce, so Kraft refuses them.
Before you start
- Docker with BuildKit (
docker buildx versionprints it), logged in to a registry you can push to. Replaceregistry.example.com/acmebelow with yours. - The machine that runs Kraft logged in to the same registry: Kraft reads the Kit with that machine's
docker manifest inspectand pulls it with itsdocker run. - An Anthropic API key in the Kraft server's environment.
1. Write the image
The image needs the agent CLI on its PATH and the minimum every sandboxed image needs: git, sh, CA certificates, and curl under a network policy. See Sandboxed workers.
FROM docker.io/library/node:22-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends bash ca-certificates curl git \
&& rm -rf /var/lib/apt/lists/*
RUN npm install -g @anthropic-ai/claude-code
Save it as Dockerfile.
The image needs no agent Entrypoint. Kraft keeps the image's entrypoint and replaces its command with the one the harness file builds. A Kit whose entrypoint is the agent itself does not work.
2. Write the descriptor
Save this as kraft-worker-claude.yaml beside the Dockerfile:
# syntax=docker/sandbox-kit:3
schemaVersion: "3"
kind: workload
displayName: Kraft claude worker
dockerfile: Dockerfile
capabilities:
- type: com.docker.sandbox/network-policy@1
config:
runtime:
allow: [api.anthropic.com, statsig.anthropic.com]
- type: com.docker.sandbox/credential@1
config:
service: anthropic
phase: runtime
apiKey:
name: ANTHROPIC_API_KEY
proxyManaged: true
inject:
- {domain: api.anthropic.com, header: x-api-key}
- type: com.docker.sandbox/resources@1
config: {cpu: 2, memory: 2gib}
Two of these capabilities need something from you: the Kit has no install phase, and a Kit for Claude lists Claude's own hosts itself. What each capability becomes in Kraft:
network-policy@1: the sessions reachapi.anthropic.comandstatsig.anthropic.comand nothing else. There is noinstallphase, so the repository'ssetup_commandreaches nothing: build what setup needs into the image, or add aninstallallow list. Kraft does not add the Claude harness's own hosts under a Kit, so a Kit for Claude lists them itself.credential@1: the container seesANTHROPIC_API_KEYonly as a placeholder. Kraft's egress proxy, the proxy that carries a sandboxed session's outbound traffic, adds the real key to thex-api-keyheader on requests toapi.anthropic.com. The value comes from the server variable you bind to theanthropicservice in step 4.resources@1: two CPUs and a 2 GiB memory limit on every session.
3. Build, push and pin it
docker buildx build -f kraft-worker-claude.yaml \
-t registry.example.com/acme/kraft-worker-claude:1 --push .
docker buildx imagetools inspect registry.example.com/acme/kraft-worker-claude:1
The # syntax=docker/sandbox-kit:3 line makes BuildKit build it with the Kit frontend, which puts the descriptor on the image's manifest. The second command prints the digest (Digest: sha256:...).
Pin the Kit by that digest. Kraft refuses a Kit named by tag alone, because a tag can point at a different Kit tomorrow.
4. Point a repository at it
In repos.yaml, under the repository's entry:
sandbox:
kind: kit
runtime: docker
kit: registry.example.com/acme/kraft-worker-claude:1@sha256:<the digest>
In sandbox.yaml, bind the credential's service to the server variable holding the key:
credentials:
anthropic: ANTHROPIC_API_KEY
The right-hand side is a variable in the Kraft server's environment. It can have any name.
Check it worked
kraft admin doctor
The repository's sandbox row reads and checks the Kit. If the Kit is refused, that row fails and names the Kit and why.
A kit hosts row is a warning, and it is expected: this Kit is for Claude, and sessions of other harnesses under it are refused their hosts. It reads:
warn kit hosts <repo> the Kit does not allow what these harnesses need, so their sessions are refused it: amp requires ampcode.com; codex requires api.openai.com, chatgpt.com; gemini requires generativelanguage.googleapis.com
Then file a work item on the repository. Its first task records a sandbox_kit_resolved event:
kraft view events ID --type sandbox_kit_resolved
If the item stops instead, see A Kit is refused.
Change the Kit
A rebuilt Kit has a new digest, and an item filed before the change keeps the sandbox it was filed with.
- Rebuild and push, as in step 3.
- Copy the new digest from the
imagetools inspectoutput. - Put it in
kit:under the repository'ssandbox:inrepos.yaml.
Kraft rereads repos.yaml when an item is filed, so no kraft admin reload is needed.
Next steps
- Kits: every field a Kit sandbox accepts.
sandbox.yaml: bind credentials to server variables.- A Kit is refused: the stop reasons for a Kit Kraft will not run.
Add or override a harness
Add an agent CLI to Kraft with a YAML file, and override a shipped one.
Give a worker its login
Give the agent CLI a worker runs on, whether Claude Code, Codex, Cursor, OpenCode, Amp, Antigravity or Gemini CLI, a login it can use headless, and check that the worker reaches it.