Bash Pipe

/v1/bash is a subprocess pipe-based command execution service for programmatic and agent-friendly workflows. It is different from the PTY-based Shell Terminal: Bash Pipe keeps stdout and stderr separate and supports incremental output reads with offsets.

Shell vs Bash

/v1/shell/v1/bash
BackendTerminal / PTYsubprocess pipe
Best forInteractive terminals, WebSocket UI, human takeoverAgent tool calls, short commands, long command polling
OutputOne combined output fieldSeparate stdout and stderr
Read modelTerminal snapshots and waitsIncremental reads with offset / stderr_offset
stdinTerminal input streamWrites to the running process stdin pipe
Command identitySession-levelEach command has a command_id

Execute Short Commands

Most tool calls can complete with a single exec request. status is the command lifecycle state; exit_code is the command success indicator.

curl -X POST "http://localhost:8080/v1/bash/exec" \
  -H "Content-Type: application/json" \
  -d '{
    "command": "pwd && ls -la",
    "exec_dir": "/home/gem",
    "timeout": 30,
    "hard_timeout": 120
  }'

Typical response shape:

{
  "success": true,
  "data": {
    "session_id": "SESSION_ID",
    "command_id": "COMMAND_ID",
    "command": "pwd && ls -la",
    "status": "completed",
    "stdout": "/home/gem\n...",
    "stderr": null,
    "exit_code": 0,
    "offset": 128,
    "stderr_offset": 0
  }
}

SDK Examples

Python:

from agent_sandbox import Sandbox

client = Sandbox(base_url="http://localhost:8080")

result = client.bash.exec(
    command='printf "out\\n" && printf "err\\n" >&2',
    timeout=30,
    hard_timeout=120,
).data

print(result.stdout)
print(result.stderr)
print(result.exit_code)

TypeScript:

import { SandboxClient } from "@agent-infra/sandbox";

const client = new SandboxClient({ baseUrl: "http://localhost:8080" });

const response = await client.bash.exec({
  command: 'printf "out\\n" && printf "err\\n" >&2',
  timeout: 30,
  hard_timeout: 120,
});

if (!response.ok) {
  throw new Error("Bash exec failed");
}

console.log(response.body.data?.stdout);
console.log(response.body.data?.stderr);
console.log(response.body.data?.exit_code);

Long Commands And Incremental Output

When a command is still running after timeout, /v1/bash/exec returns status: "running" and the command continues in the background. Use /v1/bash/output to read incremental output.

# 1. Start a command. If it is not done within 1 second, it returns running.
curl -X POST "http://localhost:8080/v1/bash/exec" \
  -H "Content-Type: application/json" \
  -d '{
    "command": "sleep 3; echo done",
    "timeout": 1,
    "hard_timeout": 30
  }'
# 2. Continue reading with the returned session_id, offset, and stderr_offset.
curl -X POST "http://localhost:8080/v1/bash/output" \
  -H "Content-Type: application/json" \
  -d '{
    "session_id": "SESSION_ID",
    "offset": 0,
    "stderr_offset": 0,
    "wait": true,
    "wait_timeout": 10
  }'

wait=false returns the currently available output immediately. wait=true long-polls until new output arrives, the command finishes, or wait_timeout expires. For agent polling, prefer wait=true to avoid unnecessary empty polling.

Long-Lived Processes

For services that do not exit on their own, use async_mode: true, read startup logs, and call /kill when done.

curl -X POST "http://localhost:8080/v1/bash/exec" \
  -H "Content-Type: application/json" \
  -d '{
    "command": "python3 -m http.server 3000 --directory /tmp",
    "async_mode": true,
    "hard_timeout": 3600
  }'

Read logs:

curl -X POST "http://localhost:8080/v1/bash/output" \
  -H "Content-Type: application/json" \
  -d '{
    "session_id": "SESSION_ID",
    "offset": 0,
    "stderr_offset": 0,
    "wait": true,
    "wait_timeout": 5
  }'

Stop the process:

