Guide

Working with secrets with Docker Sandboxes

  • Beginner
  • 15 min
  • Published
  • Verified

What you'll learn

  • Where credentials are stored and accessible
  • Configure secrets for built-in and custom services (using kits)
  • Learn about the credential bindings file that records what you have approved
  • Use registry credentials for pulling private template and kit images

The terminals below are simulated. Nothing runs on your machine, and each one starts from the state the guide has reached at that point.

An AI coding agent is not much use without credentials. It needs a model API key to think, a GitHub token to open a pull request, and often a token for some service your own team runs. But handing an autonomous agent your credentials is exactly the thing that makes people hesitate to run one at all.

Docker Sandboxes answer that by never giving the agent the credential at all. Your secrets stay on your host, in your operating system’s own credential store. A host-side proxy sits on the sandbox’s only route out and attaches the right credential to matching requests as they leave. Inside the sandbox, the token is a placeholder that authenticates nothing (if it exists at all).

The path one request takes out of a sandbox Inside the sandbox, curl runs with a placeholder GH_TOKEN and the request crosses the VM boundary carrying no usable token. On the host, the proxy matches the domain to the built-in github service, reads the real token from the OS keychain, and writes/overrides the Authorization header. Only then does the request reach api.github.com. Inside the sandbox $ curl https://api.github.com/user GH_TOKEN=gho_sbxproxymanaged000… VM boundary 1 · leaves with no usable token Host-side proxy · on your host matches api.github.com to the github service reads the real token from your OS keychain writes/overrides the Authorization header 2 · authenticated, and the token never moved api.github.com 200 OK · authenticated as moby

This guide will teach you about the different forms of credentials and help you learn how to use each one.

Services

A credential is only useful if it’s connected to a service. Simply put, a service is a network endpoint that will use a credential. Examples might be GitHub, Anthropic, or your own company-specific API.

Sandboxes support several built-in services, such as anthropic and google. Custom services can be configured by using kits.

Beyond that, there are also registry credentials, which are used to authenticate to container registries when pulling templates and kits and can be leveraged by the sandbox itself.

Where your credentials live

The credentials are stored securely by your operating system’s credential manager. Specifically:

Host Where the value actually lives
macOS System Keychain
Windows Windows Credential Manager
Linux Secret Service (GNOME Keyring, KDE Wallet)
Headless Linux, and some WSL setups ~/.config/com.docker.sandboxes, mode 0700

To list the stored secrets, use the sbx secret ls command:

Host machine

sbx secret ls
SCOPE      TYPE      NAME        SECRET
(global)   service   anthropic   (stored)

A few things to note about the secrets based on the output:

  • SCOPE — (global) means every sandbox may use it. A secret can also be scoped to one sandbox.
  • TYPE — the two types are service and registry
  • NAME — the service identifier
  • SECRET — (stored), never the value. There is no sbx secret get.

Note that there is no mechanism to retrieve the secret value itself.

Store a secret

To store a secret, use the sbx secret set command. If you don’t pass the value through stdin, a prompt will ask for the value, ensuring it doesn’t appear in your command history:

Host machine

sbx secret set github
Applied global secret to existing sandboxes
Saved secret github (global)

A global secret applies to every sandbox, including ones that already exist. The Applied global secret to existing sandboxes line appears only when you have some.

You can also pipe the secret through stdin. Because github is already stored from the previous step, add --force to replace it. Without it, sbx asks for confirmation, which it can’t do while stdin is a pipe, so it stops with an error:

Host machine

gh auth token | sbx secret set github --force
Enter secret: Applied global secret to existing sandboxes
Saved secret github (global)

If you want to set a secret for only a specific sandbox, add the --sandbox flag to the command:

Host machine

sbx secret set --sandbox my-sandbox github
Applied secret to sandbox my-sandbox
Saved secret github (my-sandbox)

Store the instruction, not the value

If a credential already lives somewhere in a vault or another tool (such as 1Password, AWS Secrets Manager, or a CLI), you can configure sbx with a command on how to retrieve it with the --command flag:

Host machine

sbx secret set github --command 'gh auth token' --refresh 30m
✓ command resolved successfully
Applied global secret to existing sandboxes
Saved command secret github (global)

sbx now runs gh auth token when it needs the token and re-runs it every 30 minutes, so a rotated token stays current and there is no stale second copy to leak. --refresh on-demand re-runs it per request instead.

Notably, github reads (stored) in the listing, just as the plain value did. sbx secret ls does not currently distinguish a stored value from a stored command. The Saved command secret line is your confirmation.

Host machine

sbx secret ls
SCOPE        TYPE      NAME        SECRET
(global)     service   anthropic   (stored)
(global)     service   github      (stored)
my-sandbox   service   github      (stored)

For a secret manager, store a reference. --ref reads 1Password op:// references with the op CLI and AWS Secrets Manager ARNs with the aws CLI, which must be installed and signed in. sbx resolves the reference once when you store it, and refuses to store it if that fails:

$ sbx secret set anthropic --ref 'op://Work/Anthropic/credential'
$ sbx secret set openai --ref 'arn:aws:secretsmanager:us-west-2:111122223333:secret:openai-api-key'

