One command to run an AI coding agent inside a Docker Sandbox.
sand wraps sbx: it finds or creates the sandbox, reconciles mounts and MCP
servers, copies your harness configuration in, keeps conversation history
across rebuilds, and logs every step and every sbx command it runs.
$ sand claude . ../shared-lib:ro ../docs:rwWhat it gives you on top of sbx run:
- one command per project: the sandbox is found or created, and extra mounts and MCP servers are reconciled with what you ask for;
- your harness configuration (settings, instructions, commands, agents, plugins, hooks, skills) follows you into the sandbox on every launch;
- conversation history survives rebuilds and
sbx rm; - a status line with the sandbox, git branch, API and MCP state, and your subscription limits;
- blocked network access and mounts can be granted while the agent works, from a second terminal, from a prompt inside Claude Code, or from Tern;
- ACP for editors (Zed, JetBrains, Tern), with Google Vertex AI as an authentication option.
sand is pre-1.0 and under active development;
flags, config keys and on-disk state can still change between releases.
| Area | State |
|---|---|
| Platforms | Linux (x86-64, arm64), macOS (Apple silicon) and Windows (x86-64). CI builds and tests on all three. |
| Harnesses | claude, codex and opencode are built in, with config copy, history and status. Every other sbx agent (copilot, cursor, devin, docker-agent, droid, gemini, kiro, shell) runs without those, and you can define your own. |
| Terminal UI | The default: sand <harness> opens the agent's own UI in your terminal. |
| Vertex AI | Built-in wiring for Claude Code; other harnesses describe theirs with vertex_overlay. |
| ACP for editors | --install-acp registers Zed, JetBrains and Tern. The first start of a new sandbox can be slow enough for an editor to time out. |
| Tern native surface | The whole session drawn in a Tern pane. It is the newest part and changes the most; --no-native opts out per run. |
sand needs sbx installed and signed in; it does not install or update sbx
itself.
Requires the sbx CLI (signed in with sbx login; when the login is missing or
expired, sand runs sbx login for you if it has a terminal).
Prebuilt executables for Linux, macOS and Windows are attached to every release. The install scripts download the one for your platform and verify its checksum.
Linux and macOS (installs to ~/.local/bin):
$ curl -fsSL https://raw.githubusercontent.com/insertt/sand/main/install.sh | shWindows (installs to %LOCALAPPDATA%\Programs\sand and adds it to your PATH):
PS> irm https://raw.githubusercontent.com/insertt/sand/main/install.ps1 | iexSet SAND_VERSION (for example v0.1.0) to install a specific release instead
of the latest, and SAND_INSTALL_DIR to install somewhere else. Run the script
again to upgrade.
To build from source instead, with a recent stable Rust:
$ cargo install --locked --path .sand [HARNESS] [MOUNT...] [FLAGS] [-- AGENT_ARGS...]
HARNESS is claude, codex, opencode, any other sbx agent, or one you
define in the config file. Without it, default_harness is used (claude
unless configured). Each MOUNT is a directory with an optional :ro or :rw
suffix (read-write by default). The first one is the primary workspace; with no
mounts, the current directory is used, so sand codex and sand codex . are
the same.
| Flag | Effect |
|---|---|
--mcp a,b |
Expose these MCP servers (names from sbx mcp ls). |
--resume |
Attach to the existing sandbox without the "different setup" question: mounts and MCP servers on the command line are added, nothing is removed. Fails if there is none. |
--no-attach |
Create or update the sandbox and start it, then exit without opening the agent. sand [HARNESS] --resume attaches later. |
--shell, --no-shell |
Set the sandbox up as usual, then open a login shell in it (in the primary workspace) instead of the agent; exiting the shell ends the session. --no-shell opens the agent even with attach = "shell" in the config. |
--recreate |
Remove the sandbox and create it again, asking what to keep of its conversation history, mounts and MCP servers (see Recreating a sandbox). |
--force-recreate |
Remove the sandbox and create it again from the command line alone, without asking: its history, and mounts and MCP servers the command line does not name, are dropped. |
--scratch |
No project: use the shared scratch directory, which persists and is shared by all harnesses. |
--dedicated-scratch |
No project: a throwaway mountless sandbox, removed when the agent exits. |
--name NAME |
Sandbox name. The default is <harness>-<directory>. |
--template T |
Create the sandbox from a template (see Templates). |
--acp |
Speak ACP on stdio instead of opening the terminal UI (see Editors). |
--native, --no-native |
Draw the session natively in Tern, or open the agent's terminal UI, for this run. Both win over tern_native and SAND_TERN_NATIVE. |
--install-acp zed|jetbrains|tern |
Register sand [HARNESS] --acp in the editor or terminal, then exit. |
--open-config |
Open the config file in your editor, then exit. |
--on-conflict replace|merge|abort |
Answer the "sandbox exists with a different setup" question up front. |
-v, --verbose |
Echo every sbx command sand runs and show its output (otherwise shown only when a command fails). Works with the subcommands too. |
Other commands:
| Command | Effect |
|---|---|
sand status [HARNESS] [DIR] |
Everything sand recorded about a sandbox, and live checks (below). sand describe is an alias. |
sand allow [HARNESS] [DIR] |
Allow hosts the sandbox was blocked from reaching (see Blocked network access). |
sand mount [HARNESS] DIR[:ro|:rw]... |
Add extra mounts to an existing sandbox, also while an agent runs (see Adding mounts later). |
sand umount [HARNESS] DIR... |
Remove them again. |
sand template build|save|ls|rm |
Manage templates. |
sand status prints the harness, kind, creation time, template, nightly image,
mounts (sand's own included), MCP servers and data directory, whether sbx
reports the sandbox running and whether a sand session is attached, and checks
whether the agent API is reachable from inside it and how the MCP servers are
doing. The live checks run only while the sandbox is running, so sand status
never starts one; --no-probe skips them and runs nothing inside the sandbox,
and --json prints the same on stdout for scripts.
If the requested mounts, MCP servers or template differ from what the sandbox
has, sand shows both and asks:
- Replace makes the sandbox match the command exactly. Extra mounts are
changed in place with
sbx mountandsbx umount. A different primary workspace, a removed MCP server or a different template cannot be changed in place, so the sandbox is recreated (history is kept). - Merge keeps what is there and adds what is missing.
- Abort changes nothing.
Adding MCP servers is never a conflict; they are attached live with
sbx mcp load. Without a terminal, pass --on-conflict. In --acp mode,
sand merges.
--recreate removes the sandbox and creates it again from the command line.
Before it does, it lists what the command line would lose and asks what to keep,
everything ticked:
- the conversation history, when there is any;
- mounts the sandbox has and the command line does not name;
- MCP servers the sandbox has and the command line does not name.
So sand claude --recreate rebuilds the sandbox with its history, mounts and
MCP servers, and unticking an item leaves it out. When there is nothing to keep,
it does not ask. It needs a terminal (or Tern's native surface, which asks one
item at a time); without one it fails before removing anything. --force-recreate asks nothing and keeps
nothing the command line does not name, history included:
$ sand claude --recreate # rebuild, choosing what to keep
$ sand claude --force-recreate # rebuild from the command line aloneTo add a directory to the sandbox of the current directory, also while an agent is running in it:
$ sand mount ../shared-lib:ro # the agent sees it right away
$ sand umount ../shared-libThe sandbox is picked like for sand status: the current directory and the
default harness, or sand mount codex ..., --name or --scratch. Mounting a
directory again with another mode remounts it; the primary workspace cannot be
changed this way.
When starting a session, --resume adds mounts without asking. The first
directory still picks the sandbox, so repeat the primary workspace:
$ sand claude --resume . ../shared-lib:roIn Claude Code you can also just ask the agent: "mount ~/notes read-only", or
in the command line's syntax, "mount ~/notes:ro" or "mount ../shared-lib:rw".
The sand mod gives it a request_mount tool; the request shows up above the
prompt with Mount read-write, Mount read-only and Dismiss, and
choosing a mount has sand confirm it in a dialog on the host (see
Prompts in Claude Code), naming the resolved
directory and mode, before it mounts it as sand mount would. ~ is your home
on the host and relative paths start at the primary workspace. The agent cannot
ask for a directory inside a read-write mount of its sandbox (it could swap it
for a link while you read the dialog); use sand mount, or /mount in
Tern, for that.
The directory appears at the same path inside the sandbox, the agent is told
once it is there, and it stays mounted like one sand mount added. On the next
launch it is also a working directory of Claude Code, as every extra mount is
(permissions.additionalDirectories in the sandbox's settings). sand's own data
directory is never mounted this way.
sand holds an OS file lock for the sandbox while a session runs. A second
sand on the same sandbox, whether in a terminal or from an editor, fails
immediately and says who holds it. The lock is released if sand crashes.
Sessions started with plain sbx run are not detected. sand mount,
sand umount, sand status and sand allow do not take the lock.
Bypass permissions. Every session runs in the harness's bypass mode. sbx
adds the flag by default; sand also enforces it in the copied settings and
re-adds the flag when your agent arguments would replace sbx's defaults.
Host configuration. On every launch, sand copies your user-level harness
configuration into the sandbox: settings, global instructions, commands,
agents, plugins, hooks. Settings are merged in this order: what the
sandbox was created with, then your host settings, then sand's overrides.
Absolute paths that point into the copied directories (plugin install paths,
for example) are rewritten to their sandbox location. Files you delete on the
host are removed from the sandbox on the next launch.
Skills are not copied: sbx shares them through its own
skills store,
mounted read-only in the sandbox. For Claude Code and Codex, sand runs
sbx skills import --force on every launch so the store matches the skills on
your host (set import_skills = false under [harness.<name>] to manage the
store yourself). A sandbox created before sbx had the store has to be
recreated (--recreate) to see it.
Not copied: credential files (.credentials.json, auth.json), env entries
in settings whose names look like credentials (use sbx secret set, or see
Authentication), and MCP servers defined in the host
configuration (use --mcp).
History. Conversation history directories are backed by host directories
under sand's data directory and bind-mounted into the sandbox, so they survive
--recreate (unless you choose not to keep them) and even a manual sbx rm;
--force-recreate discards them.
Status. For Claude Code, sand installs a status line with the sandbox,
the git branch, agent API and MCP server status, and below it the model, the
context window in use and how much of the 5-hour and weekly subscription limits
is spent:
📦 claude-myproject │ ⎇ main │ Subscription ✓ │ MCP atlassian ✓ gitlab ⚠ auth
🤖 Opus 5.5 │ ctx ████░░░░░░ 42% │ 5h 23% │ 7d 41%
The API mark names how the agent signs in: Subscription when Claude Code
reports limits, API otherwise and API (Vertex) with Vertex AI.
Claude Code reports the limits only for Pro and Max subscriptions and only
after the first response of a session; until then that part is left out, as is
the branch outside a git repository.
Codex cannot run a status-line command, but it has a built-in status line, so
sand sets tui.status_line to show the model, git branch, context in use and
the 5-hour and weekly limits. To choose other items, set
overlay = { tui = { status_line = [...] } } under [harness.codex]. OpenCode
shows the model and context itself and has no limits to report.
The sandbox, API and MCP status cannot be added to the Codex or OpenCode
status line, so sand prints them as a banner before the agent starts, and
sand status shows them on demand.
| Harness | Config copied from | History kept | Status line | ACP command |
|---|---|---|---|---|
claude |
~/.claude |
projects, todos, file-history |
yes | npx -y @agentclientprotocol/claude-agent-acp |
codex |
~/.codex |
sessions |
built-in + banner | npx -y @agentclientprotocol/codex-acp |
opencode |
~/.config/opencode |
~/.local/share/opencode |
banner | opencode acp |
| anything else | nothing | nothing | banner | set acp_command |
By default the agent uses whatever credentials sbx has for it (sbx secret set, or the sign-in sbx offers). auth under [harness.<name>] selects
another way.
# config.toml
[vertex]
project = "my-gcp-project"
region = "global" # default
[harness.claude]
auth = "vertex"Claude Code then runs against Vertex AI with a Google Cloud access token it
never sees. On every launch sand:
- registers the token as an
sbxcustom secret of that sandbox:sbx secret set-custom --sandbox <name> --host <vertex hosts> --env ANTHROPIC_AUTH_TOKEN --command 'gcloud auth print-access-token' --refresh 45m. Thesbxdaemon runs the command on the host and again once the cached token is 45 minutes old (Google's tokens last an hour), so sessions of any length keep working, including ones started with--no-attachor plainsbx run; - sets
CLAUDE_CODE_USE_VERTEX=1,CLAUDE_CODE_SKIP_VERTEX_AUTH=1,ANTHROPIC_VERTEX_PROJECT_ID,CLOUD_ML_REGIONandANTHROPIC_AUTH_TOKENin theenvof the sandbox'ssettings.json.ANTHROPIC_AUTH_TOKENholds a placeholder (sand-vertex-<sandbox>); the sandbox proxy replaces it with the token on requests to the Vertex AI hosts; - probes the Vertex AI host instead of
api.anthropic.comfor the API status.
gcloud has to be signed in on the host (gcloud auth login) and on the
PATH of the sbx daemon, and the Vertex AI host has to be allowed by the
sandbox's network policy (sand allow claude --host aiplatform.googleapis.com:443).
Other env entries, such as model overrides, go in
overlay = { env = { ... } }.
All [vertex] keys:
| Key | Default | |
|---|---|---|
project |
required | Google Cloud project ID |
region |
global |
Vertex AI region |
hosts |
from the region | hosts that receive the token: <region>-aiplatform.googleapis.com and aiplatform.googleapis.com |
token_command |
gcloud auth print-access-token |
host command that prints a token; sbx runs it through the host shell from a temporary directory, so name helpers by absolute path |
refresh |
45m |
how long sbx keeps a token: a duration, or on-demand to run the command for every request |
Only Claude Code has built-in Vertex AI wiring. For another harness, describe
it with vertex_overlay (settings merged in when auth = "vertex"; in
strings, {project}, {region} and {host} come from [vertex] and
{token} is the placeholder) and vertex_token_env (an environment variable
the agent gets the placeholder in). This works for any agent that sends a
bearer token it reads from its settings or environment, for example Gemini
models through Vertex AI's OpenAI-compatible endpoint in OpenCode:
[harness.opencode]
auth = "vertex"
[harness.opencode.vertex_overlay.provider.vertex]
npm = "@ai-sdk/openai-compatible"
name = "Vertex AI"
options = { baseURL = "https://{host}/v1/projects/{project}/locations/{region}/endpoints/openapi", apiKey = "{token}" }
models = { "google/gemini-2.5-pro" = {} }It does not work for agents that sign in to Google themselves (Gemini CLI,
OpenCode's google-vertex providers): they exchange a credential file for
tokens, and that would have to be in the sandbox.
The sandbox's network policy rejects hosts that are not on an allow list.
sand allow, run in another terminal while the agent works, lists the hosts
the sandbox was blocked from reaching and lets you tick the ones to allow:
$ sand allow claude # choose from the blocked hosts
$ sand allow claude --list # only print them
$ sand allow claude --host zed.dev:443 # allow a host without askingIt reads sbx policy log and adds the rules with sbx policy allow network --sandbox <name>, so they apply to that sandbox only; --global allows the
hosts for every sandbox. Hosts are allowed exactly as they were logged,
including the port. Requests that were blocked have already failed, so ask the
agent to try again. Every block is listed, oldest first, and can be allowed.
A block with a reason other than default deny, such as an explicit deny rule,
shows that reason: deny rules take precedence, so an allow may not lift it.
A host the sandbox reached after it was last blocked is marked "reached
since" rather than hidden.
For Claude Code, sand also ships a small mod, sand, into the
sandbox. When a tool call runs into the network policy, it shows a toast and a
row above the prompt with the blocked host. When the sandbox daemon published
an approval for it, the row offers that approval's own options; otherwise it
offers to allow the host for this sandbox, or to dismiss it. The agent is told
the host is waiting for your answer, and when it is allowed, that it can retry.
So that a blocked agent is noticed while you are in another window, the mod
also raises a desktop notification as soon as a host is blocked, such as
"claude-myproject is blocked from reaching pypi.org. Answer in Claude Code."
Claude Code sends it the way it sends its own: as the escape sequence your
terminal turns into a notification (iTerm2, Ghostty, kitty), so it comes from
the terminal under the permission you already gave it, and follows your Claude
Code notification setting. Other terminals, like macOS Terminal, ring the bell
instead. So that the agent can tell which terminal it runs in, sand passes
the host's terminal variables (TERM_PROGRAM and the like) into the session,
and sets TERM_PROGRAM from TERM for Ghostty and kitty reached over SSH.
Choosing an option in Claude Code only files a request. sand answers it on
the host while the session runs: allowing anything first needs your
confirmation in a native dialog (osascript on macOS, zenity or kdialog
on Linux, a message box on Windows), which the agent cannot answer, and only
then does sand run sbx policy approval respond or
sbx policy allow network --sandbox <name>. Refusing an approval needs no
confirmation. Requests for wildcards, lists of hosts or approvals of other
sandboxes are refused without asking, a burst of retries for a host you just
declined does not ask again, and every request is recorded in
network-prompts.log in the sandbox's data directory. Where no dialog can be
shown (a host without a graphical session), the request fails with a pointer
to sand allow.
A host you refused before stays blocked by the deny rule that refusal left,
which wins over any allow. The mod offers to lift it, the dialog says so, and
sand removes that rule for this sandbox before allowing the host. A deny rule
that applies to every sandbox is not removed: the request fails with the
sbx policy rm command to remove it yourself.
Requests go through a directory of each sandbox's own, mounted read-write at
~/.sand-requests in that sandbox alone, and sand answers in another,
~/.sand-policy, which the sandbox can only read. sand never follows a link
the agent puts in the first. Since a request names its sandbox by where it
appears, keep sand's data directory out of your mounts: sand warns about a
mount that contains it. The mod is loaded through CLAUDE_CODE_PLUGIN_DIRS in
the sandbox's Claude Code settings. Set network_prompts = false under
[harness.claude] to leave it out.
A host is only allowed, from the mod or from sand's own UI, when sbx policy log shows the sandbox was really blocked from reaching it, so the agent cannot
get a row for a host by printing a block message. It can still try to reach any
host to get one, so read the host before you allow it.
sbx owns the MCP registry. Register a server once with sbx mcp add, or
describe it under [mcp.<name>] in the config file and sand registers it the
first time you ask for it. A sandbox created with --mcp gets exactly those
servers (sbx static mode). A sandbox created without it lets the agent discover
registered servers through the gateway.
Before a sandbox starts, sand checks the servers it uses and runs
sbx mcp auth <name> for each one whose credentials are missing or expired.
sbx refreshes the credential on its own when it can and asks you to sign in
only when that needs your consent. A server that cannot be authorized is
reported and the launch continues without it. Without a terminal nothing is
authorized and sand names the command to run instead; --acp mode skips
the check.
A template is an image with your tooling already installed. Three ways to get one:
# config.toml
[templates.rust]
dockerfile = "~/sand/rust.Dockerfile" # built with `sand template build rust`
context = "~/sand" # build context (default: the Dockerfile's directory)
build_args = { RUST_VERSION = "1.90" }
[templates.corp]
image = "ghcr.io/acme/agent-image:3" # pulled by the sandbox runtime$ sand template build rust # docker build, then load into the sandbox runtime
$ sand template save claude-proj tuned # or snapshot a sandbox you set up by hand
$ sand claude --template rustBase a Dockerfile on the harness's sbx image, for example
FROM docker/sandbox-templates:claude-code. A Dockerfile template that has not
been built yet is built on first use. Set template = "rust" under
[harness.<name>] to make it that harness's default.
sbx creates a sandbox from an image it already has, so the agent CLI inside
can be weeks old. To start from the image that is rebuilt every night with the
latest release of the harness instead:
# config.toml
nightly = true # every harness…
[harness.codex]
nightly = false # …except this onesand then creates the sandbox with --template docker.io/sbx/<agent>-image:latest --pull always (docker/sandbox-templates:<agent>-docker-nightly for gemini
and shell, which have no such image). A template, from --template or the
harness's template, takes precedence.
The image is fixed when a sandbox is created, so on every launch sand asks
the registry whether a newer build has been published since. If one has, it
offers to recreate the sandbox from it: history is kept, anything you installed
in the sandbox by hand is lost. Choosing to keep the sandbox is remembered
until the next build. --on-conflict replace recreates without asking and
--on-conflict merge keeps the sandbox; without a terminal the sandbox is
kept and the log says a newer image exists. --resume and --acp skip the
check, and a registry that cannot be reached never blocks a launch.
$ sand claude --install-acp zed
$ sand codex --install-acp jetbrains
$ sand claude --install-acp ternThis adds an agent server entry that runs sand <harness> --acp (the previous
file is kept as <file>.sand-backup). The editor starts it in the project
directory, which becomes the primary workspace. In ACP mode stdout carries only
the protocol; sand's log goes to stderr.
On Windows the editor works with paths like C:\Users\me\proj while the agent
sees /c/Users/me/proj, so sand translates paths in the protocol stream in
both directions.
The first start of a new sandbox takes a while. If the editor times out, run
sand <harness> once in that project from a terminal.
In a Tern pane, sand <harness> draws the
whole session natively, in the look of Tern's own agent chat. Right after the
sbx sign-in check, sand asks the terminal whether it speaks the Tern Surface
Protocol. If it does, setup shows as a live list of steps, and its questions
(a sandbox with a different setup, a newer image) come up as cards with a
button per answer (1–9 picks, Enter takes the first, Esc aborts). Once the
sandbox is ready, the steps fold into one line, and sand starts the harness's
ACP server in the sandbox and becomes its client:
- your messages are cards; each agent turn groups its replies (streamed Markdown), folded thoughts and tool cards (commands with their output and how they ended, edits as diffs with a +/− count), and ends with its time, tokens and cost;
- a call that needs permission asks in its card: Enter allows, Esc denies;
- above the composer: the plan, and a working line with what the agent is doing and for how long;
- the composer stays at the bottom of the pane and shows what the status line shows: a context bar (used and window size), the model and its reasoning effort, the permission mode and the session's cost, and on a line under them the sandbox with its git branch, whether the agent's API is reachable, and the MCP servers: green when all are connected, a warning or failed mark when one needs attention or failed, and each server's state in a dropdown. The model, effort and mode chips are pickers of the choices the agent offers (Shift+Tab also steps through the modes, and a mode other than the default shows in the warning color); until it says, they show what your harness settings pick. Branch, API and MCP are checked again while the session runs.
Typing / completes commands under the caret (↑↓ choose, Tab completes,
Enter runs, Esc hides): the agent's own (/init, /review, …), sent to it
as a prompt, and sand's, which ACP offers as methods rather than commands:
/sessions (or /resume without an id) lists the agent's sessions for this
project in a picker, and resumes the one you choose, replaying its
conversation when the agent can; /resume <id> resumes one directly; /new
(or /clear) starts a new session; /fork goes on in a copy of the current
session; /mount <dir>[:ro|:rw] mounts a directory of your computer into the
sandbox, like sand mount, with no further question since you typed it; and
/help lists them all. Resuming and forking need an agent that supports ACP's
session listing, loading and forking.
Typing @ completes file names from the workspace, and @../ reaches into the
other directories the sandbox has mounted.
The composer edits like a native text field (select with the mouse or Shift+arrows, ⌘A, ⌘Z to undo, even a sent draft) and shows the rest of a command being typed faintly after the caret. Messages written while the agent works (or starts) wait above the composer, up to eight, each with a ✕ to take it back, and go in order as each turn ends; after a stop or a failure the queue pauses until you continue or discard it. A queued message's Send now button, or Ctrl+Enter on the draft, sends it at once instead: ACP has no way into a running turn, so the turn is stopped and the message goes as soon as the agent confirms, with the rest of the queue after it. A new session's welcome offers to continue the last conversation, and a few prompts to start with. While the pane is hidden, what happens (a finished turn, a permission request, the agent exiting) is noted above the composer for when you come back. Each turn ends with its time, tokens and cost.
Pasting an image (or Ctrl+V) attaches the clipboard's image to the next message, shown as a thumbnail above the composer with a × to remove it; up to four images go with a message, even without text, to an agent that takes images over ACP. Tern does not hand a program a pasted image's bytes, so sand reads the clipboard of the machine it runs on (osascript on macOS, wl-paste or xclip on Linux, PowerShell on Windows), which over ssh is not yours.
Network access the sandbox was blocked from shows above the composer as a
card with sbx's own options (allow once, always, deny…), answered with a
click, for any harness. Claude Code's requests to mount a directory or allow a
host ask there too, instead of in a host dialog: the mod cannot run, since
Claude Code's own UI is not on screen, so sand gives the agent the same
request_mount tool through a small MCP server of its own (sand-acp, in the
session's MCP list; network_prompts = false leaves it out). Both are noted
when they come in while the pane is hidden.
You can type during setup; the first message is sent once the agent is ready.
Enter sends, Shift+Enter starts a new line and Esc stops a turn. As in
Claude Code, Ctrl+C stops a turn or clears the draft first, and otherwise
quits (or stops setup) only when pressed twice in a row; a toast asks for the
second press. Ctrl+D twice quits too. This needs no setup in Tern and works for
every kind of sandbox, but not with agent arguments, which open the terminal
UI. The subscription limits (5h, 7d) appear under the composer for Claude Code
Pro and Max plans, and the API mark reads Subscription instead of API.
ACP reports a limit's state but not how much of it is spent, so sand asks
Anthropic for the figures itself, every few minutes and soon after each turn,
with the Claude login on the host (~/.claude/.credentials.json, or the
Keychain on macOS). The request goes from the host, not through the sandbox,
and the token is never logged, stored or passed to the sandbox, which only
gets the percentages. An expired login is not refreshed (Claude Code on the
host does that); the limits then stay as they were. Set
usage_limits = false in the config to turn this off. If setup fails or is
aborted, its log is printed in the terminal as usual. The agent's stderr goes
to acp-agent.log in the sandbox's runtime directory. To keep the terminal
UI, set tern_native = false in the config, or SAND_TERN_NATIVE=0 (or
TERN_TSP=0) for one run; SAND_TERN_NATIVE=1 turns it back on over the
config. --native and --no-native do the same for one run and win over both
the config and SAND_TERN_NATIVE; --native still needs Tern and opens the
terminal UI (with a warning) elsewhere.
--install-acp tern adds a custom agent sand-<harness> to Tern's
settings.json ($TERN_CONFIG_DIR if set) and lists it in own_tools, so
the agent uses its own file and terminal tools, which run inside the sandbox,
rather than Tern's, which would run on the host.
With native drawing turned off, in a Tern pane that opens agent commands as
ACP chats (Settings › Agents), sand <harness> then sets the sandbox up as usual and opens the session in
Tern's chat (tern acp sand-<harness>) instead of the agent's terminal UI.
Tern starts sand <harness> --acp in the current directory, which keeps the
sandbox's mounts and MCP servers; while the chat is open, the first sand
holds a handoff lock (locks/<sandbox>.handoff in sand's data directory), so
the ACP session skips the setup it has just done and starts the agent right
away. With --name, --scratch,
--dedicated-scratch, a primary workspace other than the current directory,
or agent arguments, the terminal UI opens instead. To always get the terminal
UI, add the agent's id to agents.keep_tui in Tern's settings.
sand reads config.toml from the platform config directory
(~/.config/sand on Linux, ~/Library/Application Support/sand on macOS,
%APPDATA%\sand on Windows), or from $SAND_CONFIG. Everything is optional.
sand --open-config opens the file in $VISUAL or $EDITOR (creating it with
commented examples if there is none) and checks it when the editor closes.
default_harness = "codex"
nightly = true # use nightly harness images (see Templates)
tern_native = false # keep the agent's terminal UI in Tern
usage_limits = false # do not ask Anthropic for subscription limits (see Tern)
attach = "shell" # open a shell in the sandbox instead of the agent ("harness" is the default)
sandbox_home = "/home/agent" # the agent user's home in sandboxes (the default)
# Change a built-in harness…
[harness.claude]
template = "rust"
exclude = ["node_modules"] # names never copied, in addition to credentials
overlay = { model = "opus" } # merged over the built-in settings overlay
# …or describe a new one.
[harness.aider]
agent = "shell" # sbx agent or kit to run
host_config_dir = "~/.aider"
sandbox_config_dir = ".aider" # relative to the sandbox home
copy = ["conf.yml", "prompts"]
history = [".aider/history"] # relative to the sandbox home
bypass_args = ["--yes-always"]
acp_command = ["aider", "--acp"]
api_check_url = "https://api.openai.com/v1/models"
[mcp.linear]
url = "https://mcp.linear.app/mcp"
[mcp.local] # `url` or `command`, not both; registered with `sbx mcp add`
command = "npx"
args = ["-y", "some-mcp-server"]All harness keys: agent, host_config_dir, sandbox_config_dir, copy,
extra_copy, exclude, settings_files, settings_format, strip_keys,
overlay, history, bypass_args, statusline, acp_command,
api_check_url, template, nightly, import_skills, network_prompts,
attach (overrides the top-level one for that harness), auth, vertex_overlay, vertex_token_env.
State lives in the platform data directory (~/.local/share/sand,
~/Library/Application Support/sand, %LOCALAPPDATA%\sand), or
$SAND_DATA_DIR: per-sandbox state and history, the shared scratch directory,
and session locks.
Environment variables:
| Variable | Effect |
|---|---|
SAND_CONFIG |
Path of the config file. |
SAND_DATA_DIR |
Where sand keeps its state. |
SAND_SBX |
The sbx binary to run. |
SAND_REGISTRY |
Base URL of the registry asked about nightly images (a mirror). |
SAND_TERN_NATIVE |
0 or 1: draw sessions natively in Tern or not, over tern_native; --native and --no-native win. TERN_TSP=0 turns it off too. |
SAND_CONFIRM |
A command that replaces the native dialog that confirms a mount or an allowed host: it is run with the question as its last argument, and exit status 0 approves. |
SAND_VERSION, SAND_INSTALL_DIR |
Read by the install scripts only. |
$ cargo build
$ cargo test # unit tests, and end-to-end tests against tests/fake_sbx.sh
$ cargo clippy --all-targets
$ cargo fmt --allThe end-to-end tests (tests/e2e.rs, Unix only) run the real sand binary
against a fake sbx that records every command, and check the sequence of sbx
calls. They need neither Docker nor a signed-in sbx. CI (.github/workflows)
runs format, clippy with warnings denied, build and test on Linux, macOS and
Windows; a published release builds and attaches the executables.
The Claude Code mod that sand ships into sandboxes lives in
assets/claude-mod/sand, and the MCP server that gives ACP agents
request_mount in assets/mcp/sand.mjs; both are compiled into the binary.
