Using docker sbx to run Claude Code sandboxes¶
This is an experiment, not settled practice
Our use of Docker Sandboxes is an active trial by the DevOps Tech Lead Forum, and this tutorial
is an early draft of it. The sbx env and sbx kit commands it relies on are marked
experimental by the CLI itself, so they can change or be removed in any release, and a future
version of sbx may break the files you create here.
Don't build team tooling or process on this yet, and expect to redo the set-up at least once. Please tell us what you find. Create an issue in the guidebook project, or open a merge request if you have suggestions.
In this tutorial we're going to go through installing Docker
sandboxes (the sbx CLI) on macOS and using it to run
Claude Code inside an isolated microVM rather than directly on your machine. This tutorial covers
macOS only; sbx also supports other platforms but this guide doesn't go into that.
By the end of this tutorial we'll have:
- installed the
sbxCLI, - started a Claude Code session inside a sandbox,
- logged in to Claude for the first time,
- watched a request get blocked by the network policy and allowed the domains we need,
- moved that configuration into the project so everyone working on it gets the same sandbox,
- imported our Claude Code skills into the sandbox, and
- learned how to list, stop and remove sandboxes.
Prerequisites¶
- A Mac with Apple silicon. Intel Macs aren't supported, as Docker's installation requirements set out.
- macOS Sonoma (14) or later.
- Homebrew, used to install the CLI in the next section.
- A Docker account, used to sign in to
sbx. - A Claude account on your University email address, used to sign in to Claude from inside the sandbox.
Installing docker sbx¶
Install the sbx CLI via Homebrew:
brew trust docker/tap
brew install docker/tap/sbx
Then sign in with your Docker account:
sbx login
This opens a browser to sign in via Docker's own OAuth flow. Signing in ties sandboxes you create to your identity.
Two different logins
sbx login signs you in to Docker. Later in this tutorial we'll also run /login inside a
Claude Code session to sign in to Claude as a one-time step covered in Logging in to
Claude.
Starting a Claude Code session¶
cd into the project you want to work on, then start a sandboxed Claude Code session:
cd ~/my-project
sbx run claude
This boots a microVM, mounts your project's working tree into it, and starts Claude Code running
inside that isolated environment. The sandbox is named after the directory, so ~/my-project gives
you claude-my-project. Pass --name if you'd rather choose the name yourself.
Claude runs without permission prompts
Inside a sandbox sbx starts Claude as claude --dangerously-skip-permissions, so it won't
stop to ask before editing a file or running a command. That is deliberate. The sandbox is the
security boundary here rather than Claude's own prompts, and running the agent this way is the
point of putting it in one. Docker covers how your own arguments interact with that default
under default startup
command.
You can still change mode from inside the session. Shift+Tab cycles through the permission
modes, so you can drop into plan mode when you want Claude to think something through before it
touches anything.
Always start with the Balanced profile
The first time you use sbx on a machine it asks you to initialise a global network policy. Always pick Balanced. It permits traffic to common development services while blocking everything else, which is the right default for day-to-day work.
Mounting more than one project
By default only the project you cd'd into is mounted. If Claude also needs to see a second
project, for example a shared library in a separate repo, pass its path as an extra argument:
sbx run claude ~/my-project ~/shared-library:ro
Append :ro to mount a workspace read-only. Only mount what the sandbox actually needs, since
every extra workspace widens what the agent can read or change.
Logging in to Claude¶
The first time you start a Claude Code session in a sandbox, you'll need to sign in. Inside the running session, run:
/login
This opens a browser OAuth flow. Sign in using the Claude account provisioned to your university email address.
You shouldn't need to do this again
The session token from /login is stored on your host, never inside the sandbox itself.
Future sandboxes on the same machine reuse it automatically, so this is a one-time step per
machine rather than something you repeat for every sandbox.
Allowing network access¶
Balanced blocks everything it doesn't recognise, and that includes our own GitLab. Ask Claude to pull one of our base images:
Pull registry.gitlab.developers.cam.ac.uk/uis/devops/platform/base-images/python:3.14-debian13
The pull fails. Exactly how Claude reports that varies from run to run, and the registry's own
error is a bare 403 Forbidden, which looks the same whether the host is blocked or the image
needs authentication. The policy log settles it:
sbx policy log
Blocked requests:
SANDBOX HOST REASON
claude-my-project registry.gitlab.developers.cam.ac.uk:443 No matching allow rule (default deny)
The real output carries a few more columns, but that row is the whole story. The request never left the sandbox, so authentication never came into it.
To fix this you need to run the following command from a second terminal on your host. sbx isn't
on the path inside the sandbox and the agent has no route to the daemon, so Claude can't widen its
own network policy itself, even if you ask it to:
sbx policy allow network gitlab.developers.cam.ac.uk,registry.gitlab.developers.cam.ac.uk
These cover docker pull and docker push against images in our GitLab container registry, and
git operations (clone, fetch, push) against the GitLab instance itself. The change takes effect
immediately, so ask Claude to retry the pull in the session you already have open and it should
succeed.
That works, and for a one-off domain it's often all you need. It's worth seeing what it created though:
sbx policy inspect local-policy
RULE RULE_ID EDITABLE ACTION
- 66be0af6-0f56-4074-a2a3-e10175ed143a yes sbx policy rm network --id 66be0af6-...
- 0c2484bc-ef8b-4c4c-b0fb-7b27f20939d6 yes sbx policy rm network --id 0c2484bc-...
One command produced one rule per domain, each under a generated UUID rather than a name. The rules are global, so they apply to every sandbox on this machine rather than just the project that needed them. They also live only on your machine, so everyone else on the project has to work the same thing out and run the same command. That scales poorly as soon as more than one person is involved.
The next section moves the same configuration into a kit inside the project, so tidy up before you go on. Find the IDs of the two rules you just added, remove them, and remove the sandbox too:
sbx policy inspect local-policy
sbx policy rm network --id <rule-id>
sbx rm claude-my-project
The sandbox goes because a kit is only applied when a sandbox is created, so the next section needs to build a fresh one. That takes our GitLab away again, which is the point. When the pull works at the end of the next section, you'll know the kit is what did it.
Moving that configuration into a kit in your project¶
Rather than configuring each machine by hand, you can describe the sandbox in two files checked into the project. Everyone who clones the repo then gets the same sandbox.
your-project/
├── sbxenv.yaml
└── .sbx/
└── uis-devops-baseline/
└── spec.yaml
sbxenv.yaml says which agent to run, what to mount, and which
kits to apply:
schemaVersion: "1"
agent: claude
workspace: .
kits:
- ./.sbx/uis-devops-baseline
A kit is a reusable bundle of sandbox configuration. Ours is a mixin, meaning it extends the
built-in Claude agent rather than replacing it, and for now all it does is widen the network
allowlist. Put the domains you allowed by hand a moment ago into
.sbx/uis-devops-baseline/spec.yaml:
schemaVersion: "2"
kind: mixin
name: uis-devops-baseline
version: 0.1.0
displayName: UIS DevOps baseline
description: Baseline sandbox set-up for UIS DevOps projects.
permissions:
network:
allow:
- gitlab.developers.cam.ac.uk:443
- registry.gitlab.developers.cam.ac.uk:443
Add further domains to the same list as and when a project needs them. Docker's kit spec reference covers the other things a kit can carry, including environment variables and setup commands.
Two files, two different schema versions
sbxenv.yaml is schemaVersion: "1" and the kit's spec.yaml is schemaVersion: "2". These
are separate schemas that happen to share a field name. Swapping them over fails with an
unsupported schemaVersion error.
The ./ at the start of the kit path matters. sbx resolves an explicit relative path against the
directory holding sbxenv.yaml, so the file keeps working wherever you run sbx from. A bare
.sbx/uis-devops-baseline is read as a registry reference instead.
Check the kit is valid before you use it:
sbx kit validate .sbx/uis-devops-baseline
Everything is in place, so let the environment build the sandbox:
sbx env run
The first time you run this, sbx shows an environment plan and asks you to approve it. The plan lists the kit it will apply, the workspace it will mount and the sandbox it will create, so you can see what the file is about to do before anything happens. Approve it and sbx boots the sandbox with the kit applied. Later runs reuse that sandbox and only ask again when the plan changes.
The kit's domains now appear in a policy of their own, scoped to that sandbox, leaving the global
local-policy alone:
sbx policy ls
POLICY SOURCE APPLIES TO SUMMARY
d5061445-22bc-46e4-8a2d-5d0f4f0f8641 kit sandbox:claude-my-project network: 8 allow
local-policy local all network: 192 allow; ...
That policy holds the domains from our kit alongside the ones the built-in Claude agent needs. Its
rules are managed by the kit and recreated each time the sandbox is created, so edit spec.yaml
and recreate the sandbox rather than trying to change them with sbx policy.
Ask Claude to pull that image once more to confirm the kit is doing the work:
Pull registry.gitlab.developers.cam.ac.uk/uis/devops/platform/base-images/python:3.14-debian13
For a domain a single project needs once, sbx policy allow network is still there as a quick
escape hatch, but anything worth keeping belongs in the kit, where it is version-controlled and
shared with everyone working on the project.
The kit sits in a directory the agent can write to
sbx binds sbxenv.yaml read-only inside a sandbox the environment created, but the kit's
spec.yaml is an ordinary file in your project tree and stays writable. An agent can therefore
widen its own network allowlist, and the change takes effect the next time the sandbox is
created. What contains the agent is the default-deny global policy and the microVM boundary,
not the kit. Treat an edit to .sbx/uis-devops-baseline/spec.yaml like any other change the
agent proposes and review it before it's merged.
Importing your Claude Code skills¶
If you have user-scoped Claude Code skills set up on your host, you can bring them into a sandbox with:
sbx skills import
Run with --dry-run first to preview what would be copied without actually copying anything.
Once you're happy with the preview, run the same command without the flag to perform the import.
This is a one-time copy, not a live mount
sbx skills import copies your skills into the sandbox's state at the time you run it. It is
not a live or synced mount. If you edit a skill on your host afterwards, a sandbox that's
already imported the old copy won't see the change. Re-run sbx skills import whenever you want
to refresh a specific sandbox with your latest skills.
Listing running sandboxes¶
To see which sandboxes are currently running:
sbx ls
Stopping and removing a sandbox¶
To pause a sandbox you're not using but want to come back to, stop it by name:
sbx stop claude-my-project
To permanently delete the sandbox your sbxenv.yaml describes, along with anything else the
environment created, run this from the project directory:
sbx env rm
sbx rm <name> still works for a sandbox you created with sbx run rather than from an
environment file. In both cases the name is the one shown by sbx ls.
Storing secrets outside the sandbox¶
This tutorial doesn't cover secrets, but there's a feature worth knowing about. sbx secret set
can store a reference to a secret rather than the secret itself, either a 1Password op://
reference or an AWS Secrets Manager ARN. sbx resolves the reference on your host when it's needed,
and the proxy authenticates the agent's requests, so the value never lands in the sandbox. It needs
the op CLI installed and signed in, and the same thing can be declared in sbxenv.yaml alongside
the kit we set up above.
We've been experimenting with this, and it ties in directly with our current work to make op
references the standard way of handling secrets in our webapp
boilerplate. We haven't gained enough confidence
in the features to document them here yet. Read Docker's guide to managing
credentials, have a play, and
report back to the Platforms Team to help steer how we use and implement this.
Summary¶
In this tutorial we installed sbx and got Claude Code running in a sandbox with a single command.
We hit the Balanced network policy blocking our own GitLab, allowed those domains by hand, and saw
that doing it that way leaves unnamed global rules that only exist on one machine. We then moved
the same configuration into an sbxenv.yaml and a uis-devops-baseline kit checked into the
project, so the allowlist is version-controlled, shared with everyone working on it, and scoped to
that sandbox rather than the whole machine. Along the way we logged in to Claude, imported our
skills, and saw how to list, stop and remove sandboxes.
See also¶
- Docker Sandboxes documentation, the official
sbxdocs - Sandbox environment
files, the reference for
the
sbxenv.yamlwe created here - Isolating agentic AI coding tools with Docker Sandboxes, why we're trialling this and what it does and doesn't protect against