> ## Documentation Index
> Fetch the complete documentation index at: https://docs.microsandbox.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Commands

> Execute commands, stream output, and interact with sandboxes

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.

## Execute a command

Run a command and wait for it to complete. You get back the exit code, stdout, and stderr.

<CodeGroup>
  ```rust Rust theme={null}
  let output = sb.exec("python", ["-c", "print('hello')"]).await?;
  println!("{}", output.stdout()?);     // "hello\n"
  println!("{}", output.status().code); // 0
  ```

  ```typescript TypeScript theme={null}
  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
  ```

  ```python Python theme={null}
  output = await sb.exec("python", ["-c", "print('hello')"])
  print(output.stdout_text)     # "hello\n"
  print(output.success)         # True
  print(output.exit_code)       # 0
  ```

  ```go Go theme={null}
  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
  ```

  ```bash CLI theme={null}
  msb exec worker -- python -c "print('hello')"
  ```
</CodeGroup>

## Execution options

These options apply to a single execution and don't change the sandbox's defaults.

<CodeGroup>
  ```rust Rust theme={null}
  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?;
  ```

  ```typescript TypeScript theme={null}
  const output = await sb.execWith("python", (e) =>
      e.args(["compute.py"])
          .cwd("/app")
          .env("PYTHONPATH", "/app/lib")
          .timeout(30_000)
          .rlimit("nofile", 1024),
  );
  ```

  ```python Python theme={null}
  from microsandbox import Rlimit

  output = await sb.exec(
      "python",
      ["compute.py"],
      cwd="/app",
      env={"PYTHONPATH": "/app/lib"},
      timeout=30.0,
      rlimits=[Rlimit.nofile(1024)],
  )
  ```

  ```go Go theme={null}
  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),
  )
  ```

  ```bash CLI theme={null}
  msb exec worker \
    -w /app \
    -e PYTHONPATH=/app/lib \
    --timeout 30s \
    --rlimit nofile=1024 \
    -- python compute.py
  ```
</CodeGroup>

## Shell commands

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.

<CodeGroup>
  ```rust Rust theme={null}
  let output = sb.shell("ls -la /app && echo done").await?;
  ```

  ```typescript TypeScript theme={null}
  const output = await sb.shell("ls -la /app && echo done")
  ```

  ```python Python theme={null}
  output = await sb.shell("ls -la /app && echo done")
  ```

  ```go Go theme={null}
  output, err := sb.Shell(ctx, "ls -la /app && echo done")
  ```

  ```bash CLI theme={null}
  msb exec worker -- sh -c "ls -la /app && echo done"
  ```
</CodeGroup>

## Stream output

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 theme={null}
  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 theme={null}
  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;
      }
  }
  ```

  ```python Python theme={null}
  handle = await sb.exec_stream("tail", ["-f", "/var/log/app.log"])

  async for event in handle:
      match event.event_type:
          case "stdout": print(event.data.decode(), end="")
          case "stderr": print(event.data.decode(), end="", file=sys.stderr)
          case "exited": print(f"Exited: {event.code}")
  ```

  ```go Go theme={null}
  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
      }
  }
  ```
</CodeGroup>

## Interactive attach

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 theme={null}
  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 theme={null}
  // 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"),
  );
  ```

  ```python Python theme={null}
  # 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",
  )
  ```

  ```go Go theme={null}
  // 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.
  exitCode, err = sb.Attach(ctx, "python")
  if err != nil {
      return err
  }
  fmt.Println("python exited with", exitCode)
  ```

  ```bash CLI theme={null}
  # Attach to the default shell
  msb exec -t worker

  # Attach to a specific command
  msb exec -t worker -e DEBUG=1 -w /app -- python
  ```
</CodeGroup>

<Tip>
  Press `Ctrl+]` (or your configured detach keys) to detach from the session without stopping the process. The process keeps running inside the VM and you can reattach later via its session ID.
</Tip>

## Write stdin

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.

<CodeGroup>
  ```rust Rust theme={null}
  let mut handle = sb.exec_stream_with("python", |e| e.stdin_pipe().tty(true)).await?;
  let stdin = handle.take_stdin().unwrap();
  stdin.write(b"print('hello')\n").await?;
  stdin.write(b"exit()\n").await?;
  handle.wait().await?;
  ```

  ```typescript TypeScript theme={null}
  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();
  ```

  ```python Python theme={null}
  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()
  ```

  ```go Go theme={null}
  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)
  ```
</CodeGroup>

## Session IDs

Each streaming exec creates a session ID that you can use to correlate stream events and persisted log entries.

<CodeGroup>
  ```rust Rust theme={null}
  let handle = sb.exec_stream("python", ["server.py"]).await?;
  let session_id = handle.id().to_string();
  ```

  ```typescript TypeScript theme={null}
  const handle = await sb.execStream("python", ["server.py"])
  const sessionId = handle.id
  ```

  ```python Python theme={null}
  handle = await sb.exec_stream("python", ["server.py"])
  session_id = handle.id
  ```

  ```go Go theme={null}
  handle, err := sb.ExecStream(ctx, "python", []string{"server.py"})
  if err != nil {
      return err
  }
  sessionID, err := handle.ID()
  ```
</CodeGroup>
