Skip to content

box.toml configuration reference

box.toml describes one box: the workload it runs, what each process can reach directly, the credentials bound to destinations, the MCP servers, and where decision records go. box run --config <file> reads exactly the file you name.

Every table refuses keys it doesn’t define. An unknown key stops the run with an error that lists the keys the table accepts.

TableWhat it declares
Top levelThe box’s name, its state directory box_dir, and its policy file.
[agent]The workload the box runs: its command, workspace, env, and filesystem.
[agent.env]Environment variables for the agent. Nothing comes from your shell except HOME and PATH when unset.
[agent.filesystem]The agent’s filesystem grants: paths its own process reaches without a policy decision.
[tool.<label>]A host program the agent’s shell can start in its own sandbox.
[tool.<label>.filesystem]That tool’s filesystem grants.
[tool.<label>.network]Whether that tool’s traffic goes through the egress gateway.
[mcp.<name>], type = "stdio"A local MCP server that runs contained, in its own sandbox.
[mcp.<name>.network]Whether that server’s traffic goes through the egress gateway.
[mcp.<name>], type = "http"A remote MCP server, reached through the egress gateway.
[egress.<name>]A credential, and the destinations it’s attached to.
[telemetry.<label>]Where decision records and the agent’s telemetry go: a file or an OTLP collector.

In the tables below, Required is Yes when the key has no default and Box refuses the table without it.

KeyTypeValuesDefaultRequiredMeaning
namestring1 to 32 bytes from A-Z a-z 0-9 . _ -. Not . or ...NoneYesThe box’s name. One path component. It labels the box in its record and telemetry.
box_dirpathAbsolute. ~ isn’t expanded.NoneYesThe box’s private state directory. See box_dir.
policypathAbsolute, or relative to the directory holding box.toml.NoneNoThe Dogwood policy file. Without it, nothing is granted and every decision is a denial.

[agent], [tool.<label>], [mcp.<name>], [egress.<name>], and [telemetry.<label>] are the other top-level tables. [agent] is optional in the file but required to run. Tool labels and MCP server names are one component of A-Z a-z 0-9 . _ -.

  • Absolute. ~ isn’t expanded here.
  • The parent must exist. Box creates the directory with mode 0700, and creates no directory above it. A missing parent is refused with the box directory's parent does not exist; create it, because Box creates no directory above the one `box_dir` names.
  • An existing directory must be yours, mode 0700, and empty or a box Box created.
  • At most 90 bytes, so <box_dir>/run/box.sock fits the 103-byte socket path limit.
  • Outside the agent’s workspace, and not containing box.toml or the policy file. Each is refused by name: `box_dir` <path> is equal to or below the agent's workspace <path>, configuration source <path> is inside `box_dir` <path>, and policy source <path> is inside `box_dir` <path>.

box_dir selects the box: one directory is one box. It holds bin, run, trust, and private, and none of them is a home. One box run owns a box_dir at a time. Policy history persists in box_dir across runs, in <box_dir>/private/dogwood.redb, and the directory stays until you delete it: end the run process to stop a box, and remove the directory to delete it.

To reset a box, stop it and remove its box_dir. The next box run creates a fresh one, and every when temporal rule starts from an empty history. Remove the whole directory, not only the history file: Box refuses a box whose record says it has run but whose history is missing.

[agent], each [tool.<label>], and each [mcp.<name>] with type = "stdio" take the same four keys. A tool and a stdio server also take network.contain_egress, and [agent] refuses network with `network` is refused on `[agent]`:

KeyTypeValuesDefaultRequiredMeaning
commandstring arrayElement 0 is an absolute path or a bare name. See command.NoneYesThe program and its leading arguments.
workspacepathAbsolute, an existing directory. ~ isn’t expanded.See workspaceNoThe starting directory. Grants no access by itself.
envtable of stringsSee env.{}NoEnvironment variables for the process.
filesystemtableThe lists in Filesystem lists.EmptyNoPaths the process reaches directly, without a policy decision.

You can write env and filesystem inline (env = { PATH = "/usr/bin:/bin" }) or as their own tables ([agent.env], [agent.filesystem]).

