docs/sandboxes/commands.mdx
Command execution doesn't use SSH or the network. There's a dedicated channel between the host and a guest agent running inside the VM, completely separate from the sandbox's network stack. The agent spawns processes, streams output back, and reports exit codes.
One nice consequence: exec works even when a sandbox has networking fully disabled.
Sandbox creation is boot-only: configuring an image, ENTRYPOINT, or CMD does not execute that workload. Use the default-workload methods when you want the SDK to resolve the effective OCI ENTRYPOINT + CMD and run it. A configured CMD override replaces the image CMD, while the effective entrypoint is preserved. If neither field supplies an executable, these methods return the typed NoDefaultCommand error instead of falling back to a shell.
const sb = await Sandbox.builder("worker")
.image("example/worker:latest")
.cmd(["worker.py", "--once"])
.create();
const output = await sb.execDefault();
sb = await Sandbox.create(
"worker",
image="example/worker:latest",
cmd=["worker.py", "--once"],
)
output = await sb.exec_default()
sb, err := m.CreateSandbox(ctx, "worker",
m.WithImage("example/worker:latest"),
m.WithCmd("worker.py", "--once"),
)
output, err := sb.ExecDefault(ctx)
Streaming variants are exec_default_stream, execDefaultStream, and ExecDefaultStream. Interactive variants are attach_default, attachDefault, and AttachDefault. Literal exec and attach continue to ignore ENTRYPOINT and CMD.
Run a command and wait for it to complete. You get back the exit code, stdout, and stderr.
<CodeGroup> ```rust Rust let output = sb.exec("python", ["-c", "print('hello')"]).await?; println!("{}", output.stdout()?); // "hello\n" println!("{}", output.status().code); // 0 ```const output = await sb.exec("python", ["-c", "print('hello')"]);
console.log(output.stdout()); // "hello\n"
console.log(output.success); // true
console.log(output.code); // 0
output = await sb.exec("python", ["-c", "print('hello')"])
print(output.stdout_text) # "hello\n"
print(output.success) # True
print(output.exit_code) # 0
output, err := sb.Exec(ctx, "python", []string{"-c", "print('hello')"})
fmt.Print(output.Stdout()) // "hello\n"
fmt.Println(output.Success()) // true
fmt.Println(output.ExitCode()) // 0
msb exec worker -- python -c "print('hello')"
These options apply to a single execution and don't change the sandbox's defaults.
<CodeGroup> ```rust Rust let output = sb.exec_with("python", |e| e .args(["compute.py"]) .cwd("/app") .env("PYTHONPATH", "/app/lib") .timeout(Duration::from_secs(30)) .rlimit(RlimitResource::Nofile, 1024) ).await?; ```const output = await sb.execWith("python", (e) =>
e.args(["compute.py"])
.cwd("/app")
.env("PYTHONPATH", "/app/lib")
.timeout(30_000)
.rlimit("nofile", 1024),
);
from microsandbox import Rlimit
output = await sb.exec(
"python",
["compute.py"],
cwd="/app",
env={"PYTHONPATH": "/app/lib"},
timeout=30.0,
rlimits=[Rlimit.nofile(1024)],
)
output, err := sb.Exec(ctx, "python", []string{"compute.py"},
m.WithExecCwd("/app"),
m.WithExecEnv(map[string]string{"PYTHONPATH": "/app/lib"}),
m.WithExecTimeout(30*time.Second),
)
msb exec worker \
-w /app \
-e PYTHONPATH=/app/lib \
--timeout 30s \
--rlimit nofile=1024 \
-- python compute.py
Run a command through the sandbox's configured shell (defaults to /bin/sh). Useful for pipelines, redirects, and other shell syntax that exec doesn't interpret.
const output = await sb.shell("ls -la /app && echo done")
output = await sb.shell("ls -la /app && echo done")
output, err := sb.Shell(ctx, "ls -la /app && echo done")
msb exec worker -- sh -c "ls -la /app && echo done"
For long-running processes or large output, streaming gives you stdout, stderr, and exit events as they happen instead of buffering everything until the command finishes.
<CodeGroup> ```rust Rust let mut handle = sb.exec_stream("tail", ["-f", "/var/log/app.log"]).await?;while let Some(event) = handle.recv().await { match event { ExecEvent::Stdout(data) => print!("{}", String::from_utf8_lossy(&data)), ExecEvent::Stderr(data) => eprint!("{}", String::from_utf8_lossy(&data)), ExecEvent::Exited { code } => break, _ => {} } }
```typescript TypeScript
const handle = await sb.execStream("tail", ["-f", "/var/log/app.log"]);
for await (const event of handle) {
switch (event.kind) {
case "stdout": process.stdout.write(event.data); break;
case "stderr": process.stderr.write(event.data); break;
case "exited": console.log(`Exited: ${event.code}`); break;
}
}
from microsandbox import ExecEventType
handle = await sb.exec_stream("tail", ["-f", "/var/log/app.log"])
async for event in handle:
match event.event_type:
case ExecEventType.STDOUT: print(event.data.decode(), end="")
case ExecEventType.STDERR: print(event.data.decode(), end="", file=sys.stderr)
case ExecEventType.EXITED: print(f"Exited: {event.code}")
handle, err := sb.ExecStream(ctx, "tail", []string{"-f", "/var/log/app.log"})
for {
event, err := handle.Recv(ctx)
if err != nil {
return err
}
switch event.Kind {
case m.ExecEventStdout:
os.Stdout.Write(event.Data)
case m.ExecEventStderr:
os.Stderr.Write(event.Data)
case m.ExecEventExited:
fmt.Printf("Exited: %d\n", event.ExitCode)
case m.ExecEventDone:
return nil
}
}
Bridges your terminal directly to a process inside the sandbox for a fully interactive PTY session. Useful for debugging, running REPLs, or anything that expects a real terminal.
<CodeGroup> ```rust Rust let exit_code = sb.attach_shell().await?;let exit_code = sb.attach_with("python", |a| a .env("DEBUG", "1") .cwd("/app") .detach_keys("ctrl-q") ).await?;
```typescript TypeScript
// Attach to the default shell
const exitCode = await sb.attachShell();
// Attach to a specific command with custom detach keys
await sb.attachWith("bash", (a) =>
a.env("DEBUG", "1").cwd("/app").detachKeys("ctrl-q"),
);
# Attach to the default shell
exit_code = await sb.attach_shell()
# Attach to a specific command with custom detach keys
exit_code = await sb.attach(
"python",
env={"DEBUG": "1"},
cwd="/app",
detach_keys="ctrl-q",
)
// Attach to the default shell.
exitCode, err := sb.AttachShell(ctx)
if err != nil {
return err
}
fmt.Println("shell exited with", exitCode)
// Attach to a specific command with options.
exitCode, err = sb.AttachWith(ctx, "python", nil,
m.WithAttachUser("dev"),
m.WithAttachEnv(map[string]string{"DEBUG": "1"}),
m.WithAttachCwd("/app"),
m.WithAttachDetachKeys("ctrl-q"),
)
if err != nil {
return err
}
fmt.Println("python exited with", exitCode)
# Attach to the default shell
msb exec -t worker
# Attach to a specific command
msb exec -t worker -e DEBUG=1 -w /app -- python
Streaming handles can also accept stdin. Enable stdin_pipe and write bytes to it. Combined with tty: true, this lets you drive interactive processes programmatically.
const handle = await sb.execStreamWith("python", (e) => e.stdinPipe().tty(true));
const stdin = await handle.takeStdin();
await stdin!.write("print('hello')\n");
await stdin!.write("exit()\n");
await stdin!.close();
await handle.wait();
from microsandbox import Stdin
handle = await sb.exec_stream("python", stdin=Stdin.pipe(), tty=True)
stdin = handle.take_stdin()
await stdin.write(b"print('hello')\n")
await stdin.write(b"exit()\n")
await stdin.close()
await handle.wait()
handle, err := sb.ExecStream(ctx, "python", nil, m.WithExecStdinPipe())
stdin := handle.TakeStdin()
_, _ = stdin.Write([]byte("print('hello')\n"))
_, _ = stdin.Write([]byte("exit()\n"))
_ = stdin.Close()
_, err = handle.Wait(ctx)
Each streaming exec creates a session ID that you can use to correlate stream events and persisted log entries.
<CodeGroup> ```rust Rust let handle = sb.exec_stream("python", ["server.py"]).await?; let session_id = handle.id().to_string(); ```const handle = await sb.execStream("python", ["server.py"])
const sessionId = handle.id
handle = await sb.exec_stream("python", ["server.py"])
session_id = handle.id
handle, err := sb.ExecStream(ctx, "python", []string{"server.py"})
if err != nil {
return err
}
sessionID, err := handle.ID()