What the sandbox can see

What the agent can see inside the sandbox can vary. Sometimes, a fake credential is configured to prevent CLIs and other tools from thinking a credential needs to be configured. Other times, no credential is configured at all.

For example, take a look at the GitHub token inside the sandbox:

Inside the sandbox

printenv GH_TOKEN
gho_sbxproxymanaged000000000000000000000

There is a value, but it is worthless. Look at its shape: a gho_ prefix and then padding. That is deliberate. Plenty of tools check the format of a token before they will start, and some refuse to run without the variable set at all. Therefore, sbx supplies something that passes those checks and carries no secret.

If you attempt to get information about the current user, it’ll work as you expect:

Inside the sandbox

curl https://api.github.com/user
{"login":"moby","id":10000001,"type":"User","name":"Moby Dock","company":"@docker","public_repos":42,"created_at":"2013-03-20T00:00:00Z"}

A kit brings its own service

Built-in services cover the model providers and GitHub. Everything else, including the APIs your own company runs, needs to be defined using a kit.

A kit is a directory holding a descriptor file named after the directory. The following kit defines a new service called example-tickets, which will inject credentials for requests going to api.tickets.example.com. Create it as kits/example-tickets-api/example-tickets-api.yaml:

# syntax=docker/sandbox-kit:3
schemaVersion: "3"
kind: mixin
displayName: Example tickets API
description: Access to the example tickets API, with a proxy-managed token

capabilities:
  - type: com.docker.sandbox/network-policy@1
    config:
      runtime:
        allow:
          - api.tickets.example.com

  - type: com.docker.sandbox/credential@1
    description: Example tickets API token
    config:
      service: example-tickets
      phase: runtime
      apiKey:
        name: EXAMPLE_TICKETS_TOKEN
        proxyManaged: true
        inject:
          - domain: api.tickets.example.com
            header: Authorization
            format: "Bearer %s"

The credential@1 capability is the part that matters here. The network-policy@1 capability lets the sandbox reach the host at all: sbx refuses a kit that injects a credential for a domain its network policy doesn’t allow.

Read the credential as a contract between three parties:

  • the kit declares the match (domain) and the header (header and format), and no value
  • you supply the value, by defining a secret for the example-tickets service
  • the proxy brings the two together, per request

Nothing in that file is a secret, which is what makes a kit safe to commit and hand to a teammate.

Start a new sandbox with Claude Code and the kit:

Host

sbx create --name sandbox-with-kit docker/sbx-kit-claude --kit ./kits/example-tickets-api
KITS
  ○ ./kits/example-tickets-api — building…
  ✓ ./kits/example-tickets-api — built (0s)
      sbx-kit-src:example-tickets-api-8d4188f631be

This kit wants to use these credentials:
  example-tickets:  API key sent to api.tickets.example.com (not set)
Approve these credentials?
  1. Approve all
  2. Review each
  3. No   start without them
Choice [1-3, default 3]: 1
  ✓ Binding saved to /home/moby/.config/sbx/credentials.yaml

SANDBOX
  Name:       sandbox-with-kit
  Agent:      sbx-kit-claude
  Kits:       docker/sbx-kit-claude
              ./kits/example-tickets-api
  Workspace:  none · no workspace bind mount
  Skills:     0 entries · read-only
    Source:   ~/.local/state/sandboxes/sandboxes/agent-skills
    Target:   /home/agent/.claude/skills
  Image:      docker.io/docker/sbx-kit-claude@sha256:c0132e89a89d94ddca3984513afdb36be57bd087fcc75823a4d14064b6a39ea5
  CPU:        4
  Memory:     8 GiB

IMAGE
  ✓ image — ready
WARN: no credential resolved for required service; sandbox will start without it
WARN: required credential not found for service