No table chooses what a process may use. The agent gets every [tool.<label>], [mcp.<name>], and [egress.<name>] the file declares, and each tool and MCP server gets every egress route. The box is the credential boundary: a process that must not hold a credential belongs in another box.

  • Element 0 is an absolute path or a bare name. A bare name resolves on env.PATH when it’s set, otherwise on your PATH, otherwise on /usr/bin:/bin. A relative path containing / is refused.
  • box run --config <file> -- <args> appends <args> to the agent’s command.
  • When command is a script, add the interpreter its first line names to exec.
  • An argument after element 0 can include ${OTEL_EXPORTER_OTLP_ENDPOINT}, ${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT}, or ${OTEL_EXPORTER_OTLP_PROTOCOL}, which Box replaces with its telemetry receiver. Any other ${…} is refused. Arguments passed after -- aren’t expanded.
  • For [mcp.<name>], element 0 must be a bare name that isn’t zsh, bash, sh, python, or python3. It names the alias Box puts in its own bin directory, so the agent starts the server by that name. Two stdio servers can’t share a program.
  • For [agent], defaults to the directory box run started in. Must exist, be a directory, and not be your home directory.
  • For a tool, defaults to the shell’s current directory when the tool’s grants or the agent’s workspace contain it, else the agent’s workspace.
  • For a stdio MCP server, defaults to the agent’s workspace.
  • ~ isn’t expanded here.
  • Names match [A-Za-z_][A-Za-z0-9_]*. HOME and TMPDIR must be absolute.

  • HOME defaults to your home directory, and can’t be inside box_dir or enclose it. A home that encloses box_dir would put this box’s own state, and any sibling box’s, in the agent’s reach. Without env.PATH, the process gets your PATH. For [agent], Box puts its own bin directory first either way. A name that isn’t one of Box’s aliases falls through to the later PATH directories, so the agent can run a program its exec list grants by name. A tool or MCP server gets no Box directory on its PATH.

  • TERM and COLORTERM reach the process only when env names them, so a program that picks colors from them renders plain otherwise.

  • A table can’t set these names. Box sets the proxy, CA, PWD, and USER variables itself, and refuses the loader and interpreter hooks:

    • Any name starting LD_, DYLD_, PYTHON, or OTEL_EXPORTER_OTLP_.
    • NODE_OPTIONS, BASH_ENV, ENV, RUBYOPT, PERL5LIB, PERL5OPT, GEM_PATH, CLASSPATH, JAVA_TOOL_OPTIONS, _JAVA_OPTIONS.
    • HTTP_PROXY, HTTPS_PROXY, http_proxy, https_proxy, NO_PROXY, no_proxy, NODE_USE_ENV_PROXY.
    • SSL_CERT_FILE, NODE_EXTRA_CA_CERTS, CODEX_CA_CERTIFICATE, AWS_CA_BUNDLE, REQUESTS_CA_BUNDLE, GIT_SSL_CAINFO.
    • PWD, USER.

    Either kind is refused with `env` name "<name>" is set by the box itself, so a table cannot claim it.

The filesystem lists are a process’s filesystem grants, part of its sandbox grants: the operating system enforces them, and no policy decision covers them. [agent.filesystem] takes all eight lists. [tool.<label>.filesystem] and [mcp.<name>.filesystem] take all but metadata and exec, and Box refuses either one by name. Every list is a path array, and every list defaults to empty.

A tool or stdio MCP server needs neither list: on macOS it can test whether a path exists and read its metadata across your home, and it can run any program it can read. A server that runs on an interpreter, such as Node or Python, needs read for that interpreter’s install directory.

KeyTypeDirectory entryFile entryDefaultRequired
readpath arrayRead the treeRead the file[]No
writepath arrayWrite the tree, and read nothingWrite the file[]No
read_filepath arrayRefusedRead the file[]No
write_filepath arrayRefusedWrite the existing file[]No
listpath arrayList and stat the tree, no contentRefused[]No
metadatapath arraystat the treeRefused[]No
execpath arrayRun any binary in the treeRun the executable[]No
denypath arraySubtract the tree from every grantSubtract the file[]No

