Tutorial

Share one container with your agents

A team of agents working in your checkout is a team working on your real machine, with your real credentials, usually with their permission prompts turned off. The sandbox puts the whole project — your panes and every agent on the team — inside a single container instead. One filesystem, one toolchain, one blast radius. This walkthrough turns it on, opens a shell in it, sends a team into it, and shows what it does and does not protect.

The sandbox needs Docker or Podman — nothing else. No VS Code, no devcontainer CLI, no image to pull from a registry Sinclair publishes. If your project happens to have a devcontainer.json, it is read; if it does not, nothing about this is different. On macOS, is the modifier — on Linux, read it as (Ctrl).

1. Turn it on

Open File ▸ Sandbox (it is also under AI ▸ Sandbox once AI is enabled) and pick Use Sandbox for This Project. The first row of that menu is a live status line, so it is always the place to look when you want to know what the sandbox is doing.

The toggle writes a real setting, so it survives a restart:

// ~/.config/sinclair/settings.json
{
  "sandbox-enabled": true
}

"This project" means the repository the focused pane is in — found by walking up to the nearest .git, so every pane in a checkout lands in the same container instead of a pane three directories down getting one of its own. Each project gets its own container, named after the path, so two checkouts called api in different places never share one, and reopening either finds the container it already had rather than starting a second.

Careful

Paths too broad to mount are refused: /, anything one level down (/Users, /home), and your home directory. A sandbox mounts what it is given, and mounting $HOME would hand an agent with its prompts turned off your whole machine. Open a pane inside a repository first.

2. Open a shell in it

Sandbox ▸ Open Shell in Sandbox gives you a normal tab whose shell is inside the container. Scrollback, search, copy mode, recording, themes: all of it works, because the pane is an ordinary pty like every other.

The first time, this takes a few minutes. Sinclair generates a small Dockerfile, layers your agent CLI onto a Debian base, and builds it locally — there is no image to download from us. The menu status line names the step it is on (Building the sandbox image…) the whole time. Afterwards the image is cached and opening a shell is instant.

Tip

The Containers panel in the sidebar ( the activity bar's ❖ icon) has the same controls plus anything Sinclair wants to warn you about, and it is the one place that shows how many panes are currently inside.

3. Your paths still mean what they meant

The project is mounted at its own path. If your repo is /Users/you/code/api on the host, it is /Users/you/code/api inside the container too — not /workspaces/api.

That is not a cosmetic choice. Git records absolute paths when it creates a worktree: the main repo's .git/worktrees/<name>/gitdir and the worktree's own .git file point at each other. Under any other mapping, a worktree made inside the container is broken on the host and vice versa. With the paths equal, the worktree workflow keeps working unchanged from both sides — which matters, because giving each agent its own worktree is exactly how you keep a team from editing over each other inside the shared container.

Careful

Create worktrees under the project, not beside it. The tutorial's ../wt/thing convention puts them outside the mount, where no agent can see them. .worktrees/thing is inside, and works.

4. Send a team in

With the sandbox on, AI ▸ Teams ▸ any team brings the container up first, then opens the roster. Every member is exec'd into that same container, so the whole team shares a filesystem, a build cache, and one toolchain. A quick-launched agent (AI ▸ Agents ▸ Launch …) and a saved agent definition go to the same place.

Relay itself stays on the host. Only the agent process runs inside, which is why a crash in the container can never take the mesh — or your terminal — down with it. The bus is still reachable: Sinclair points each agent at the engine's gateway host (host.docker.internal, or host.containers.internal under Podman) instead of a loopback address that would mean the container itself.

Standalone, the same thing from the CLI:

relay launch alice --role frontend \
  --sandbox sinclair-sbx-api-9f3c1a20 \
  --sandbox-engine docker \
  --sandbox-workdir /Users/you/code/api

5. Signing in, once

An agent inside a Linux container cannot read a credential your macOS keychain is holding, so the first agent you launch there will ask you to sign in. It only happens once: the agent's home directory is a named volume, so the login — along with folder-trust answers and shell history — survives rebuilding the image and recreating the container.

6. What it actually protects

Sinclair launches unattended team members with their permission prompts bypassed, because a member sitting on a dialog in a split nobody is watching is a member doing nothing. That trade is very different depending on where it happens:

Careful

Never mount /var/run/docker.sock into the sandbox. It is a common devcontainer convenience and it is a one-line container escape: it hands an agent running with prompts bypassed root on your host, which is the whole thing you were trying to prevent.

7. Make it your environment

The generated image is deliberately thin. Point it somewhere better, or add to it:

{
  "sandbox-enabled": true,

  // Use an image you already maintain, as-is. Sinclair builds nothing —
  // it trusts this image to carry the agent CLI.
  // "sandbox-image": "ghcr.io/you/dev:latest",

  // Or keep the generated layer and change what it is built on / with.
  "sandbox-base": "debian:bookworm-slim",
  "sandbox-packages": ["ripgrep", "build-essential"],
  "sandbox-setup": ["curl -sSf https://sh.rustup.rs | sh -s -- -y"],

  // Extra mounts: source:target[:ro]. A bare path mounts at itself.
  "sandbox-mount": ["~/.gitconfig:/sandbox/home/.gitconfig:ro"],
  "sandbox-env": ["DATABASE_URL=postgres://host.docker.internal/dev"],

  // Ceilings for everything in the container, together.
  "sandbox-memory": "8g",
  "sandbox-cpus": "4"
}

The generated layer uses apt-get, so it wants a Debian or Ubuntu base. For anything else — Alpine, a language-specific image, your company's build image — set sandbox-image and install the agent CLI in that image yourself.

8. If the project has a devcontainer.json

It is read, and used where it helps. The image it names becomes the base Sinclair layers the agent CLI onto, so your agents land in the toolchain your team already agreed on. Its containerEnv and remoteEnv reach the container, with your own sandbox-env winning on conflicts.

Mounts and remoteUser from the file are applied too. The full list of what is and is not read is in Work in your project's dev container.

If an editor already has a container up for this folder, Sinclair enters that one instead of building a parallel one — and will never stop or remove it, because your editor session is very likely attached to it. The Containers panel says so when that happens.

Tip

Two lines in your devcontainer.json make it behave much better with agents: "shutdownAction": "none" so closing your editor does not stop a container with a team working in it, and the identity mount below so worktrees stay valid on both sides.

// .devcontainer/devcontainer.json
{
  "workspaceMount": "source=${localWorkspaceFolder},target=${localWorkspaceFolder},type=bind",
  "workspaceFolder": "${localWorkspaceFolder}",
  "shutdownAction": "none"
}

Turn the whole behaviour off with "sandbox-devcontainer": false if you would rather Sinclair ignored the file.

9. Cleaning up

The container outlives any single pane — several panes and every agent are inside it, so closing one tab must not take the others down. By default it also outlives the last one: rebuilding a toolchain every session is slow and an idle container costs nothing. Set "sandbox-persist": false to have it stop when the last pane leaves.

Neither ever touches a container Sinclair did not create.

Known limits

Where next