sbx exec, sbx cp, and sbx ports
Three verbs reach into a sandbox: exec runs a command and starts a stopped sandbox, cp moves files across the boundary, and ports binds tcp4 unless told otherwise.
A server inside m101-demo listens on port 8080. From the host you want to open it, copy a file in, and read a result out. docker exec, docker cp, and docker run -p do those jobs for a container, and sbx has the same three verbs with a changed rule in each. When you finish this section, you can run a command as any user, copy a file either way, and publish a port that localhost reaches.
sbx exec
exec: sbx exec [flags] SANDBOX COMMAND [ARG...] runs one command inside the sandbox, and it starts a stopped sandbox first. The flags follow docker exec, but detached exec with -d or --detach is not supported, so -d appears in the help only to say so. The 2025 plugin ran docker sandbox exec -d detached, which conflict C31 records. With --cloud, sbx exec rejects the flags -d, --user, and --privileged instead of ignoring them.
The command runs as the agent user, uid 1000, with the sudo and docker groups, in the workspace directory. -u root runs it as root, which the help shows for apt-get update and the capture uses to create /opt/marker-from-demo. -w sets another working directory, -e KEY=VALUE and --env-file FILE set variables for that one command, and -it opens a shell.
Inside, a sandbox knows its own name. Since v0.39.0 a sandbox gives its own identity in the environment variables SANDBOX_NAME and SANDBOX_ID, and SANDBOX_VM_ID stays as a deprecated copy of the name. WORKSPACE_DIR names the mount, and HOME is /home/agent.
$ sbx exec m101-demo sh -c 'env | sort'
…
HOME=/home/agent
…
PWD=$CAPTURE/fixtures/repo
…
SANDBOX_ID=<uuid>
SANDBOX_NAME=m101-demo
SANDBOX_VM_ID=m101-demo
…
WORKSPACE_DIR=$CAPTURE/fixtures/repo
…
[exit 0]sbx cp
cp: sbx cp SRC DST, where one side is a sandbox path written as SANDBOX:PATH and the other side is a host path. A copy from one sandbox to another is not supported. The capture's sbx cp m101-demo:/tmp/out.txt m101-demo-2:/tmp/x exits 1 with that message, so route such a copy through the host. A directory copy places the directory itself at the destination. When the destination is an existing directory, the source goes inside it, and -L follows symbolic links in the source. The sandbox path is a container path, so /tmp/in.txt in the capture sits outside the workspace mount and is deleted with the sandbox.
The one rule with a security history is copy-out. Release v0.38.0, published 2026-08-06, fixed a flaw that let a copy-out with sbx cp escape its destination. The flaw is CVE-2026-17106.
$ sbx cp $CAPTURE/work/in.txt m101-demo:/tmp/in.txt
[exit 0]
$ sbx exec m101-demo sh -c 'cat /tmp/in.txt; printf out > /tmp/out.txt'
in
[exit 0]
$ sbx cp m101-demo:/tmp/out.txt $CAPTURE/work/out.txt
[exit 0]
$ cat $CAPTURE/work/out.txt
out
[exit 0]
$ sbx cp m101-demo:/tmp/out.txt m101-demo-2:/tmp/x
error: copying between sandboxes is not supported
try: sbx cp --help
[exit 1]sbx ports
ports: sbx ports SANDBOX lists the published ports, --publish SPEC adds one, and --unpublish SPEC removes one. The spec is [[HOST_IP:]HOST_PORT:]SANDBOX_PORT[/PROTOCOL], and without a HOST_PORT the daemon picks a free port. Publishing starts a stopped sandbox before it creates the host binding, as exec does.
The rule to learn is the address family. Without a PROTOCOL, publishing uses tcp4, so the host binding is 127.0.0.1 alone. To bind both families, publish tcp explicitly. This changed in v0.42.0: a published port no longer listens on ::1 unless you name the protocol. The 2025 plugin bound both loopbacks, as conflict C39 records.
PROTOCOL | Host binding when HOST_IP is omitted |
|---|---|
| none | 127.0.0.1 as tcp4, or tcp6 on ::1 when HOST_IP is an IPv6 address |
tcp, udp | 127.0.0.1 and ::1, or 127.0.0.1 alone when the sandbox is IPv4-only |
tcp4, udp4 | 127.0.0.1 |
tcp6, udp6 | ::1 |
The capture publishes 18081:8080, gets 127.0.0.1:18081 -> 8080/tcp4, and curl answers 200 on 127.0.0.1 and fails with exit 7 on ::1.
$ sbx ports m101-demo --publish 18081:8080
Published 127.0.0.1:18081 -> 8080/tcp4
[exit 0]
$ sbx ports m101-demo
HOST IP HOST PORT SANDBOX PORT PROTOCOL
127.0.0.1 18081 8080 tcp4
[exit 0]$ curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18081/README.md
200
[exit 0]
$ curl -sS -o /dev/null -w '%{http_code}\n' 'http://[::1]:18081/README.md'
curl: (7) Failed to connect to ::1 port 18081 after <n> ms: Couldn't connect to server
000
[exit 7]--publish 8080/tcp then binds both families, each on an ephemeral host port, and sbx ports --json lists every binding as host_ip, host_port, sandbox_port, and protocol.
$ sbx ports m101-demo --publish 8080/tcp
Published 127.0.0.1:<port> -> 8080/tcp
Published [::1]:<port> -> 8080/tcp
[exit 0]
$ sbx ports m101-demo --json
…
{
"host_ip": "::1",
"host_port": "<port>",
"sandbox_port": 8080,
"protocol": "tcp"
}
]
[exit 0]Unpublish without a protocol removes the mapping in both cases: published with the same default, or published as dual-stack tcp. The capture ends with sbx ports m101-demo --json printing []. -p on sbx run applies at creation only, so change ports later with sbx ports. sbx ls prints the bindings in its PORTS column, as 127.0.0.1:18081->8080/tcp4 in the capture. In the cloud a published port gets a public URL and UDP is refused, as sbx --cloud shows.
Docker Sandboxes (sbx) and Docker Agent (docker-agent) sbx 0.47.0, docker-agent 1.149.0 · bf4169c · 2026-10-07 · Edition 2026.10