Each entry:

  • Is absolute or starts with ~/.
  • Has no .., no glob characters (* ? [ ] { }), and no control characters, and appears once.
  • Exists, isn’t a symbolic link, and is the canonical spelling. On macOS, write /private/tmp, not /tmp. A deny entry can name a missing path on macOS only.
  • Doesn’t nest inside another grant of the same operation, and isn’t covered entirely by a deny.
  • Doesn’t reach into box_dir, and, except in metadata, doesn’t enclose it.
  • If it’s a write grant, doesn’t reach the policy file. In a box with a stdio MCP server, it also doesn’t sit at or above a directory on your PATH.

A directory entry that encloses this box’s box.toml and policy file grants everything below it except the directory that holds those two files. Box refuses grants on the roots of system trees, on machine secrets, and on trees that enclose a credential store; see Grant files and directories.

Every refusal names the table and the entry, as `[agent]` filesystem entry "<entry>" is refused: <reason>. Shape refusals happen when the file loads. Existence and identity refusals happen after Box creates box_dir and before the workload starts:

EntryReason in the refusal
A relative path`filesystem.read` path "<entry>" must be absolute or start with `~/`
A pattern or a control character `filesystem.read` path "<entry>" must be exact and contain no pattern or control character
A .. component `filesystem.read` path "<entry>" carries `..`; name the path it means
A path that doesn’t exist<path> is not there: No such file or directory (os error 2)
A symbolic link<path> is a symbolic link, and the refusal names what it points at
A spelling the kernel resolves elsewhere<path> is not the path the kernel checks, which is <path>
A directory in read_file or write_file<path> is a directory; `read_file` names one file, and a tree belongs in `read`
A file in list<path> is a file, and `list` enumerates a directory tree
A file in exec that isn’t executable<path> is not an executable regular file
Two grants of one operation that nest<path> lies inside <path>, granted by `read`
A writable grant that reaches the policy file `write` reaches this box's policy file, <path>

Box prints every grant on stderr before the workload starts, under strands-box: [agent] runs <program> with no policy decision over these paths:, and names a granted credential store, such as strands-box: [tool.<label>]: exposes ~/.ssh (read). A process can always run its own command, so the list shows that grant as exec <command> (command, implicit), and removing a directory from exec doesn’t refuse it.

A write grant alone isn’t runnable. The agent’s sandbox refuses to map code from a writable grant. When the agent’s command or an interpreter it runs lies inside a writable grant, the list carries strands-box: warning: [agent] command <path> lies inside the writable grant <path>, so the process can replace the program it runs.

A [tool.<label>] applies when a program the agent’s shell starts begins with the tool’s command: the same program, then the same fixed arguments. Box resolves the program to its canonical path before it compares, and the longest matching prefix wins. A program that no table names still runs when it lies under one of the agent’s exec entries, with the agent’s own filesystem grants. Either way, starting it is a shell:spawn decision; the table by itself starts nothing.

A stdio server takes the four process keys, plus:

KeyTypeValuesDefaultRequiredMeaning
typestring"stdio"NoneYesSelects a local server.
network.contain_egressbooleantrue, falsetrueNotrue sends the server’s traffic through the egress gateway. false gives it direct network access to IP hosts, with no policy decision and no credential injection.

network.contain_egress can also be written as its own table, [mcp.<name>.network]. With false, Box records one egress:native entry when the server starts.

A stdio server starts only when a shell:spawn permit covers its command. Without one, the open fails with MCP server "<name>" may not start: <decision>, and the server doesn’t run.

KeyTypeValuesDefaultRequiredMeaning
contain_egressbooleantrue, falsetrueNotrue sends the tool’s traffic through the egress gateway. false gives it direct network access to IP hosts, with no policy decision, no credential injection, and no traffic record.

With false, Box prints a native egress line for the tool at startup, and records egress:native each time the tool runs. A program that runs under an agent exec entry, with no tool table, always goes through the gateway. See Let a tool skip the gateway.

Binds a credential to destinations. Reachability is a policy decision. Every process in the box gets the route’s placeholder, so the box, not the table, is the credential boundary.

KeyTypeValuesDefaultRequiredMeaning
destinationsstring arrayNon-empty. See Destination patterns.NoneYesWhere the credential is attached.
protocolstring"http""http"NoWhat the destination speaks. "mcp" is refused; use [mcp.<name>] with type = "http".
secret.refstringenv://NAME, aws://PROFILE, credsd://ENVIRONMENTNoneYesWhere the credential comes from.
secret.headerstringAn HTTP header nameAuthorizationNoThe header that carries the secret.
secret.prefixstringAny textBearer for Authorization, else emptyNoText before the secret.
secret.placementstringheader, basic_auth, query_paramheaderNoWhere the secret goes.
secret.paramstringA query parameter nameNoneWith query_paramThe query parameter name. Refused with any other placement.
secret.injectstringphantom, alwaysphantomNophantom attaches the real secret only to a request that carries the box’s placeholder, and refuses any other. always attaches it to every request to the destinations, with or without the placeholder. env:// only.
secret.phantom_prefixstring1 to 64 characters from A-Z a-z 0-9 - _ . ~strands_box_NoThe start of the placeholder Box gives the process, for an agent that checks its key format. env:// only.

The secret.* keys can also be written as one inline table: secret = { ref = "env://GITHUB_TOKEN", prefix = "token " }.

  • env://NAME reads the variable from the shell that runs box run. It must be set, and not only whitespace. NAME can’t be HOME, PATH, or a name env refuses.
  • aws://PROFILE signs each request with SigV4, using the profile’s static keys. A profile that uses credential_process, role_arn, or SSO is refused.
  • credsd://ENVIRONMENT signs each request with credentials from the Credentials Daemon (credsd).
  • basic_auth and query_param take no header or prefix.
  • aws:// and credsd:// take no header, prefix, placement, param, inject, or phantom_prefix. A credsd:// reference must name an environment, as in credsd://prod-inference.
  • An entry with no secret is refused: it declares only a destination, which policy already decides.