AGENT
    assembling kit image from 2 kits
    pulling kit image /home/moby/kits/example-tickets-api
  ✓ assembled kit image (11 layers)
    setting 3 environment variables
    running 4 install commands
      ✓ env -i PATH="$PATH" HOME="$HOME" USER="$USER" TERM="$TERM" … (kit=sbx-kit-claude, user=0, 266ms)
      ✓ env -i PATH="$PATH" HOME="$HOME" USER="$USER" TERM="$TERM" … (kit=sbx-kit-claude, user=0, 98ms)
      ✓ env -i PATH="$PATH" HOME="$HOME" USER="$USER" TERM="$TERM" … (kit=sbx-kit-claude, user=agent, 45ms)
      ✓ env -i PATH="$PATH" HOME="$HOME" USER="$USER" TERM="$TERM" … (kit=sbx-kit-claude, user=agent, 6.9s)
    registering 3 startup commands, run on every container start
      ○ sh -c chown -R agent:agent /home/agent/.claude/projects /ho… (kit=sbx-kit-claude, user=0)
      ○ sh -c command -v apt-get > /dev/null 2>&1 && (apt-get updat… (kit=sbx-kit-claude, background, user=0)
      ○ sh -c set -e [ -n "$MCP_GATEWAY_URL" ] || exit 0 export PAT… (kit=sbx-kit-claude, user=agent)
      view startup log: cat /var/log/sbx-kit-startup.log

  ✓ Created sandbox sandbox-with-kit
      try: sbx run --name sandbox-with-kit

Before it creates the sandbox, sbx asks you to approve the kit’s use of the example-tickets credential, and on which domain. Choose Approve all. The secret isn’t stored yet, so the prompt says (not set), and sbx warns that the sandbox starts without it.

Try accessing the API from inside the sandbox:

Host machine · no credential stored yet

sbx exec sandbox-with-kit -- curl https://api.tickets.example.com/v1/tickets
{"error":{"code":"unauthorized","message":"Missing bearer token"}}

The call is rejected. The proxy recognises the domain from the kit’s declaration, but no credential is injected because there is no stored value for example-tickets yet.

Set the secret, this time using a 1Password reference:

Host machine

sbx secret set example-tickets --ref 'op://Engineering/example-tickets/token'
✓ ref resolved successfully
Applied global secret to existing sandboxes
  Captured resolver environment:  <none>
Saved ref secret example-tickets (global)

The secret is now stored! And the request should now work.

Host machine

sbx exec sandbox-with-kit -- curl https://api.tickets.example.com/v1/tickets
{"data":[{"id":"TIX-418","title":"Flaky test in the checkout suite","status":"open"},{"id":"TIX-421","title":"Upgrade the base image","status":"in_progress"}],"total":2}

What the kit is allowed to do

A kit that declares a credential injection is asking for something specific: permission to have one of your credentials attached to requests bound for a domain it names. You granted that permission when you chose Approve all, before the secret existed. Storing the secret does not grant it — it is tracked separately, in a file you can read:

Host machine (Mac/Linux)

cat ~/.config/sbx/credentials.yaml
bindings:
    anthropic:
        apiKey:
            domains:
                - api.anthropic.com
                - mcp-proxy.anthropic.com
        oauth:
            domains:
                - platform.claude.com
    example-tickets:
        apiKey:
            domains:
                - api.tickets.example.com

That file — ~/.config/sbx/credentials.yaml, or %APPDATA%\sbx\credentials.yaml on Windows — lists the mechanisms and domains you have approved, per service. The built-in kits shipped with sbx, such as the one sbx run claude uses, need no binding. Any other kit does, including Docker’s published docker/sbx-kit-claude, so its declared injection has to be bound here before the proxy will honour it. The anthropic entry is there because you approved that kit’s use of your Anthropic key the first time you ran it. The binding for example-tickets covers exactly one host. If that kit later updates and starts asking for a second domain, the binding it already has does not cover the new one, and sbx asks you to approve it again.

It is worth reading occasionally for exactly that reason. It answers “what have I allowed, and for which hosts?” on one screen.

That is the whole design. The sandbox is a consumer of authenticated access, never a holder of credentials.

Credentials for pulling images

Service secrets answer “how does the agent call an API?”. Registry credentials answer a different question: how does sbx pull a private sandbox template or kit artifact in the first place before there is a sandbox to inject anything into?

These secrets use the same command, but add the --registry flag:

Host machine

gh auth token | sbx secret set --registry ghcr.io --username moby --password-stdin
Saved registry secret ghcr.io (host only, username moby)
sbx secret ls
SCOPE         TYPE       NAME              SECRET
(global)      service    anthropic         (stored)
(global)      service    example-tickets   (stored)
(global)      service    github            (stored)
my-sandbox    service    github            (stored)
(host only)   registry   ghcr.io           moby/gho_FA******...******4444

Unlike a service secret, a registry credential’s SECRET column shows the username and a masked form of the token: its first and last few characters.

Registry credentials are, by default, scoped to only the sbx tool to pull templates and kits. To share the credential with sandboxes, scope it in deliberately:

# Host only
$ gh auth token | sbx secret set --registry ghcr.io --password-stdin

# One specific sandbox
$ gh auth token | sbx secret set --sandbox my-app --registry ghcr.io --password-stdin

# Every sandbox
$ gh auth token | sbx secret set --all-sandboxes --registry ghcr.io --password-stdin

Recap

There are three kinds of credential, plus one way to avoid keeping a copy at all:

What Configured with Where the value lives Reaches the sandbox?
Built-in service sbx secret set github OS credential store No — header injected
Kit-declared service sbx secret set example-tickets OS credential store No — header injected
Either, dynamically --command '…' or --ref '…' Wherever it already lives No — header injected
Registry credential sbx secret set --registry … --password-stdin OS credential store No, unless scoped in

Worth carrying forward:

  • The sandbox holds no credentials. If there is a “credential” in the sandbox, it is only a placeholder. Resist the urge to use .env files or export environment variables in the sandbox.
  • A kit’s declaration is public, its value is yours. The kit names the domain and the header. You supply the secret. Nothing secret goes in the kit.
  • Permission and value are separate. A stored secret is not an approval. The bindings file is what says a third-party kit may have your credential attached to a given host.
  • Scope narrowly. --sandbox over global, and global over --all-sandboxes.