4.4

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:

the environment file the capture kit wroteyamlFull log
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
KeyWhat it declares
schemaVersion, name, agentthe file version "1", the sandbox name, the built-in agent or kit
argsinputs with default or required: true, read as ${{ env.args.NAME }}
kitsmixins, as a reference or as source plus args
workspace, additionalWorkspacesthe host directories to mount, with an object form for clone mode
envvariables for the sandbox
secrets, bindings, registriescredentials with value, ref, or command, and the domains they reach
mcp.serversMCP servers registered with the gateway
portspublished ports, sandbox, host, protocol, hostIP
lifecyclehost commands under initialize, postCreate, preRemove
sandboxOptionstemplate, 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.

the first plan for m101-envtextFull log
── 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 once
The LOAD ENVIRONMENT block is cut.

The 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.

Fig. 4.4an environment file becomes a plan, an approval, and a state recordflow
An environment file becomes a plan, an approval, and a state record Top: the captured sbxenv.yaml with its agent, workspace, args, kits, env, lifecycle, and ports keys. An arrow for sbx env plan leads to the plan it printed, eight rows with a plus sign in the margin for each addition, the workspace row that needs approval, the totals line, and the warning that host commands run outside the sandbox. Below, two branches. Left: approval on sbx env create writes the state record under the sbx state directory, and a later plan prints only the rows that moved. Right: the same plan with the env-arg greeting set to servus still prints every row as an addition because nothing was applied. THE FILE (SBXENV.YAML) sbxenv.yaml schemaVersion 1, name m101-env, agent shell, workspace ., args greeting (default hello), kits ../kits/hello-mixin, env GREETING: ${{ env.args.greeting }}, lifecycle initialize echo init, ports sandbox 8080 host 18083 sbx env plan ./fixtures/env ENVIRONMENT PLAN m101-env (14-env-plan.txt) + kits: source $CAPTURE/fixtures/kits/hello-mixin workspace: $CAPTURE/fixtures/env (not recorded as applied, needs your approval) + 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 Plan: + 5 to add, ~ 0 to change, - 0 to destroy. runs commands on this machine, outside the sandbox, with your own privileges margin symbols: + add, ~ change, - destroy, > run, ! forget (help-sbx sbx env) AFTER APPROVAL (NOT CAPTURED) ONE ARGUMENT CHANGED (CAPTURED) you answer y on sbx env create host commands are asked on every invocation state record, per environment in the sbx state directory, not by the file a later plan shows what moved ~ GREETING: hello -> servus, nothing else sbx env create records the plan sbx env plan --env-arg greeting=servus the arg replaces the default hello + env: GREETING: servus still + 5 to add: nothing was applied one argument
sbx env plan turns the file into margin-marked rows, approval on create records them, and a later plan prints only the rows that moved. Read top down. The plan rows are the env plan capture, and the right branch is the second plan capture, with the greeting argument changed. The left branch after approval follows the help text of sbx env, because the kit never applied the plan.

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:

the same file with one argument changedtextFull log
$ sbx env plan --env-arg greeting=servus ./fixtures/env
…
+    GREETING: servus
…
   Plan: + 5 to add, ~ 0 to change, - 0 to destroy.
Only the changed row and the totals are shown.

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