Documentation Index
Fetch the complete documentation index at: https://docs.declaw.ai/llms.txt
Use this file to discover all available pages before exploring further.
Stdio keeps stdin open on a process inside the sandbox so you can
send data incrementally and receive stdout/stderr as it’s produced.
Unlike commands.run, which waits for the process
to exit before returning output, stdio lets you:
- Send input line by line — pipe data into
cat, wc, jq, a
database CLI, or any process that reads from stdin interactively.
- Receive output as it arrives — via callbacks or an iterator, not
as one blob at the end.
- Separate stdout and stderr — independent callbacks for each stream.
- Close stdin to signal EOF — the process sees end-of-file on its
stdin, just like pressing Ctrl-D in a terminal.
- Kill long-running processes — terminate a process mid-execution
and retrieve the exit code.
Stdio is the right tool for REPLs, MCP servers, database shells,
language servers, and any process that reads from stdin in a loop.
When to use Stdio vs Commands vs PTY
Use commands.run when… | Use stdio.start when… | Use pty.create when… |
|---|
| Command takes all input upfront and exits | Command reads from stdin interactively | Command needs a real terminal (ANSI, raw mode) |
| You only need the final result | You need to send data mid-execution | You need keystroke-level input |
pip install, go build, pytest | cat, wc, jq, database CLI, MCP server | vim, htop, ssh, gh auth login |
Default to commands.run. Use stdio.start when you need to pipe
data into a running process. Use pty.create only when the command
requires a real terminal.
Architecture
stdio.start uses four REST endpoints plus one SSE stream:
| Operation | Method / route |
|---|
| Start process | POST /sandboxes/{id}/stdio — returns a cmd_id |
| Send stdin | POST /sandboxes/{id}/stdio/{cmd_id}/stdin |
| Close stdin | POST /sandboxes/{id}/stdio/{cmd_id}/stdin/close |
| Kill process | DELETE /sandboxes/{id}/stdio/{cmd_id} |
| Live output | GET /sandboxes/{id}/stdio/{cmd_id}/stream (SSE, base64-encoded frames) |
The SSE stream emits event: stdout and event: stderr frames with
base64-encoded data, plus a final event: exit frame with the exit code.
Quick start
from declaw import Sandbox
sbx = Sandbox.create()
try:
# Start a process with an open stdin pipe
chunks = []
proc = sbx.stdio.start("cat", on_stdout=lambda d: chunks.append(d))
# Send data and close stdin
proc.send_stdin("hello from stdio!\n")
proc.close_stdin()
# Wait for the process to exit
result = proc.wait(timeout=10)
print(b"".join(chunks).decode().strip()) # "hello from stdio!"
print(result.exit_code) # 0
finally:
sbx.kill()
import { Sandbox } from "@declaw/sdk";
const sbx = await Sandbox.create();
try {
const chunks: Uint8Array[] = [];
const proc = await sbx.stdio.start("cat", {
onStdout: (d) => chunks.push(d),
});
await proc.sendStdin("hello from stdio!\n");
await proc.closeStdin();
const result = await proc.wait();
const dec = new TextDecoder();
console.log(chunks.map((c) => dec.decode(c)).join("").trim());
console.log(result.exitCode);
} finally {
await sbx.kill();
}
In Go, output callbacks are provided when calling Stream(), not at
start time.ctx := context.Background()
sbx, _ := declaw.Create(ctx)
defer sbx.Kill(ctx)
handle, _ := sbx.Stdio.Start(ctx, "cat", nil)
handle.SendStdin(ctx, []byte("hello from stdio!\n"))
handle.CloseStdin(ctx)
var output []byte
result, _ := handle.Stream(ctx, &declaw.StdioStreamOpts{
OnStdout: func(data []byte) { output = append(output, data...) },
})
fmt.Println(string(output)) // "hello from stdio!"
fmt.Println(result.ExitCode) // 0
Multi-round interaction
Send multiple lines of input to a process that reads in a loop:
replies = []
proc = sbx.stdio.start(
"sh -c 'while read line; do echo \"reply: $line\"; done'",
on_stdout=lambda d: replies.append(d),
)
for i in range(5):
proc.send_stdin(f"message {i}\n")
proc.close_stdin()
result = proc.wait(timeout=10)
print(b"".join(replies).decode())
# reply: message 0
# reply: message 1
# ...
const replies: Uint8Array[] = [];
const proc = await sbx.stdio.start(
"sh -c 'while read line; do echo \"reply: $line\"; done'",
{ onStdout: (d) => replies.push(d) },
);
for (let i = 0; i < 5; i++) {
await proc.sendStdin(`message ${i}\n`);
}
await proc.closeStdin();
await proc.wait();
handle, _ := sbx.Stdio.Start(ctx,
`sh -c 'while read line; do echo "reply: $line"; done'`, nil)
for i := 0; i < 5; i++ {
handle.SendStdin(ctx, []byte(fmt.Sprintf("message %d\n", i)))
}
handle.CloseStdin(ctx)
handle.Wait(ctx)
Environment variables and working directory
proc = sbx.stdio.start(
"sh -c 'echo $GREETING from $(pwd)'",
envs={"GREETING": "hello"},
cwd="/tmp",
on_stdout=lambda d: print(d.decode().strip()),
)
proc.wait()
# hello from /tmp
const out: Uint8Array[] = [];
const proc = await sbx.stdio.start(
"sh -c 'echo $GREETING from $(pwd)'",
{
envs: { GREETING: "hello" },
cwd: "/tmp",
onStdout: (d) => out.push(d),
},
);
await proc.wait();
handle, _ := sbx.Stdio.Start(ctx,
"sh -c 'echo $GREETING from $(pwd)'",
&declaw.StdioStartOpts{
Envs: map[string]string{"GREETING": "hello"},
Cwd: "/tmp",
})
handle.Wait(ctx)
Killing a process
import time
proc = sbx.stdio.start("sleep 300")
time.sleep(1)
killed = proc.kill() # True
result = proc.wait()
print(result.exit_code) # -1
const proc = await sbx.stdio.start("sleep 300");
await new Promise((r) => setTimeout(r, 1000));
const killed = await proc.kill(); // true
const result = await proc.wait();
console.log(result.exitCode); // -1
handle, _ := sbx.Stdio.Start(ctx, "sleep 300", nil)
time.Sleep(time.Second)
handle.Kill(ctx)
result, _ := handle.Wait(ctx)
fmt.Println(result.ExitCode) // -1
Next steps