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.

index.ts
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 code

The return value has three fields:

FieldTypeDescription
stdoutstringComplete standard output from the process.
stderrstringComplete standard error from the process.
exit_codenumberExit 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.

index.ts
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:

MemberTypeDescription
pipesAsyncIterable<{ 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.
exitCodePromise<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.

index.ts
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.

index.ts
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.

index.ts
// Blocking
const result = await sandbox.exec("go test ./...", { cwd: "/workspace" });

// Streaming
const proc = await sandbox.execStream("go test ./...", { cwd: "/workspace" });

Next: Agent CLIs