Execution
Hive gives you two ways to run commands inside a sandbox: a blocking call that waits for the process to finish, and a streaming call that delivers output in real time. Both accept the same options; the difference is in how you receive the result.
exec() — blocking
sandbox.exec() runs a command and waits for it to exit. It returns a single object with the complete buffered output.
import { getOrCreateSandbox } from "hive";
const sandbox = await getOrCreateSandbox("my-sandbox", {
fs: [{ backend: "local", mount: "/workspace", acls: [{ path: "/workspace/**", access: "rw" }] }],
});
const result = await sandbox.exec("npm test", { cwd: "/workspace" });
console.log(result.stdout); // buffered stdout
console.log(result.stderr); // buffered stderr
console.log(result.exit_code); // process exit codeThe return value has three fields:
| Field | Type | Description |
|---|---|---|
stdout | string | Complete standard output from the process. |
stderr | string | Complete standard error from the process. |
exit_code | number | Exit code returned by the process. |
Use exec() when you only care about the final result and the command completes in a reasonable time. For long-running commands or when you need to display progress as it happens, use execStream() instead.
execStream() — real-time streaming
sandbox.execStream() starts a process and returns an ExecProcess handle immediately — before the process exits. Output frames arrive through proc.pipes as the process writes them.
const proc = await sandbox.execStream("npm test");
for await (const chunk of proc.pipes) {
if (chunk.stdout) process.stdout.write(chunk.stdout);
if (chunk.stderr) process.stderr.write(chunk.stderr);
}
const code = await proc.exitCode;
console.log("exited with", code);The ExecProcess handle exposes:
| Member | Type | Description |
|---|---|---|
pipes | AsyncIterable<{ stdout?: string; stderr?: string }> | Stream of output chunks. Each chunk carries a stdout or stderr string (or both). The iterable ends when the process exits. |
exitCode | Promise<number> | Resolves to the process exit code once the process has finished. |
writeStdin(data: string) | Promise<void> | Sends data to the process's standard input. |
You can await proc.exitCode before or after the for await loop. If you await it first without draining proc.pipes, output may be buffered — drain the stream first to avoid missing frames.
Interactive processes with writeStdin
writeStdin lets you send input to a process that is still running — useful for REPLs, CLI wizards, or any program that reads from stdin mid-execution.
const proc = await sandbox.execStream("python3 -i");
// Consume output in the background while we write to stdin
const reading = (async () => {
for await (const chunk of proc.pipes) {
process.stdout.write(chunk.stdout ?? "");
}
})();
await proc.writeStdin("print('hello')\n");
await proc.writeStdin("exit()\n");
await reading;
const code = await proc.exitCode;Start draining proc.pipes before calling writeStdin so the process does not block on a full output buffer. The pattern above — spawning the drain loop in the background and then writing — is the standard approach.
TTY allocation
Pass tty: true to allocate a pseudo-terminal for the process. Programs that check whether they are attached to a terminal (color output, progress bars, interactive prompts) will behave as if running in a real terminal session.
const proc = await sandbox.execStream("bash", { tty: true });When a TTY is allocated, stdout and stderr are merged into a single stream — all output arrives on chunk.stdout. The chunk.stderr field will be undefined.
Working directory
Both exec() and execStream() accept a cwd option. The process starts in that directory. If the path does not exist inside the sandbox, the command returns a non-zero exit code.
// Blocking
const result = await sandbox.exec("go test ./...", { cwd: "/workspace" });
// Streaming
const proc = await sandbox.execStream("go test ./...", { cwd: "/workspace" });Next: Agent CLIs