# Kit spec reference




> [!NOTE]
> Kits are experimental. The kit file format, CLI commands, and experience
> for creating, loading, and managing kits are subject to change as the
> feature evolves. Share feedback and bug reports in the
> [docker/sbx-releases](https://github.com/docker/sbx-releases) repository.

This page documents every field in a kit's `spec.yaml`. For an overview of
what kits are and how to use them, see [Kits](/ai/sandboxes/customize/kit-reference/kits/).

For the normative v2 grammar used by the parser and tests, see the
[`schemaVersion: "2"` specification](https://github.com/docker/sbx-kits-contrib/blob/main/spec/SPEC-v2.md)
in the `docker/sbx-kits-contrib` repository.

A kit directory has a required `spec.yaml` and an optional `files/` tree:

```text
my-kit/
├── spec.yaml       # required
└── files/          # optional — static files to inject
    ├── home/
    └── workspace/
```

## Schema versions

Starting with Docker Sandboxes version 0.36, two schema versions are supported.
Use `schemaVersion: "2"` for new kits. Version `"1"` remains accepted through
the legacy path.

The loader forks on `schemaVersion`. A v2 spec uses the v2 grammar only. Legacy
v1 fields in a `schemaVersion: "2"` spec are rejected during decode instead of
being folded into the v2 model. Keep each `spec.yaml` on one grammar.

What changed in v2:

| v1                                          | v2                                       |
| ------------------------------------------- | ---------------------------------------- |
| `credentials.sources.<id>`                  | `credentials:` list entry with `service` |
| `network.allowedDomains` / `deniedDomains`  | `permissions.network.allow` / `deny`     |
| `network.serviceDomains` / `serviceAuth`    | `credentials[].apiKey.inject`            |
| `network.publishedPorts` / `publishedPorts` | top-level `ports`                        |
| standalone `oauth:` block                   | `credentials[].oauth`                    |
| `oauth.skipIfEnv`                           | Accepted but ignored                     |
| `environment.proxyManaged`                  | `credentials[].apiKey.proxyManaged`      |
| `memory` / `agentContext`                   | `agentInstructions.content`              |
| `kind: agent` / `agent:` block              | `kind: sandbox` / `sandbox:` block       |
| `sandbox.aiFilename`                        | `agentInstructions.filename`             |
| `sandbox.entrypoint.run`                    | `sandbox.entrypoint`                     |
| `sandbox.entrypoint.args`                   | `sandbox.command.default`                |
| `sandbox.entrypoint.ttyArgs`                | `sandbox.command.interactive`            |
| `tmpfs:`                                    | `volumes:` entries with `type: tmpfs`    |
| `volumes:` (mapping form)                   | `volumes:` sequence (`- path: <path>`)   |
| `commands:` / `commands.initFiles`          | `setup:` / `setup.files`                 |
| `settings:` / `kitDir` / `persistence`      | Removed                                  |

Credential discovery also moved out of the kit in v2: a kit declares which
credentials it needs and how to inject them, but where each value comes from is
controlled by the user through
[credential bindings](/ai/sandboxes/configuration/credentials/#credential-bindings).

> [!NOTE]
> `mixins` and `sandbox.build` are accepted by the parser, but runtime support
> is pending. A kit that sets `sandbox.build` must also set `sandbox.image`.

## Top-level fields

```yaml
schemaVersion: "2"
kind: <mixin | sandbox>
name: <name>
version: <version>
displayName: <name>
description: <text>
sourceURL: <url>
licenses:
  - MIT
locked:
  - sandbox.image
security:
  privileged: false
args:
  channel:
    default: stable
    enum: [stable, beta]
```

| Field           | Required | Description                                                                                     |
| --------------- | -------- | ----------------------------------------------------------------------------------------------- |
| `schemaVersion` | Yes      | Spec schema version. Use `"2"` for this grammar.                                                |
| `kind`          | Yes      | `mixin` for kits that extend an agent; `sandbox` for kits that define one.                      |
| `name`          | Yes      | Unique identifier. Lowercase alphanumeric with hyphens, 1 to 64 characters.                     |
| `version`       | No       | Kit version.                                                                                    |
| `displayName`   | No       | Human-readable name.                                                                            |
| `description`   | No       | Short description.                                                                              |
| `sourceURL`     | No       | Source repository or documentation URL.                                                         |
| `licenses`      | No       | SPDX license identifiers.                                                                       |
| `locked`        | No       | Dotted paths child kits may not override.                                                       |
| `security`      | No       | Container security settings. `security.privileged: true` runs the container in privileged mode. |
| `args`          | No       | Arguments supplied when the kit is loaded. Schema v2 only.                                      |

A kit also declares behavior blocks such as `agentInstructions`,
`permissions`, `ports`, `credentials`, `environment`, `setup`, and `volumes`.

## Arguments

A schema v2 kit can declare arguments and reference them anywhere in
`spec.yaml` or under `files/` as `${{ kit.args.<name> }}`. Substitution happens
before the spec is decoded.

```yaml
args:
  version:
    default: latest
    description: Tool version to install
    pattern: '^(latest|[0-9]+\.[0-9]+\.[0-9]+)$'
  channel:
    default: stable
    enum: [stable, beta, nightly]
  target:
    required: true
    description: Build target

environment:
  variables:
    TOOL_VERSION: "${{ kit.args.version }}"
```

Don't use kit arguments for API tokens, passwords, or other secrets. Use
[Credentials](/ai/sandboxes/configuration/credentials/) to provide sensitive values to
a sandbox.

| Field         | Description                                                                                                  |
| ------------- | ------------------------------------------------------------------------------------------------------------ |
| Argument name | Starts with a letter or underscore and contains only letters, digits, underscores, and hyphens.             |
| `default`     | String to use when the caller supplies no value. Mutually exclusive with `required: true`.                   |
| `required`    | Set to `true` when the caller must supply a value. Mutually exclusive with `default`.                        |
| `description` | Optional help text shown when a required value is missing.                                                   |
| `enum`        | Optional list of accepted values. Mutually exclusive with `pattern`.                                         |
| `pattern`     | Optional Go RE2 regular expression matched against the complete value. Mutually exclusive with `enum`.       |

Each argument must declare either `default`, including an empty-string
default, or `required: true`. A declared default must satisfy its own `enum` or
`pattern`. Every `${{ kit.args.<name> }}` reference must have a matching
declaration.

Argument values are strings, but substitution happens before YAML decoding.
Quote a placeholder in a string-valued field so a value such as `1.20` isn't
decoded as a number.

Supply values with `--kit-arg` or `--kit-args-file` when loading the kit. See
[Pass arguments to kits](/ai/sandboxes/customize/kit-reference/kits/#pass-arguments-to-kits) for scoping,
precedence, and validation behavior.

## Kit kinds

### `kind: mixin`

A mixin layers capabilities onto an existing sandbox. It must not declare a
`sandbox:` block, `extends:`, or `mixins:`. A mixin can declare `requires:` to
pin the base agent it is designed for:

```yaml
schemaVersion: "2"
kind: mixin
name: github-tools
requires:
  agent: claude
```

`requires.agent` takes one base-agent name. It is validated as a kit name and
enforced during composition.

### `kind: sandbox`

A sandbox kit defines a full agent. A root sandbox must declare a `sandbox:`
block. A sandbox that uses `extends:` can inherit the parent image and omit its
own `sandbox:` block:

```yaml
schemaVersion: "2"
kind: sandbox
name: claude-safe
extends: claude
```

`extends:` is sandbox-only. The parent must resolve to a sandbox kit. `mixins:`
is also sandbox-only and accepted by the parser, but runtime composition support
is pending.

## Sandbox block

```yaml
sandbox:
  image: <image-ref>
  build:
    context: .
    dockerfile: Dockerfile
    args:
      AGENT_VERSION: "1.0.0"
    target: runtime
    platforms:
      - linux/amd64
  entrypoint: [my-agent, "--flag"]
  command:
    default: ["--task-mode"]
    interactive: []
  resources:
    cpu: 2
    memory: 4g
    gpu: "1"
```

| Field                | Required | Description                                                                                                     |
| -------------------- | -------- | --------------------------------------------------------------------------------------------------------------- |
| `sandbox.image`      | When `extends:` is omitted | Docker image reference.                                                                                         |
| `sandbox.build`      | No       | Build configuration. Runtime support is pending, so a kit with `build:` must also set `image:`.                 |
| `sandbox.entrypoint` | No       | Fixed process prefix as a string array. The first element is the agent binary.                                  |
| `sandbox.command`    | No       | Mode-specific argument tail. Use a list shorthand for `default`, or a mapping with `default` and `interactive`. |
| `sandbox.resources`  | No       | Optional CPU, memory, and GPU constraints. Memory uses byte-size strings such as `4096m` or `4g`.               |

The effective command is `entrypoint` plus `command.default` for non-interactive
launches, and `entrypoint` plus `command.interactive` for TTY sessions. If
`interactive` is omitted, it falls back to `default`.

For a kit that uses `extends:`, `sandbox.command` replaces the full inherited
argument tail, including flags after the binary in the parent's
`sandbox.entrypoint`. It doesn't append to that tail. Define every argument the
child needs. For example, a child of `claude` that adds `--settings` must also
include `--dangerously-skip-permissions` to preserve that behavior.

The agent's container image must provide:

- A non-root `agent` user at UID 1000 with passwordless sudo.
- A `/home/agent/` home directory owned by `agent`.
- HTTP proxy environment variables (`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`) preserved across sudo.
- The agent binary, either baked in or installed with [`setup.install`](#setup).

Build on top of `docker/sandbox-templates:shell-docker` to get these base
requirements.

## Agent instructions

```yaml
agentInstructions:
  filename: CLAUDE.md
  content: |
    Ruff is installed. Run `ruff check` before committing.
```

| Field      | Description                                                                                         |
| ---------- | --------------------------------------------------------------------------------------------------- |
| `filename` | AI profile filename. Meaningful for `kind: sandbox`; ignored with a warning for `kind: mixin`.      |
| `content`  | Markdown instructions. For a sandbox, inlined into the profile. For a mixin, written to kit memory. |

For mixins, the engine writes `content` to
`<dir-of-AI-file>/kits-memory/<kit-name>.md` and adds a `## Kits` pointer
section to the base AI file. This keeps each mixin's instructions in a separate
file.

## Credentials

A kit declares the credentials it needs and how the proxy injects them into
outbound requests. It does not declare a host discovery source. The user
provides the value through the secret store or the first-run prompt, and a
[credential binding](/ai/sandboxes/configuration/credentials/) authorizes its use. A kit
can't read arbitrary host environment variables or files.

```yaml
credentials:
  - service: <service-id>
    description: <text> # optional
    required: <true | false> # optional, default false
    provider: <provider> # optional, reserved
    apiKey:
      name: <ENV_VAR>
      proxyManaged: true
      inject:
        - domain: <domain>
          header: <header>
          format: <format>
        - domain: <domain>
          scheme: bearer
        - domain: <domain>
          scheme: basic
          username: <user> # required with scheme: basic
    oauth:
      tokenEndpoint:
        host: <host>
        path: <path>
      sentinels:
        accessToken: <sentinel>
        refreshToken: <sentinel>
      credentialFile:
        path: <path>
        structure:
          <key>:
            accessToken: "{{.AccessToken}}"
            refreshToken: "{{.RefreshToken}}"
            expiresAt: "{{.ExpiresAt}}"
            scopes: "{{.Scopes}}"
```

`credentials` is a list; each entry names a `service` and configures one or more
auth mechanisms.

| Field         | Description                                                                                                                                 |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `service`     | Credential identifier, matched against the value stored with `sbx secret set`. Lowercase kebab-case.                                        |
| `description` | Optional. Shown to the user when approving a [binding](/ai/sandboxes/configuration/credentials/#credential-bindings).                                     |
| `required`    | Marks the credential as essential to the agent. If it has no binding, `sbx` warns and starts with the credential withheld. Default `false`. |
| `provider`    | Reserved for a provider registry. Accepted with a warning and no runtime effect.                                                            |
| `apiKey`      | API-key injection (see [apiKey](#apikey)).                                                                                                  |
| `oauth`       | OAuth interception (see [oauth](#oauth)).                                                                                                   |

Each service must declare `apiKey`, `oauth`, or both. When both resolve at
runtime, the API key takes precedence and OAuth acts as the fallback.

### `apiKey`

| Field               | Description                                                                                                                                       |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`              | Environment variable name for the credential (for example, `ANTHROPIC_API_KEY`).                                                                  |
| `proxyManaged`      | If `true`, `sbx` sets `name` inside the container to the `proxy-managed` sentinel. Default `false`.                                               |
| `inject[].domain`   | Domain to inject the credential into. Must also be allowed in [`permissions.network`](#network).                                                  |
| `inject[].header`   | HTTP header the proxy sets (for example, `x-api-key`, `Authorization`).                                                                           |
| `inject[].format`   | Header value format, with one `%s` placeholder (for example, `"%s"` or `"Bearer %s"`). Mutually exclusive with `scheme`.                          |
| `inject[].scheme`   | Shorthand for common auth schemes. `bearer` expands to `Authorization: Bearer %s`; `basic` requires `username`. Mutually exclusive with `format`. |
| `inject[].username` | Username for HTTP Basic auth, for example `x-access-token` for Git over HTTPS.                                                                    |

### `oauth`

For agents that authenticate with OAuth (for example, Claude Code), the proxy
intercepts token responses and replaces real tokens with sentinels, then swaps
the real token back in on outbound requests. By default, the token never enters
the sandbox. Setting `passthrough: true` opts out of sentinel masking and sends
the real token response into the sandbox.

| Field                                    | Description                                                                                                                                                                                       |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tokenEndpoint.host` / `path`            | The OAuth token endpoint the proxy intercepts.                                                                                                                                                    |
| `sentinels.accessToken` / `refreshToken` | Sentinel values written into the container in place of the real tokens.                                                                                                                           |
| `credentialFile.path`                    | Where to write the credential file inside the container (`~` expands).                                                                                                                            |
| `credentialFile.structure`               | Declarative JSON shape. Supports `{{.AccessToken}}`, `{{.RefreshToken}}`, `{{.ExpiresAt}}`, and `{{.Scopes}}`.                                                                                   |
| `credentialFile.template`                | Go template. Supports `{{.AccessToken}}`, `{{.RefreshToken}}`, `{{.ExpiresAt}}`, `{{.Scopes}}`, and `{{.ScopesJSON}}`.                                                                          |
| `resourceHosts`                          | API hosts where the proxy attaches the token on outbound requests, distinct from the token endpoint host.                                                                                         |
| `skipIfEnv`                              | Accepted for compatibility, but ignored for schema v2. A v2 binding is authoritative instead of host environment variables.                                                                       |
| `responseFields`                         | Overrides the default field names the proxy reads from the token response.                                                                                                                        |
| `passthrough`                            | If `true`, the proxy passes the token response through unchanged instead of replacing the tokens with sentinels.                                                                                  |

`credentialFile.structure` provides a declarative alternative to
`credentialFile.template`. The engine renders it as well-formed JSON. If both
fields are set, `structure` takes precedence.

## Network

Network egress is declared under `permissions.network`. Credentials no longer carry
their own domain mapping — the proxy injects a credential only into the domains
its [`apiKey.inject`](#apikey) lists, and every domain the
sandbox reaches must be allowed here.

```yaml
permissions:
  network:
    allow: [<domain>, ...]
    deny: [<domain>, ...]
```

| Field                       | Description                                                                                                     |
| --------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `permissions.network.allow` | Domains the sandbox can reach.                                                                                  |
| `permissions.network.deny`  | Domains the sandbox is blocked from reaching. Deny takes precedence over allow, including across composed kits. |

Allow and deny patterns:

| Pattern               | Example                  | Status                      |
| --------------------- | ------------------------ | --------------------------- |
| Exact host            | `api.example.com`        | Enforced                    |
| Exact host and port   | `api.example.com:8080`   | Enforced                    |
| Single-label wildcard | `*.example.com`          | Enforced                    |
| Multi-label wildcard  | `**.example.com`         | Parsed; enforcement pending |
| Port range            | `api.example.com:80-443` | Parsed; enforcement pending |
| Port wildcard         | `api.example.com:*`      | Parsed; enforcement pending |
| CIDR                  | `10.0.0.0/8`             | Parsed; enforcement pending |

In v1 this was the `network:` block (`allowedDomains` / `deniedDomains`, plus
`serviceDomains` / `serviceAuth`). In v2, those fields are decode errors.

## Ports

Use `ports` to expose sandbox services to the host:

```yaml
ports:
  - container: 8080
    name: web
```

| Field       | Description                                                         |
| ----------- | ------------------------------------------------------------------- |
| `container` | Container port, 1 to 65535.                                         |
| `protocol`  | `tcp` or `udp`. Empty publishes one family; see below.              |
| `name`      | Optional label surfaced by tools that list published port bindings. |

Host ports are allocated ephemerally. Leave `protocol` empty unless the service
listens on IPv6: an empty value publishes IPv4 only (`127.0.0.1`), which is what
a service bound to `0.0.0.0` needs, while `tcp` publishes both `127.0.0.1` and
`::1` — and a client arriving over `::1` is accepted and then reset if nothing
in the sandbox is listening there. Users can pin host ports with
`sbx ports --publish <host>:<container>`.

## Environment

```yaml
environment:
  variables:
    <NAME>: <value>
```

| Field       | Description                                    |
| ----------- | ---------------------------------------------- |
| `variables` | Key-value pairs set directly in the container. |

Variable names must be valid shell identifiers (`[A-Za-z_][A-Za-z0-9_]*`).

Do not set `DASH_`, `SBX_`, or `DOCKER_` variables, and avoid overriding
`HOME`, `USER`, `SHELL`, `PATH`, `LD_PRELOAD`, and `LD_LIBRARY_PATH`. The
runtime reserves these names and may override them.

## Setup

```yaml
setup:
  install:
    - command: <shell-string>
      user: <uid>
      description: <text>
  startup:
    - command: [<argv>, ...]
      user: <uid>
      background: <true | false>
      description: <text>
  files:
    - path: <path>
      content: <text>
      mode: <octal>
      onlyIfMissing: <true | false>
      description: <text>
```

### Execution order

When a sandbox is created, kit content is applied in this order:

1. Network permissions and environment variables.
2. Static files under `files/home/`.
3. `setup.install` commands, in declaration order.
4. `setup.files` entries.
5. `setup.startup` commands are registered for each sandbox start.
6. Static files under `files/workspace/`, after the workspace is ready. With
   `--clone`, this means after the repository has been cloned.

For stacked kits, entries in each stage are applied in `--kit` order. An install
command can consume a bundled file from `files/home/`, but not one from
`files/workspace/` or `setup.files`, because those files land later.

`sbx kit add` recreates the sandbox rather than modifying it in place. It
supports mixin kits limited to
`environment.variables`, `setup.install`, and `permissions.network.allow`,
which follow the same order as sandbox creation. It rejects a kit that declares
static files, `setup.startup`, or `setup.files`. To use those fields, recreate
the sandbox with the kit.

### install

Runs synchronously when a kit is applied, either during sandbox creation or
through `sbx kit add`. Shell strings are passed to `sh -c`.

Kit install commands start in the template image's configured `WORKDIR`.
Docker-provided templates use `/home/agent/workspace`, which isn't necessarily
the primary workspace in a direct-mounted or clone-mode sandbox. Don't rely on
the current directory to locate workspace files. Use absolute paths for bundled
assets from `files/home/`.

| Field         | Default | Description                   |
| ------------- | ------- | ----------------------------- |
| `command`     | —       | Shell command string.         |
| `user`        | `"0"`   | User to run as. `"0"` = root. |
| `description` | —       | Human-readable description.   |

### startup

Runs at every sandbox start. String array, not interpreted by a shell.

| Field         | Default  | Description                         |
| ------------- | -------- | ----------------------------------- |
| `command`     | —        | Command and args as a string array. |
| `user`        | `"1000"` | User to run as. `"1000"` = agent.   |
| `background`  | `false`  | Block later startup commands until this command finishes. Set to `true` to let later commands run without waiting. |
| `description` | —        | Human-readable description.         |

Startup commands are non-interactive. They run before the agent
attaches, with no terminal connected, so they can't prompt the user
(for example, an interactive `aws login` will hang or fail). They also
don't gate the agent's entrypoint: the agent launches once startup
commands have been dispatched, regardless of `background`. A value of
`false` waits within the startup dispatcher before it runs the next command;
it doesn't delay the agent entrypoint. Use startup commands
for work that can run alongside the agent. Use `setup.files` for any value that
needs to land on disk before the agent runs.

Startup commands must be idempotent. They run on every sandbox start
and replay on container restarts, so a command that fails or
misbehaves on a second invocation breaks the restart path. Guard
work with existence checks, use upserts instead of inserts, and
prefer commands that converge to the same end state regardless of
how many times they run.

### files

Files written at sandbox start, with runtime substitution.

| Field           | Default  | Description                                               |
| --------------- | -------- | --------------------------------------------------------- |
| `path`          | —        | Absolute container path.                                  |
| `content`       | —        | File content. `${WORKDIR}` expands to the workspace path. |
| `mode`          | `"0644"` | File permissions in octal.                                |
| `onlyIfMissing` | `false`  | Skip if the file already exists.                          |

The runtime writes these files as the agent user with UID 1000. The target
path must be writable by that user. To write to a root-owned path such as
`/etc`, use an `install` command, which runs as root by default. Set ownership
in the install command if the agent needs to modify the file later.

## Static files

```text
my-kit/files/
├── home/       → /home/agent/
└── workspace/  → primary workspace path
```

| Kit path           | Container destination                   |
| ------------------ | --------------------------------------- |
| `files/home/`      | `/home/agent/` (config files, dotfiles) |
| `files/workspace/` | The primary workspace path              |

Parent directories are created automatically. Existing files are
overwritten. Absolute paths and path-traversal sequences (`../../`) are
rejected.

## Volumes

```yaml
volumes:
  - path: /workspace
    size: 10g
    mode: "0755"
  - path: /tmp/scratch
    type: tmpfs
    size: 512m
    mode: "1777"
```

| Field  | Description                                                         |
| ------ | ------------------------------------------------------------------- |
| `path` | Required absolute container path.                                   |
| `type` | Empty for a block-backed volume, or `tmpfs` for RAM-backed storage. |
| `size` | Optional byte-size string.                                          |
| `mode` | Optional octal permissions.                                         |

Volumes are applied only when a sandbox is created. `sbx kit add` cannot attach
volumes to a running container.

