Drive hostile or full-screen terminal UIs through the `ht` CLI from montanaflynn/headless-terminal. Use when an agent needs reliable PTY-backed interaction,...
--- name: headless-terminal description: Drive hostile or full-screen terminal UIs through the `ht` CLI from montanaflynn/headless-terminal. Use when an agent needs reliable PTY-backed interaction, screen snapshots, or wait/synchronization for tools like `vim`, `top`, `htop`, `git add -p`, SSH-driven TUIs, installers, auth prompts, or REPLs. Prefer this over plain shell I/O when redraw behavior, alternate screen handling, cursor state, or deterministic waits matter; fall back to exec/process or tmux for ordinary shell commands and human-operated sessions. --- # Headless Terminal ## Overview Use `ht` from <https://github.com/montanaflynn/headless-terminal> as the special-purpose tool for terminal programs that behave badly under plain stdin/stdout control. It gives a real PTY, terminal-state snapshots, and explicit wait conditions so the agent can drive a TUI without guessing when the screen settled. Do not let the hammer make everything look like a nail. `ht` is powerful but heavyweight; if a normal command, pipe, `exec` with `pty=true`, or tmux session fits better, use that instead. ## Workflow 1. Confirm `ht` exists before committing to this path. 2. Decide whether the task actually needs `ht`; explicitly reject hammer/nail overuse and try simpler shell or host-agent primitives first when they fit. 3. Create a uniquely named session with `ht run`. 4. Send the smallest meaningful keystrokes. 5. Wait for a deterministic condition. 6. Snapshot with `ht view`. 7. Stop and remove the session when done, unless the user explicitly wants a persistent session. ## Choose `ht` vs other tools Use `ht` when: - a program needs a real tty and redraws the whole screen - alternate-screen behavior matters - cursor position or screen state matters - the task needs reliable waits after keys are sent - you want a text or PNG snapshot of the terminal state Prefer `exec` / `process` when: - the command is ordinary shell I/O - output is line-oriented and does not depend on screen state - no full-screen TUI is involved - `exec` with `pty=true` is enough to satisfy a TTY check without needing screen snapshots Prefer tmux when: - a human will monitor or resume the session directly - persistence and shared human visibility matter more than terminal snapshots ## Preflight Check availability first: ```bash command -v ht ``` If `ht` is missing, say so plainly and switch to another tool or ask whether to install it. The expected install source is Montana Flynn's headless-terminal: ```bash brew install montanaflynn/tap/ht ``` Without Homebrew, use a release tarball from <https://github.com/montanaflynn/headless-terminal/releases> that matches the host OS/architecture, then put the `ht` binary on `PATH`. Security/trust posture for publishing: - Tap installs and release tarballs are still trust decisions; name the repo/owner explicitly. - Prefer the tap or release artifacts over random packages or copy-pasted scripts. - If a user is security-sensitive, suggest reviewing the GitHub repo and release page before installation. Important disambiguation: not every package named `ht` or `headless-terminal` is this CLI. On macOS/Homebrew, the core formula `ht` refers to HTE, a viewer/editor/analyzer for executables, not this terminal automation tool. The public npm package `headless-terminal` is an old library and may not provide the expected `ht run` / `ht send` CLI. Do not install either as a guess; verify that the candidate explicitly supports the commands this skill uses. ## Core commands ```bash ht run --name demo-$(date +%s) <cmd...> ht send demo "keys..." --wait-idle 200ms --view ht view demo ht view demo --format png > screenshot.png ht wait demo --wait-text "READY" ht stop demo ht remove demo ``` Treat command names and flags as version-sensitive. If `ht --help` is available, check it before relying on less-common flags such as PNG output or cursor waits. ## Waiting strategy This is the main reason to use `ht`. Prefer, in order: 1. `--wait-text` when a known string should appear 2. `--wait-cursor` when the cursor position is predictable 3. `--wait-idle` when the app redraws and then settles 4. `--wait-duration` only when nothing better exists Do not rely on blind sleeps when a real wait condition is available. ## Practical patterns ### Drive vim safely ```bash ht run --name notes vim /tmp/notes.md ht send notes "ihello<Esc>" --wait-idle 200ms --view ht send notes ":wq<CR>" --wait-exit ht remove notes ``` ### Drive a remote TUI over SSH ```bash ht run --name remote ssh user@host.example ht send remote "top<CR>" --wait-idle 500ms --view ht send remote "q" --wait-idle 200ms ht send remote "exit<CR>" --wait-exit ht remove remote ``` ### Inspect `git add -p` ```bash ht run --name addp git add -p ht view addp ``` Then send one choice at a time and wait after each response. ## Operating guidance - Use unique named sessions so follow-up commands stay readable and do not collide with older runs. - Send the minimum keystrokes needed; avoid giant pasted blobs. - After any state-changing input, capture a fresh view before assuming success. - If the screen looks wrong, inspect with `ht view` before sending more keys. - Clean up exited sessions with `ht remove`. - Ask before using `ht` for privacy-sensitive auth flows, remote systems, or destructive TUI operations. A real PTY can make it easy to do real damage quickly. ## Failure modes - If the program exits immediately, check the command, working directory, and whether the program refuses non-interactive/unknown terminals. - If waits time out, use a different wait condition instead of stacking longer sleeps. - If the captured screen is stale or blank, check whether the app uses an alternate screen, requires a larger terminal size, or has already exited. - If a task is simple enough for plain shell control, stop using `ht` and simplify. - If the session is for a human to keep around, tmux is usually the better container. ## References - `references/examples.md`: quick fit checks, sample command patterns, and wait-strategy examples - `references/keys.md`: vim-style key notation such as `<CR>`, `<Esc>`, arrows, control/meta keys, and raw bytes - `references/waits.md`: wait strategy decision tree and timeout guidance - `references/recipes.md`: recipes for vim, REPLs, installers, watch, terminal sizing, screenshots, and recording - `references/troubleshooting.md`: exit codes, stale views, wait timeouts, zombies, daemon issues, and `ht debug`
don't have the plugin yet? install it then click "run inline in claude" again.
extracted implicit decision logic into explicit decision points, added external connections section, documented failure modes as recovery paths, clarified session cleanup and naming strategy, added edge cases (alternate screen, terminal sizing, zombie sessions, wait timeouts), preserved original procedure with explicit inputs and outputs per step.
use ht from montanaflynn/headless-terminal when you need a real PTY to drive full-screen terminal programs that break under plain stdin/stdout. this skill matters when redraw behavior, alternate-screen handling, cursor state, or deterministic waits are critical to success. examples: vim, top, htop, git add -p, SSH-driven TUIs, interactive installers, auth prompts, or REPLs. do not use this for ordinary shell commands, line-oriented output, or human-monitored persistent sessions (use tmux for that instead).
tool availability:
ht binary on PATH from montanaflynn/headless-terminal (verify with command -v ht)brew install montanaflynn/tap/ht on macOS or download release tarball from https://github.com/montanaflynn/headless-terminal/releasesheadless-terminal is an old library; always verify the candidate supports ht run / ht send commandshost environment:
external connections:
context:
demo-$(date +%s) to avoid collisions)preflight check: run command -v ht to confirm the binary is on PATH. if missing, stop and report that ht is not installed; offer to install from the tap or release tarball (do not guess or install random packages named "ht" or "headless-terminal"). if security is a concern, suggest the user review the GitHub repo and release page first.
evaluate tool choice: decide whether ht is actually needed. if the task is ordinary shell I/O, line-oriented output without screen state dependency, or no full-screen TUI is involved, use exec with pty=true or plain shell instead. if a human will monitor or resume the session, prefer tmux. stop here if a simpler tool fits.
create session: run ht run --name <unique-name> <command...> to spawn the target program in a new named session. example: ht run --name notes vim /tmp/notes.md. capture the session name for all follow-up commands.
send keystrokes: use ht send <session-name> "<keys>" --wait-<condition> --view to send input and wait. use the smallest meaningful keystroke sequence; avoid giant pasted blobs. prefer wait conditions in this order: --wait-text "string" (known string appears), --wait-cursor <line>,<col> (cursor lands at predictable position), --wait-idle <duration> (app redraws then settles, e.g. 200ms), --wait-duration <duration> (only as last resort). always include --view to capture the screen after input.
inspect state: after each keystroke batch, read the captured view to confirm the screen settled and the program responded correctly. if the screen looks wrong, use ht view <session-name> to get a fresh snapshot before sending more keys. if the screen is stale or blank, check whether the app uses alternate screen mode, requires a larger terminal size, or has already exited.
stop and clean up: when the task is done, send exit sequence (e.g., :wq<CR> for vim or exit<CR> for SSH) with --wait-exit to let the program shut down. then run ht remove <session-name> to tear down the session. if the user explicitly requests a persistent session, skip cleanup and hand off the session name.
ht vs simpler tools:
exec or process with pty=true instead of ht.exec with pty=true.missing ht binary:
command -v ht fails, stop and report plainly that ht is not installed.brew install montanaflynn/tap/ht (macOS) or a release tarball (other OS).wait strategy:
--wait-text "string".--wait-cursor <line>,<col>.--wait-idle <duration> (typical: 200ms to 500ms for most TUIs).--wait-duration or blind sleeps unless nothing better exists; always prefer deterministic conditions.program fails or exits immediately:
session cleanup:
--wait-exit succeeds), always run ht remove <session-name> to clean up the session.screen capture format:
ht view <session-name> returns plain text (UTF-8) representation of the terminal buffer.ht view <session-name> --format png returns binary PNG image of the rendered screen (if supported by ht version).session state:
ht remove <session-name> is called or the program exits and cleanup runs.send, view, wait, stop) reference the session by name.success criteria:
ht view returns non-empty buffer.stop, remove) complete without error.immediate feedback:
ht view <session-name> shows the current screen buffer; inspect it to confirm the program is running and responsive.ht send output includes the wait result (success, timeout, or error); if success, the program responded to the keystroke.visual confirmation:
:wq prompt before exit, git add -p shows the next hunk, SSH prompt is visible).completion:
ht stop <session-name> returns successfully (program has exited or signal was sent).ht remove <session-name> returns successfully and the session is gone (no error on follow-up commands).failure indicators:
command -v ht fails: ht is not installed.ht run exits immediately: program refused to start (check command, working directory, terminal type).ht send wait times out: screen did not reach expected state; use a different wait condition or inspect with ht view.ht view returns blank or stale buffer: program may use alternate screen, need larger terminal, or have already exited.ht list or similar (if available).ht run --name notes vim /tmp/notes.md
ht send notes "ihello<Esc>" --wait-idle 200ms --view
ht send notes ":wq<CR>" --wait-exit
ht remove notes
ht run --name remote ssh user@host.example
ht send remote "top<CR>" --wait-idle 500ms --view
ht send remote "q" --wait-idle 200ms
ht send remote "exit<CR>" --wait-exit
ht remove remote
ht run --name addp git add -p
ht view addp
# then send one choice at a time:
ht send addp "y" --wait-idle 300ms --view
ht send addp "n" --wait-idle 300ms --view
# repeat until done, then clean up
ht remove addp
ht run --name demo htop
ht view demo --format png > screenshot.png
ht send demo "q" --wait-exit
ht remove demo
demo-$(date +%s)) so follow-up commands stay readable and do not collide.ht send call; avoid giant pasted blobs that make waits unpredictable.ht view before assuming success.ht view before sending more keys.ht remove immediately; do not let zombies pile up.program exits immediately:
ht view <session-name> to see the last screen state; may reveal error messages.TERM=xterm or TERM=xterm-256color before ht run.waits time out:
--wait-idle 5s to --wait-text "READY").ht view to inspect what the screen actually shows; the condition may be wrong or the program may have crashed.captured screen is stale or blank:
ht run --cols 200 --rows 50 (if supported by ht version).ht send with a dummy keystroke; if it fails, the session is dead.session cleanup issues:
ht remove fails, the session may be orphaned or the daemon may have hung.ht stop <session-name> first to force the program to exit, then ht remove.ht debug output (if available) or restart the ht daemon.<CR> for carriage return, <Esc> for escape, <C-c> for control-c, <Meta-x> for alt-x, arrows with <Up> <Down> <Left> <Right>)--wait-text for known strings, --wait-cursor for predictable cursor positions, --wait-idle for app settle time (200ms to 500ms typical), and --wait-duration only as last resort