sbxenv.yaml and `sbx env plan`
An environment file declares the agent, kits, workspace, secrets, ports, MCP servers, and host commands, and sbx env plan prints every change before create or run applies it.
A new contributor clones your repository and needs the sandbox you use. That means the shell workload, the hello-mixin, one published port, a greeting variable, and a host command that prepares the workspace. You could send a list of flags. An environment file sends the same thing as one checked-in file, and the contributor sees a plan before anything runs.
When you finish this section, you can write an sbxenv.yaml, read its plan symbol by symbol, and say which edits wait for the next create.
The file
Environment file: a sbxenv.yaml that declares one sandbox and the host resources around it, read by the five sbx env commands. The capture kit wrote this one:
schemaVersion: "1"
name: m101-env
agent: shell
workspace: .
args:
greeting:
default: hello
description: Greeting word
kits:
- source: ../kits/hello-mixin
env:
GREETING: ${{ env.args.greeting }}
lifecycle:
initialize:
- command: echo init
ports:
- sandbox: 8080
host: 18083| Key | What it declares |
|---|---|
schemaVersion, name, agent | the file version "1", the sandbox name, the built-in agent or kit |
args | inputs with default or required: true, read as ${{ env.args.NAME }} |
kits | mixins, as a reference or as source plus args |
workspace, additionalWorkspaces | the host directories to mount, with an object form for clone mode |
env | variables for the sandbox |
secrets, bindings, registries | credentials with value, ref, or command, and the domains they reach |
mcp.servers | MCP servers registered with the gateway |
ports | published ports, sandbox, host, protocol, hostIP |
lifecycle | host commands under initialize, postCreate, preRemove |
sandboxOptions | template, memory, cpus, skills, writableEnvFiles, and more |
The keys come from the file reference and from the command's own help. Three placeholders expand, ${{ env.args.NAME }}, ${{ env.projectDir }}, and ${{ env.fileDir }}, and a $ anywhere else is literal text. A relative kit path or workspace resolves against the directory of the file that declares it, so workspace: . mounts the file's own directory. A file with no workspace mounts nothing.
The plan
sbx env plan reads the file and prints what applying it would set up. It applies, approves, and records nothing.
── ENVIRONMENT PLAN
m101-env
kits:
+ - source: $CAPTURE/fixtures/kits/hello-mixin
workspace: (present, not recorded as applied, needs your approval)
path: $CAPTURE/fixtures/env
env:
+ GREETING: hello
+ sandbox:
+ name: m101-env
+ agent: shell
ports:
+ - sandbox: 8080
+ protocol: tcp4
+ host: 18083
lifecycle:
initialize:
+ - command: echo init
+ workdir: $CAPTURE/fixtures/env
+ envFiles:
+ - $CAPTURE/fixtures/env/sbxenv.yaml
Plan: + 5 to add, ~ 0 to change, - 0 to destroy.
this plan runs commands on this machine, outside the sandbox, with your own privileges
✓ not approved yet; applying asks onceThe plan has the shape of your file, and the margin carries the verdict. Five symbols exist. + adds, ~ changes, and - destroys. > marks a command that runs again, and ! marks a resource the environment applied and no longer declares.
A row that is unchanged and already approved is left out. A literal secret value appears as its SHA-256 digest. The tcp4 protocol was not in the file: published ports default to it since v0.42.0. Figure 4.4 follows the file through the plan to the state record.
Host commands and approval
The lifecycle block runs on the host, outside the sandbox, with your own privileges. initialize runs on every create and every run, postCreate once the sandbox exists, and preRemove after you confirm sbx env rm. Commands run through your shell from the project directory, with workdir and timeout per command. An environment with any of them asks on every invocation, even one that does not run them. The reason is that approving a command also trusts the script it calls. --skip-host-commands runs none of them, --auto-approve answers yes once, and sbx settings set env.rememberHostCommands true asks again only when a command changes.
The state record and the second plan
sbx records what you approved for each environment in its state directory, not next to the file. A later invocation asks only about what changed. The exact path was not captured, because the kit ran plan and never create. The second capture changed one input instead:
$ sbx env plan --env-arg greeting=servus ./fixtures/env
…
+ GREETING: servus
…
Plan: + 5 to add, ~ 0 to change, - 0 to destroy.Every row is still +, because nothing was applied. After a create, the same edit prints ~ GREETING: hello -> servus and nothing else, in the form the help text shows for a changed kit argument. Changes to workspaces, kits, ports, secrets, bindings, and sandboxOptions wait for the next create. New env values reach the next session.
Several PATH arguments deep-merge in order, and a later file wins over an earlier one, and lists such as ports concatenate rather than override. With no PATH, a .sbxenv.yaml in your home directory merges underneath as a base layer. A hidden .sbxenv.yaml in the project is no longer read.
rm, the read-only file, and --cloud
sbx env rm removes the sandbox and the secrets provisioned at its scope. Bindings stay, because they belong to the user. To remove the bindings that this environment declares too, pass --prune-bindings. The file itself is bound read-only inside the sandbox, because an agent that can edit it decides what the next plan asks about. sandboxOptions.writableEnvFiles: true lifts that.
With --cloud, workspace, additionalWorkspaces, and clone are refused, because they name host directories that a cloud sandbox cannot mount. Also, host port bindings, MCP definitions, and dynamic secret sources are rejected before any host command runs. The cloud section shows what remains.
Docker Sandboxes (sbx) and Docker Agent (docker-agent) sbx 0.47.0, docker-agent 1.149.0 · bf4169c · 2026-10-07 · Edition 2026.10