How Terminal Tool Execution Works in the Strix Sandbox: A Deep Dive
The Strix sandbox implements terminal tool execution by spawning isolated tmux-based Bash sessions per agent, using a custom PS1 prompt to capture exit codes and output, and returning structured JSON through a thread-safe session manager.
The usestrix/strix repository provides agents with a persistent, stateful command-line environment through its Terminal tool. This system runs inside a Docker-based sandbox and allows LLM agents to execute arbitrary Bash commands while maintaining working directory state, environment variables, and background processes across multiple interactions.
Architecture Overview
The terminal implementation follows a three-layer architecture that separates session management from low-level terminal control:
-
Session Manager (
strix/tools/terminal/terminal_manager.py): Maintains a thread-safe mapping of agent IDs to session IDs toTerminalSessionobjects. Handles on-demand creation, lifecycle cleanup, and exposes the public API (execute_command,close_session,list_sessions). -
Terminal Session (
strix/tools/terminal/terminal_session.py): Wraps a tmux server and pane, configures the custom prompt[STRIX_$?]$, and manages all read/write operations through the tmux interface. -
Tool-Level API: Provides JSON-serializable interfaces that the Strix runtime invokes when processing LLM-generated tool calls.
Each agent receives its own collection of named terminal sessions, with default as the standard session identifier. The sandbox container pre-installs security tools and inherits the filesystem, network, and environment, ensuring that files and working directories persist across command invocations within the same session.
The Command Execution Flow
When an LLM requests terminal tool execution, the request flows through several distinct phases before returning structured output.
Session Lookup and Creation
The process begins when TerminalManager.execute_command() receives a tool call. The manager invokes _get_or_create_session() to fetch the session for the current agent (retrieved via get_current_agent_id()).
If the requested terminal_id does not exist, the system instantiates a new TerminalSession object. During initialization, the session builds a tmux server (libtmux.Server()), creates an isolated tmux session with a unique name, and spawns a Bash pane at /bin/bash. The session immediately configures a custom PS1 value of [STRIX_$?]$ , which embeds the last command's exit status directly in the prompt string.
Command Dispatch
Once the session is active, execute_command() forwards the request to TerminalSession.execute(). The system handles two distinct cases:
- Empty commands:
_handle_empty_command()returns the current shell output without executing new input, useful for checking the present working directory or pending output. - New commands:
_execute_new_command()writes the command to the tmux pane usingpane.send_keys(command, enter=should_add_enter), then enters a polling loop.
The polling mechanism repeatedly calls _get_pane_content() until the custom PS1 marker appears (indicating command completion) or a timeout expires. Special keys like Ctrl-C or arrow keys are detected via _is_special_key and transmitted without additional newlines, enabling interactive tool usage.
Output Parsing and Exit Code Extraction
Raw pane content processing occurs through several specialized methods:
- Splitting by markers:
_matches_ps1_metadata()identifies prompt boundaries using the regex patternPS1_PATTERN = r"\[STRIX_(\d+)\]". - Output stitching:
_combine_outputs_between_matches()extracts only the output belonging to the current command, discarding previous prompts and stray carriage returns. - Command stripping:
_get_command_output()removes the echoed command string from the result and handles "continue" runs when timeouts force partial results. - Exit code extraction:
_extract_exit_code_from_matches()parses the numeric exit status from the captured PS1 marker.
The manager returns a standardized dictionary containing:
content: Cleaned stdout/stderr outputstatus:"completed","running"(timeout), or"error"exit_code: Numeric code ornullworking_dir: Current directory tracked inTerminalSession._cwd
Why tmux Powers the Isolation
The Strix sandbox relies on tmux rather than simple subprocess execution for three critical capabilities:
- Process isolation: Each session runs in its own tmux server, guaranteeing that concurrent agents cannot interfere with each other's processes or filesystem state.
- State persistence: Tmux retains the working directory, environment variables, and background jobs even when the LLM pauses between commands, enabling multi-step workflows like
cd /tmp && wget ... && unzip .... - Interactive control: The pane-based architecture supports special key sequences and signal handling required for interactive security tools and long-running scanners.
The Custom Prompt Parsing Mechanism
The custom PS1 format [STRIX_$?]$ serves as a synchronization and metadata mechanism. By searching for the pattern \[\STRIX_(\d+)\]\$ , the session can:
- Detect precisely when the shell finishes command execution and returns to the prompt
- Extract the exit code without executing separate
$?queries that would pollute the output stream - Distinguish between legitimate command output and the prompt itself, which would otherwise appear as noise in the returned content
This approach eliminates race conditions common in terminal automation where output truncation or prompt injection corrupts results.
Working with the Terminal Tool API
Below is a practical example demonstrating terminal tool execution within the Strix codebase:
from strix.tools.terminal.terminal_manager import get_terminal_manager
# 1️⃣ Grab the global manager (singleton)
tm = get_terminal_manager()
# 2️⃣ Execute a simple command in the default session
result = tm.execute_command(
command="whoami",
timeout=10.0,
terminal_id="default",
)
print("🖥️ Output:", result["content"])
print("✅ Status:", result["status"])
print("🔢 Exit code:", result.get("exit_code"))
# 3️⃣ Run a long-running scanner with partial output
scan = tm.execute_command(
command="nuclei -u https://example.com -t /usr/local/nuclei-templates/",
timeout=5.0,
)
print("\n--- Partial scanner output ---")
print(scan["content"])
print("Status:", scan["status"])
# 4️⃣ List all active sessions for the current agent
print("\nActive sessions:", tm.list_sessions())
Typical JSON response structure:
{
"content": "root\n",
"command": "whoami",
"terminal_id": "default",
"status": "completed",
"exit_code": 0,
"working_dir": "/workspace"
}
When commands exceed the timeout threshold, the status field returns "running" and the content includes captured output plus a continuation note indicating the command remains active.
Session Lifecycle Management
The TerminalManager registers cleanup handlers via _register_cleanup_handlers(), which uses atexit to kill all tmux sessions when the process exits (close_all_sessions()). Agents can manually close specific sessions via close_session() or prune dead sessions through cleanup_dead_sessions(), which handles cases where commands crash the underlying tmux session.
Summary
- Thread-safe session management:
TerminalManagerinstrix/tools/terminal/terminal_manager.pymaintains isolated session maps per agent using singleton patterns and automatic cleanup handlers. - Tmux-based isolation: Each terminal session runs in a dedicated tmux server and pane, providing process isolation and state persistence across commands.
- Custom PS1 parsing: The
[STRIX_$?]$prompt format enables reliable exit code extraction via regexr"\[STRIX_(\d+)\]"and precise command completion detection. - Structured JSON output: The system returns standardized payloads containing cleaned output, exit codes, working directories, and status indicators for both completed and running commands.
- Interactive support: Special key detection and timeout handling enable both batch scripts and interactive security tools like
nucleior custom scanners.
Frequently Asked Questions
How does the Strix sandbox maintain terminal state between commands?
The sandbox utilizes tmux sessions via TerminalSession objects that persist in memory as long as the agent is active. Because tmux maintains the Bash process, environment variables, working directory (TerminalSession._cwd), and background jobs remain intact between execute_command() calls. The session manager stores these objects in a thread-safe map keyed by agent ID and terminal ID, ensuring state survival across multiple LLM interactions.
What happens when a terminal command times out?
When a command exceeds the specified timeout in TerminalManager.execute_command(), the polling loop in _execute_new_command() returns a partial result with status set to "running". The content field contains all output captured up to that point plus a notification that the command continues executing. The tmux session remains active, allowing subsequent calls to retrieve additional output or send signals like Ctrl-C to interrupt the process.
How does the system extract exit codes without executing extra commands?
Rather than running echo $? as a separate command (which would generate additional output), Strix configures a custom PS1 prompt format [STRIX_$?]$ where $? expands to the previous command's exit code. The output parser searches for the regex pattern r"\[STRIX_(\d+)\]" to extract the numeric exit status directly from the prompt line, eliminating race conditions and output pollution.
Can multiple agents share the same terminal session?
No. The session manager maintains separate session registries per agent ID (retrieved via get_current_agent_id()). While agents can create multiple named sessions (such as default, scanner-1, scanner-2), each agent's session map is isolated from others. This design prevents cross-agent interference while allowing individual agents to run concurrent operations across multiple terminal contexts.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →