This post follows on directly from my previous post, in which I describe how to run AI agents safely using the docker sandbox tool, sbx. In this post I describe how to create custom templates, so that your sandboxes start with additional tools. I show both how to add tools to the default template, and how to start with a different docker image and layer-on the docker sandbox tooling later.
An initial caveat to this post: I've been somewhat struggling to get my personal projects working in docker sandboxes, with .NET just hanging indefinitely during builds. This seems to be specific to my projects, as a hello world build is fine, but a word of caution: your mileage may vary.
Running agents safely in a docker sandbox
As I described in my previous post, working with AI agents in their default mode can mean an infuriating number of tool calls, that interrupt your flow, and generally slow you down:

However, ignoring these tool calls using the "bypass permissions" mode (AKA YOLO/dangerous mode) can be, well, dangerous. There's plenty of examples of AI agents going rogue; do you want to risk it? Docker Sandboxes provide one solution.
Docker sandboxes run in microVMs, which are isolated from the host machine. The only folder the sandbox can access is the working directory you give access to, and all network traffic goes through a network proxy, which can either block traffic, or it can inject credentials such that the coding agent never sees them directly.
I've only used docker sandboxes for a short while, but I've found they work relatively well for my purposes. However, one limitation is that some of the projects I'm working on have a bunch of requirements for tooling, which always needs to be installed in the sandbox. Doing that every time is a bit of a pain. Luckily, there's a solution: custom templates.
Creating a custom Claude Code template
The Docker sandbox documentation describes how to create a custom template, based on one of the default templates. I'm going to use the Claud Code examples in this post, but there's different templates for each of the supported agents. For each supported image there are also 2 variants: one that includes a Docker Engine, and one that doesn't. e.g.
claude-code—includes a variety of dev tools.claude-code-docker—includes the same as above, but also has Docker Engine.
There's also a
claude-code-minimaltemplate which is similar toclaude-code, but includes fewer tools, so you don't have npm, python, or golang, for example.
To create a custom template, you need to have Docker Desktop installed as you're basically building an OCI image (effectively a docker image, kinda, sorta). That's despite the fact that docker sandboxes don't run as docker containers, but rather as microVMs.
The following example, based on the documentation shows how to start from the default template, how to install package manager dependencies, and how to install other tools, using dotnet as an example:
FROM docker/sandbox-templates:claude-code-docker
# Switch to root to run package manager installs (.NET dependencies)
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
libc6 \
libgcc-s1 \
libgssapi-krb5-2 \
libicu76 \
libssl3t64 \
libstdc++6 \
tzdata \
zlib1g
# Most tools should be installed at user-level, using the agent user
USER agent
RUN curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 10.0 --no-path
ENV DOTNET_ROOT=/home/agent/.dotnet \
PATH=$PATH:/home/agent/.dotnet:/home/agent/.dotnet/tools
This shows several important things:
- The base
docker/sandbox-templatesimages are based on Ubuntu, so useapt-getfor managing packages. - The base images include two users,
rootandagent.- System-level package installations must be made using the
rootuser. - Tools that install into the home directory must be installed using the
agentuser.
- System-level package installations must be made using the
You can build the package using familiar docker build commands, but you must push it straight to an OCI registry (Docker Hub works!). You can't just build it locally as the docker sandbox doesn't share the image store with your local Docker host.
docker build -t my-org/my-template:v1 --push .
Once you've pushed the image to an OCI registry you can use it locally in a sandbox by using the --template or -t argument when calling sbx run:
sbx run -t docker.io/my-org/my-template:v1 claude
This will pull (and cache) the template you specify, and you'll have the extra tools immediately available in your sandbox. Note that you must include the docker.io (Docker Hub) or other prefix when specifying the template (which differs from when you're running "normal" docker commands).
I've created some sandboxes for .NET, similar to the above, and pushed them to dockerhub. You can see the definition of the images here. Feel free to use them if you wish!
Basing your custom templates on the standard default templates works well when you just want to make some extra tools available to your sandbox, but what if you fundamentally want to use a different base image? That's a bit trickier…
What if you need to change the base image?
The "supported" approach to these custom templates is shown in the previous section: you start with the docker/sandbox-templates and then install the extra tools on that base image. Currently, those images are based on ubuntu 25.10, which is a nice current base image. But what if you need to use an older image for running tests. This is the case for the Datadog .NET SDK where we build using old distro versions to ensure we can support customers running with early glibc versions.
This proves a little tricky, as it's not officially supported. On the one hand, to emulate the work the base images do, mostly there's just a few crucial configurations you need to add, such as setting NO_PROXY, creating an agent user, and installing the claude CLI. However, the docker/sandbox-templates images contain a lot more than that. Unfortunately, the contents of these images aren't readily available on GitHub, for example.
Luckily, you can see the contents of each layer on Docker Hub. It's a little bit messed up due to how buildkit renders it, but it is understandable. Based on each of those layers, I was able to effectively reverse-engineer the layering the docker/sandbox-templates:claude-code-docker image on top of a different base image.
This is all very hacky, could change at any time, and comes with no guarantees that it works for you 😅
The following shows a dockerfile that aims to perform all the steps the default docker/sandbox-templates to, but based on an arbitrary base image. There's quite a lot in here, but in summary:
- It configures various environment variables.
- Installs various basic tools (curl, certificates) and sets up various keyrings.
- Configures the
agentuser. - Sets up a
CLAUDE_ENV_FILEtemporary session file. - Installs a variety of tools (npm, golang, python, make etc).
- Installs Claude Code.
All in all, it looks a bit like this:
FROM dd-trace-dotnet/debian-tester AS base
# Grab stuff from the original sandbox
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
ENV PATH=/home/agent/.local/bin:/usr/local/share/npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ENV NO_PROXY=localhost,127.0.0.1,::1,172.17.0.0/16
ENV no_proxy=localhost,127.0.0.1,::1,172.17.0.0/16
WORKDIR /home/agent/workspace
RUN apt-get update \
&& apt-get install -yy --no-install-recommends \
ca-certificates \
curl \
gnupg \
&& install -m 0755 -d /etc/apt/keyrings \
&& curl -fsSL https://download.docker.com/linux/debian/gpg | \
gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
&& chmod a+r /etc/apt/keyrings/docker.gpg \
&& echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
# Remove base image user
# Create non-root user
# Configure sudoers
# Create sandbox config
# Set up npm global package folder under /usr/local/share
RUN userdel ubuntu || true \
&& useradd --create-home --uid 1000 --shell /bin/bash agent \
&& groupadd -f docker \
&& usermod -aG sudo agent \
&& usermod -aG docker agent \
&& mkdir /etc/sudoers.d \
&& chmod 0755 /etc/sudoers.d \
&& echo "agent ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/agent \
&& echo "Defaults:%sudo env_keep += \"http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY SSL_CERT_FILE NODE_EXTRA_CA_CERTS REQUESTS_CA_BUNDLE JAVA_TOOL_OPTIONS\"" > /etc/sudoers.d/proxyconfig \
&& mkdir -p /home/agent/.docker/sandbox/locks \
&& chown -R agent:agent /home/agent \
&& mkdir -p /usr/local/share/npm-global \
&& chown -R agent:agent /usr/local/share/npm-global
RUN touch /etc/sandbox-persistent.sh && chmod 644 /etc/sandbox-persistent.sh && chown agent:agent /etc/sandbox-persistent.sh
ENV BASH_ENV=/etc/sandbox-persistent.sh
# Source the sandbox persistent environment file
# Export BASH_ENV so non-interactive child shells also source the persistent env
RUN echo 'if [ -f /etc/sandbox-persistent.sh ]; then . /etc/sandbox-persistent.sh; fi; export BASH_ENV=/etc/sandbox-persistent.sh' \
| tee /etc/profile.d/sandbox-persistent.sh /tmp/sandbox-bashrc-prepend /home/agent/.bashrc > /dev/null \
&& chmod 644 /etc/profile.d/sandbox-persistent.sh \
&& cat /tmp/sandbox-bashrc-prepend /etc/bash.bashrc > /tmp/new-bashrc \
&& mv /tmp/new-bashrc /etc/bash.bashrc \
&& chmod 644 /etc/bash.bashrc \
&& rm /tmp/sandbox-bashrc-prepend
&& chmod 644 /home/agent/.bashrc \
&& chown agent:agent /home/agent/.bashrc
USER root
# Setup Github keys
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
| tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
&& chmod a+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
| tee /etc/apt/sources.list.d/github-cli.list > /dev/null
# Install all the tools available in the claude-code-docker image
RUN apt-get update \
&& apt-get install -yy --no-install-recommends \
dnsutils \
docker-buildx-plugin \
docker-ce-cli \
docker-compose-plugin \
git \
jq \
less \
lsof \
make \
procps \
psmisc \
ripgrep \
rsync \
socat \
sudo \
unzip \
gh \
bc \
default-jdk-headless \
golang \
man-db \
nodejs \
npm \
python3 \
python3-pip \
containerd.io docker-ce \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
LABEL com.docker.sandboxes.start-docker=true
USER agent
FROM base AS claude
# Install Claude Code
RUN curl -fsSL https://claude.ai/install.sh | bash
ENV CLAUDE_ENV_FILE=/etc/sandbox-persistent.sh
CMD ["claude", "--dangerously-skip-permissions"]
If you don't want all the extra tools like npm, python and golang, you can instead base it on the claude-code-minimal image instead. In that case, the final tool install step looks a bit like this instead:
RUN apt-get update \
&& apt-get install -yy --no-install-recommends \
bubblewrap \
dnsutils \
docker-buildx-plugin \
docker-ce-cli \
docker-compose-plugin \
git \
gh \
jq \
less \
lsof \
make \
procps \
psmisc \
ripgrep \
rsync \
socat \
sudo \
unzip \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
Or, you know, install a mix of those tools. That's the advantage of this approach at least, you can install more of fewer tools, whatever you want! Whichever approach you like, you can again build and push the image to an OCI registry:
docker build --tag dd-trace-dotnet/sandbox --push .
You can then use the image in your sbx sandbox, just as before, but this time you'll be running in a base image that has all of your prerequisites installed.
Updating the version of Claude Code only
You might notice in the above Dockerfile that I put the Claude Code image in its own section of the multi-stage build:
FROM base AS claude
# Install Claude Code
RUN curl -fsSL https://claude.ai/install.sh | bash
ENV CLAUDE_ENV_FILE=/etc/sandbox-persistent.sh
CMD ["claude", "--dangerously-skip-permissions"]
That's not necessary, but I did it for a subtle reason. Claude Code updates a lot, but I didn't really want to update the entire image repeatedly for performance reasons. By moving the Claude Code install to its own final stage, I could rebuild just that stage, without having to rebuild the entire image, by using --no-cache-filter:
docker build --tag dd-trace-dotnet/sandbox --push --no-cache-filter claude .
It's just a minor thing, but it means updating to the latest Claude Code version is a much quicker process.
I still need to test this image out properly, but I tried it out with a previous version and it was working pretty well for me. I'd be interested to know if anyone else has tried something similar, or if you have a better solution (short of just yolo/dangerous direct on the host!).
Summary
In this post I described how to create custom templates for Docker Sandboxes. First I showed the official approach, which layers tools on top of yhe default sandbox templates in docker/sandbox-templates. This is the easiest approach, and works well if the specific base image doesn't matter too much to you. Then I showed how I reverse-engineered the sandbox templates to allow completely swapping out the base image. This was necessary for a project I was working on, where I specifically wanted to run agents in the same base image we use to build the project. this approach isn't supported, and I'm not 100% it's quite right, but it seems to do the job well enough!