PatternMatches
hostThe host on ports 443 and 80
host:portThe host on that port
host/prefixPaths under the prefix
host/*suffixPaths ending in the suffix
*.domainEvery subdomain of the domain, not the domain itself

A prefix matches by whole segment: api.example.com/v1/chat matches /v1/chat/completions but not /v1/chatbot. Paths match case-sensitively, against the gateway’s canonical form of the path. Hosts match in lowercase.

A pattern matching every host, port 0, and two overlapping patterns are refused. A host used by an [mcp.<name>] server can’t appear in another table.

KeyTypeValuesDefaultRequiredMeaning
typestring"http"NoneYesSelects a remote server.
destinationsstring arrayNon-empty destination patternsNoneYesThe server’s address.
secrettableThe same keys as [egress.<name>] secret.*NoneNoThe credential the gateway attaches to the server’s requests.

<name> is context.input.server in mcp:call rules.

A telemetry table declares one target for decision records and the agent’s telemetry. The label is your own, and a refusal names it.

KeyTypeValuesDefaultRequiredMeaning
kindstringfile, otlpNoneYesfile appends one OTLP JSON request per line to a file. otlp sends OTLP over HTTP to a collector.
destinationstringFor file, an absolute path, ~, or a path starting with ~/, with no .. component. For otlp, the collector’s base URL.NoneYesWhere the records go.
includestring arrayOne or more of deny, permit, trace, logs, metricsEvery recordNoWhich records the target receives. An empty list or a repeated word is refused.
secret.refstringenv://NAME or secret://NAME, which means the sameNoneNoA credential Box attaches to each export. A literal value is refused.
secret.headerstringAn HTTP header nameAuthorization, with a Bearer prefixNoThe header that carries the secret. Any other header carries the bare value.

include takes these words:

WordRecords
denyEvery effective denial
permitEvery effective permit
traceThe agent’s own spans, and Box’s control records such as box_started, policy_installed, and box_stopped

The agent’s log records and metrics reach every target, whatever include says, so logs and metrics are accepted and change nothing. An empty include is refused with the target names no signal, so nothing would reach it, and a repeated word with the target names deny twice.

Without any [telemetry] table, every record goes to <box_dir>/private/telemetry/records.jsonl, which the agent can’t reach. Declaring a target replaces that default. Box doesn’t rotate the file, and doesn’t sign or chain its records, so anything running as you can edit it afterwards.

  • A file destination inside box_dir must be under box_dir/private/. Any other is refused as inside the box directory <path> but outside its private tree, where a process could truncate it.
  • Box doesn’t compare the destination with the agent’s grants, so name a file outside every path the agent can write. A file inside a granted directory is one the agent can truncate.
  • Declare one target per file. Two boxes that append to one file can tear a line.

Box sets OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, and OTEL_EXPORTER_OTLP_PROTOCOL for the agent, pointing at its own loopback receiver. The agent’s spans, logs, and metrics exist only if it turns on its own exporters, for example with OTEL_TRACES_EXPORTER = "otlp", OTEL_LOGS_EXPORTER = "otlp", and OTEL_METRICS_EXPORTER = "otlp" in [agent.env]. A switch left at none means that signal never arrives, whatever include says.

A target that fails while the box runs loses the record. Box reports no such loss, and doesn’t retry. Telemetry in the Box repository lists each record’s attributes and shows jq queries for the file.

Key~ expanded
Filesystem lists, telemetry destinationsYes
box_dir, workspace, policy, command, env.HOME, env.TMPDIRNo

Box refuses a box_dir whose record another build of Box wrote: box record is version N, but this build understands M; select an empty `box_dir`, or remove the stale box state before the next run. Remove that box_dir, or point box_dir at an empty directory, and the next box run creates a fresh record. Removing it discards the box’s policy history.

Box refuses a problem with the file’s shape when it loads the file, before any box state exists: an unknown key, an invalid name, a relative box_dir, command, workspace, env.HOME, or env.TMPDIR, an env name Box owns, a patterned, relative, or duplicated filesystem entry, an overlapping or all-hosts destination, and an egress entry with no secret or a literal one.

A box_dir whose parent doesn’t exist is refused before anything is created. Every other check that needs the surrounding OS happens after box run creates box_dir and before the workload starts: a missing workspace, an env.HOME inside or enclosing box_dir, a telemetry file inside box_dir but outside private/, a filesystem entry that doesn’t exist or is a symbolic link, and a [tool.<label>] command that names a missing file. The box directory stays behind, and the next box run reports the same refusal until you fix the file.

Keep box.toml and the policy file outside the project the agent can write. This example lives in ~/.box-config/my-service/, and runs Claude Code as Run Claude Code in a box sets it up.

~/.box-config/my-service/box.toml
name = "my-service"
box_dir = "/Users/me/.box/my-service"
policy = "policy.dw"
[agent]
# Claude Code, with its own prompts off, so the policy decides each command.
command = ["/Users/me/.local/bin/claude", "--dangerously-skip-permissions"]
workspace = "/Users/me/src/my-service"
[agent.env]
PATH = "/usr/bin:/bin"
AWS_REGION = "us-west-2"
CLAUDE_CODE_USE_BEDROCK = "1"
ANTHROPIC_MODEL = "global.anthropic.claude-opus-5"
# Claude Code's settings, sessions, and temporary files stay out of the project.
CLAUDE_CONFIG_DIR = "/Users/me/.box-config/my-service/claude/config"
CLAUDE_CODE_TMPDIR = "/Users/me/.box-config/my-service/claude/tmp"
TMPDIR = "/Users/me/.box-config/my-service/claude/tmp"
DISABLE_AUTOUPDATER = "1"
# The project is only listed, so Claude Code reaches its files through its Bash tool,
# where the policy decides each one.
[agent.filesystem]
read = ["~/.local/share/claude/versions", "~/.box-config/my-service/claude"]
exec = ["~/.local/share/claude/versions"]
write = ["~/.box-config/my-service/claude"]
list = ["~/src/my-service"]
[tool.git]
command = ["/usr/bin/git"]
env = { PATH = "/usr/bin:/bin", GIT_CONFIG_GLOBAL = "/dev/null" }
[tool.git.filesystem]
read = ["~/src/my-service", "/Library/Developer/CommandLineTools"]
write = ["~/src/my-service"]
[egress.model]
destinations = ["bedrock-runtime.us-west-2.amazonaws.com"]
secret.ref = "env://AWS_BEARER_TOKEN_BEDROCK"
[egress.github]
destinations = ["api.github.com"]
secret = { ref = "env://GITHUB_TOKEN", prefix = "token " }
# Installed with `uv tool install mcp-server-fetch`.
[mcp.fetch]
type = "stdio"
command = ["mcp-server-fetch"]
[mcp.fetch.filesystem]
read = ["~/.local/share/uv/tools/mcp-server-fetch", "~/.local/share/uv/python"]
[telemetry.local]
kind = "file"
destination = "~/box-records/my-service.jsonl"
include = ["deny", "permit", "trace"]