curl -X POST "http://localhost:8080/v1/bash/kill" \
  -H "Content-Type: application/json" \
  -d '{
    "session_id": "SESSION_ID",
    "signal": "SIGTERM"
  }'

Write To stdin

/v1/bash/write writes to the stdin pipe of the running process. Use it for cat, prompt-driven scripts, REPLs, and similar programs. For line-buffered programs, include \n at the end of each line.

# Start a process that waits for stdin.
curl -X POST "http://localhost:8080/v1/bash/exec" \
  -H "Content-Type: application/json" \
  -d '{
    "command": "cat",
    "async_mode": true,
    "hard_timeout": 120
  }'
# Write one line of input.
curl -X POST "http://localhost:8080/v1/bash/write" \
  -H "Content-Type: application/json" \
  -d '{
    "session_id": "SESSION_ID",
    "input": "hello from stdin\n"
  }'
# Read the echoed output.
curl -X POST "http://localhost:8080/v1/bash/output" \
  -H "Content-Type: application/json" \
  -d '{
    "session_id": "SESSION_ID",
    "offset": 0,
    "wait": true,
    "wait_timeout": 5
  }'

Some programs write prompts to stderr, including some REPLs and interactive commands. Consumers should inspect both stdout and stderr.

Session State

/v1/bash creates a new process for each exec. The same session_id preserves API-level state, such as the default working directory, but command-internal cd and export calls do not affect later requests.

Set the session default working directory with exec_dir:

curl -X POST "http://localhost:8080/v1/bash/exec" \
  -H "Content-Type: application/json" \
  -d '{
    "command": "pwd",
    "exec_dir": "/tmp"
  }'

Reuse the same session:

curl -X POST "http://localhost:8080/v1/bash/exec" \
  -H "Content-Type: application/json" \
  -d '{
    "session_id": "SESSION_ID",
    "command": "pwd && echo ${MY_VAR:-unset}"
  }'

If later commands should continue in a directory, pass exec_dir again or update the API-level session default. Do not rely on cd inside a command.

Parameters

Common POST /v1/bash/exec parameters:

ParameterTypeDescription
commandstringShell command to execute
session_idstringTarget session; auto-created when omitted
exec_dirstringAbsolute working directory; updates the session default
envobjectExtra environment variables for this command only
async_modebooleanReturn running immediately when true
timeoutnumberSoft timeout; returns running while the command continues in the background
hard_timeoutnumberHard timeout; kills the process and returns timed_out
max_output_lengthnumberMaximum inline stdout / stderr length for sync responses; default 50000, set 0 to disable truncation for the request

Common POST /v1/bash/output parameters:

ParameterTypeDescription
session_idstringTarget session
command_idstringOptional target async command
offsetnumberstdout byte offset to read from
stderr_offsetnumberstderr byte offset to read from
waitbooleanWhether to long-poll for new output
wait_timeoutnumberMaximum wait time when wait=true

Error Handling

HTTP success for /v1/bash means the request was accepted by the service. It does not mean the command itself succeeded. Recommended order:

  1. Check the HTTP status code.
  2. Check response success.
  3. Use data.status for the command lifecycle.
  4. When status="completed", use exit_code for command success.
import requests

response = requests.post(
    "http://localhost:8080/v1/bash/exec",
    json={"command": "python3 missing.py", "timeout": 30},
)
response.raise_for_status()

payload = response.json()
result = payload["data"]

if result["status"] == "running":
    print("command is still running; call /v1/bash/output")
elif result["status"] == "completed" and result["exit_code"] != 0:
    print("command completed with a non-zero exit code")
elif result["status"] in {"timed_out", "killed"}:
    print(f"command interrupted: {result['status']}")
  • POST /v1/bash/exec
  • POST /v1/bash/output
  • POST /v1/bash/write
  • POST /v1/bash/kill
  • GET /v1/bash/sessions
  • POST /v1/bash/sessions/create
  • POST /v1/bash/sessions/{session_id}/close