v2.0.0next
Agents and harnesses

Build a worker Kit

Build a Docker Sandbox Kit for Kraft's Claude workers, push it to your registry, and run a repository's sandbox from it, pinned by digest.

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 version prints it), logged in to a registry you can push to. Replace registry.example.com/acme below with yours.
  • The machine that runs Kraft logged in to the same registry: Kraft reads the Kit with that machine's docker manifest inspect and pulls it with its docker 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.

Dockerfile
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.

Kraft mounts each session's result file into the container, so a tool that replaces it by renaming a new file over it would fail. The Claude CLI writes it in place.

2. Write the descriptor

Save this as kraft-worker-claude.yaml beside the Dockerfile:

kraft-worker-claude.yaml
# 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 reach api.anthropic.com and statsig.anthropic.com and nothing else. There is no install phase, so the repository's setup_command reaches nothing: build what setup needs into the image, or add an install allow 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 sees ANTHROPIC_API_KEY only as a placeholder. Kraft's egress proxy, the proxy that carries a sandboxed session's outbound traffic, adds the real key to the x-api-key header on requests to api.anthropic.com. The value comes from the server variable you bind to the anthropic service 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:

config/repos.yaml
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:

config/sandbox.yaml
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.

  1. Rebuild and push, as in step 3.
  2. Copy the new digest from the imagetools inspect output.
  3. Put it in kit: under the repository's sandbox: in repos.yaml.

Kraft rereads repos.yaml when an item is filed, so no kraft admin reload is needed.

Next steps

Copyright © 2026