Automates terminal TUI applications (vim, htop, lazygit, dialog) through managed PTY sessions. Use when the user needs to interact with terminal apps, edit files in vim/nano, navigate TUI menus, click terminal buttons/checkboxes, or automate CLI workflows with interactive prompts.
All flags (--name, -s, --format, etc.) MUST come BEFORE positional arguments:
# CORRECT - flags before command/arguments
pilotty spawn --name myapp vim file.txt
pilotty key -s myapp Enter
pilotty snapshot -s myapp --format text
# WRONG - flags after command (they get passed to the app, not pilotty!)
pilotty spawn vim file.txt --name myapp # FAILS: --name goes to vim
pilotty key Enter -s myapp # FAILS: -s goes nowhere useful
This is the #1 cause of agent failures. When in doubt: flags first, then command/args.
pilotty spawn vim file.txt # Start TUI app in managed session
pilotty wait-for "file.txt" # Wait for app to be ready
pilotty snapshot # Get screen text, cursor, and dimensions
pilotty key i # Enter insert mode
pilotty type "Hello, World!" # Type text
pilotty key Escape # Exit insert mode
pilotty kill # End session
pilotty spawn <command> starts the app in a background PTYpilotty wait-for <text> ensures the app is readypilotty snapshot returns the current screen statekey, type) or coordinate clickscontent_hash to observe screen changespilotty spawn <command> # Start TUI app (e.g., pilotty spawn htop)
pilotty spawn --name myapp <cmd> # Start with custom session name (--name before command)
pilotty kill # Kill default session
pilotty kill -s myapp # Kill specific session
pilotty list-sessions # List all active sessions
pilotty daemon # Manually start daemon (usually auto-starts)
pilotty stop # Stop daemon and all sessions
pilotty examples # Show end-to-end workflow example
pilotty snapshot # Full JSON with text content and hash
pilotty snapshot --format compact # JSON without text field
pilotty snapshot --format text # Plain text with cursor indicator
pilotty snapshot -s myapp # Snapshot specific session
# Wait for screen to change (eliminates need for sleep!)
HASH=$(pilotty snapshot | jq '.content_hash')
pilotty key Enter
pilotty snapshot --await-change $HASH # Block until screen changes
pilotty snapshot --await-change $HASH --settle 50 # Wait for 50ms stability
pilotty snapshot --settle 50 --strict # Exit nonzero on deadline/exit
pilotty output # Readable rendered output history
pilotty output -s myapp # Readable history for a named session
pilotty output -s myapp --ansi # Exact retained ANSI/VT bytes
Use readable output to inspect content that has left the visible viewport. Use --ansi
only when exact terminal bytes are required for replay or diagnostics.
pilotty type "hello" # Type text at cursor
pilotty type -s myapp "text" # Type in specific session
pilotty key Enter # Press Enter
pilotty key Ctrl+C # Send interrupt
pilotty key Escape # Send Escape
pilotty key Tab # Send Tab
pilotty key F1 # Function key
pilotty key Alt+F # Alt combination
pilotty key Up # Arrow key
pilotty key -s myapp Ctrl+S # Key in specific session
# Key sequences (space-separated, sent in order)
pilotty key "Ctrl+X m" # Emacs chord: Ctrl+X then m
pilotty key "Escape : w q Enter" # vim :wq sequence
pilotty key "a b c" --delay 50 # Send a, b, c with 50ms delay
pilotty key -s myapp "Tab Tab Enter" # Sequence in specific session
pilotty click 5 10 # Click at row 5, col 10
pilotty click -s myapp 10 20 # Click in specific session
pilotty scroll up # Scroll up 1 line
pilotty scroll down 5 # Scroll down 5 lines
pilotty scroll up 10 -s myapp # Scroll in specific session
pilotty resize 120 40 # Resize terminal to 120 cols x 40 rows
pilotty resize 80 24 -s myapp # Resize specific session
pilotty wait-for "Ready" # Wait for text to appear (30s default)
pilotty wait-for "Error" -r # Wait for regex pattern
pilotty wait-for "Done" -t 5000 # Wait with 5s timeout
pilotty wait-for "~" -s editor # Wait in specific session
| Option | Description |
|--------|-------------|
| -s, --session <name> | Target specific session (default: "default") |
| --format <fmt> | Snapshot format: full, compact, text |
| -t, --timeout <ms> | Timeout for wait-for and await-change (default: 30000) |
| -r, --regex | Treat wait-for pattern as regex |
| --name <name> | Session name for spawn command |
| --delay <ms> | Delay between keys in a sequence (default: 0, max: 10000) |
| --await-change <hash> | Block snapshot until content_hash differs |
| --settle <ms> | Wait for screen to be stable for this many ms (default: 0) |
| --strict | Exit 3 on capture deadline or 4 when the session exits |
| --ansi | Write exact retained ANSI/VT bytes from output |
PILOTTY_SESSION="mysession" # Default session name
PILOTTY_SOCKET_DIR="/tmp/pilotty" # Override socket directory
RUST_LOG="debug" # Enable debug logging
The snapshot command returns structured JSON describing the current screen:
{
"outcome": "immediate",
"size": { "cols": 80, "rows": 24 },
"cursor": { "row": 5, "col": 10, "visible": true },
"text": "Settings:\n [x] Notifications [ ] Dark mode\n [Save] [Cancel]",
"content_hash": 12345678901234567890
}
Use --format text for a plain text view with cursor indicator:
--- Terminal 80x24 | Cursor: (5, 10) ---
bash-3.2$ [_]
The [_] shows cursor position. Use the text content to understand screen state and navigate with keyboard commands.
Stop guessing sleep durations! Use --await-change to wait for the screen to actually update:
# Capture baseline hash
HASH=$(pilotty snapshot | jq '.content_hash')
# Perform action
pilotty key Enter
# Wait for screen to change (blocks until hash differs)
pilotty snapshot --await-change $HASH
# Or wait for screen to stabilize (for apps that render progressively)
pilotty snapshot --await-change $HASH --settle 100
Flags:
| Flag | Description |
|------|-------------|
| --await-change <HASH> | Block until content_hash differs from this value |
| --settle <MS> | After change detected, wait for screen to be stable for MS |
| -t, --timeout <MS> | Maximum wait time (default: 30000) |
| --strict | Keep evidence but exit nonzero for deadline or exited |
Every snapshot reports why it returned:
| Outcome | Meaning | Agent action |
|---------|---------|--------------|
| immediate | No wait requested | Read the current screen |
| changed | --await-change was satisfied | Continue with the new screen |
| settled | The requested stability window was satisfied | Treat the screen as stable |
| deadline | Time expired; latest evidence is included | Inspect evidence, then retry with more time if useful |
| exited | The session ended; final evidence is included | Do not send more input; inspect exit metadata or output |
Deadline and exit outcomes still exit 0 by default. Add --strict only when shell
control flow should fail for those outcomes.
Why this is better than sleep:
sleep 1 is a guess - too short causes race conditions, too long slows automation--await-change waits exactly as long as needed - no more, no less--settle handles apps that render progressively (show partial, then complete)When interacting with AI-powered TUIs (like opencode, etc.) that stream responses, you need a longer --settle time since the screen keeps updating as tokens arrive:
# 1. Capture hash before sending prompt
HASH=$(pilotty snapshot -s myapp | jq -r '.content_hash')
# 2. Type prompt and submit
pilotty type -s myapp "write me a poem about ai agents"
pilotty key -s myapp Enter
# 3. Wait for streaming response to complete
# - Use longer settle (2-3s) since AI apps pause between chunks
# - Extend timeout for long responses (60s+)
pilotty snapshot -s myapp --await-change "$HASH" --settle 3000 -t 60000
# 4. Response may be scrolled - scroll up if needed to see full output
pilotty scroll -s myapp up 10
pilotty snapshot -s myapp --format text
Key parameters for streaming:
--settle 2000-3000: AI responses have pauses between chunks; 2-3 seconds ensures streaming is truly done-t 60000: Extend timeout beyond the 30s default for longer generationsFor manual polling (not recommended), use content_hash directly:
# Get initial state
SNAP1=$(pilotty snapshot)
HASH1=$(echo "$SNAP1" | jq -r '.content_hash')
# Perform action
pilotty key Tab
# Check if screen changed
SNAP2=$(pilotty snapshot)
HASH2=$(echo "$SNAP2" | jq -r '.content_hash')
if [ "$HASH1" != "$HASH2" ]; then
echo "Screen changed - inspect the new text"
fi
pilotty uses keyboard-first navigation, just like a human would:
# 1. Take snapshot to see the screen
pilotty snapshot --format text
# 2. Navigate using keyboard
pilotty key Tab # Move to the next control
pilotty key Enter # Activate/select
pilotty key Escape # Cancel/back
pilotty key Up # Move up in list/menu
pilotty key Space # Toggle checkbox
# 3. Type text when needed
pilotty type "search term"
pilotty key Enter
# 4. Click at coordinates for mouse-enabled TUIs
pilotty click 5 10 # Click at row 5, col 10
Key insight: Read the snapshot text to understand what's on screen, then use keyboard commands to navigate.
# 1. Spawn vim
pilotty spawn --name editor vim /tmp/hello.txt
# 2. Wait for vim to load and capture baseline hash
pilotty wait-for -s editor "hello.txt"
HASH=$(pilotty snapshot -s editor | jq '.content_hash')
# 3. Enter insert mode
pilotty key -s editor i
# 4. Type content
pilotty type -s editor "Hello from pilotty!"
# 5. Wait for screen to update, then exit (no sleep needed!)
pilotty snapshot -s editor --await-change $HASH --settle 50
pilotty key -s editor "Escape : w q Enter"
# 6. Verify session ended
pilotty list-sessions
Alternative using individual keys:
pilotty key -s editor Escape
pilotty type -s editor ":wq"
pilotty key -s editor Enter
# 1. Spawn dialog checklist (--name before command)
pilotty spawn --name opts dialog --checklist "Select features:" 12 50 4 \
"notifications" "Push notifications" on \
"darkmode" "Dark mode theme" off \
"autosave" "Auto-save documents" on \
"telemetry" "Usage analytics" off
# 2. Wait for dialog to render (use await-change, not sleep!)
pilotty snapshot -s opts --settle 200 # Wait for initial render to stabilize
# 3. Inspect the screen and capture its hash
SNAP=$(pilotty snapshot -s opts)
echo "$SNAP" | jq -r '.text'
HASH=$(echo "$SNAP" | jq '.content_hash')
# 4. Navigate to "darkmode" and toggle it
pilotty key -s opts Down # Move to second option
pilotty key -s opts Space # Toggle it on
# 5. Wait for change and inspect the updated screen
pilotty snapshot -s opts --await-change $HASH | jq -r '.text'
# 6. Confirm selection
pilotty key -s opts Enter
# 7. Clean up
pilotty kill -s opts
# 1. Spawn a form application
pilotty spawn --name form my-form-app
# 2. Get snapshot to understand the visible form
pilotty snapshot -s form --format text
# 3. Tab to first input (likely already focused)
pilotty type -s form "myusername"
# 4. Tab to password field
pilotty key -s form Tab
pilotty type -s form "mypassword"
# 5. Tab to remember me and toggle
pilotty key -s form Tab
pilotty key -s form Space
# 6. Tab to Login and activate
pilotty key -s form Tab
pilotty key -s form Enter
# 7. Check result
pilotty snapshot -s form --format text
# 1. Spawn htop
pilotty spawn --name monitor htop
# 2. Wait for display
pilotty wait-for -s monitor "CPU"
# 3. Take snapshot to see current state
pilotty snapshot -s monitor --format text
# 4. Send commands
pilotty key -s monitor F9 # Kill menu
pilotty key -s monitor q # Quit
# 5. Kill session
pilotty kill -s monitor
AI-powered TUIs stream responses, requiring special handling:
# 1. Spawn the AI app
pilotty spawn --name ai opencode
# 2. Wait for the prompt to be ready
pilotty wait-for -s ai "Ask anything" -t 15000
# 3. Capture baseline hash
HASH=$(pilotty snapshot -s ai | jq -r '.content_hash')
# 4. Type prompt and submit
pilotty type -s ai "explain the architecture of this codebase"
pilotty key -s ai Enter
# 5. Wait for streaming response to complete
# - settle=3000: Wait 3s of no changes to ensure streaming is done
# - timeout=60000: Allow up to 60s for long responses
pilotty snapshot -s ai --await-change "$HASH" --settle 3000 -t 60000 --format text
# 6. If response is long and scrolled, scroll up to see full output
pilotty scroll -s ai up 20
pilotty snapshot -s ai --format text
# 7. Clean up
pilotty kill -s ai
Gotchas with AI apps:
--settle 2000-3000 because AI responses pause between chunks-t 60000 for complex promptsscroll up to see the beginningEach session is isolated with its own:
# Run multiple apps (--name must come before the command)
pilotty spawn --name monitoring htop
pilotty spawn --name editor vim file.txt
# Target specific session
pilotty snapshot -s monitoring
pilotty key -s editor Ctrl+S
# List all
pilotty list-sessions
# Kill specific
pilotty kill -s editor
The first session spawned without --name is automatically named default.
Important: The
--nameflag must come before the command. Everything after the command is passed as arguments to that command.
pilotty uses a background daemon for session management:
You rarely need to manage the daemon manually.
Errors include actionable suggestions:
| Exit | Category | Agent action |
|------|----------|--------------|
| 0 | Command completed | Inspect snapshot outcome; it may be deadline or exited without --strict |
| 1 | Generic/API error | Read the error code, message, and suggestion |
| 2 | Usage error | Fix the command syntax |
| 3 | Strict capture deadline | Inspect printed evidence and retry with more time if appropriate |
| 4 | Session lifecycle ended | Do not retry input against that session; use status, snapshot, or output |
{
"code": "SESSION_NOT_FOUND",
"message": "Session 'abc123' not found",
"suggestion": "Run 'pilotty list-sessions' to see available sessions"
}
{
"code": "SPAWN_FAILED",
"message": "Failed to spawn process: command not found",
"suggestion": "Check that the command exists and is in PATH"
}
# The pattern: capture hash, act, await change
HASH=$(pilotty snapshot | jq '.content_hash')
pilotty key Enter
pilotty snapshot --await-change $HASH --settle 50
# This replaces fragile patterns like:
# pilotty key Enter && sleep 1 && pilotty snapshot # BAD: guessing
pilotty spawn my-app
pilotty wait-for "Ready" # Ensure app is ready
pilotty snapshot # Then snapshot
pilotty snapshot --format text | grep "Error" # Check for errors
pilotty key Enter # Then proceed
pilotty wait-for "Ready" -t 5000 || {
pilotty snapshot --format text # Check what's on screen
# Adjust approach based on actual state
}
For detailed patterns and edge cases, see:
| Reference | Description | |-----------|-------------| | references/session-management.md | Multi-session patterns, isolation, cleanup | | references/key-input.md | Complete key combinations reference |
Executable workflow scripts:
| Template | Description | |----------|-------------| | templates/vim-workflow.sh | Edit file with vim, save, exit | | templates/dialog-interaction.sh | Handle dialog/whiptail prompts | | templates/multi-session.sh | Parallel TUI orchestration |
Usage:
./templates/vim-workflow.sh /tmp/myfile.txt "File content here"
./templates/dialog-interaction.sh
./templates/multi-session.sh
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer