Skill: inertia186/headless-terminal (version 1.0.0)

--- 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 [REDACTED_SECRET] 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`

path: CHANGELOG.md
content: # Changelog

## 1.0.0

Initial public release.

### Added
- Guidance for driving hostile or full-screen terminal UIs with Montana Flynn's `ht` CLI
- Wait/synchronization decision guidance for `--wait-text`, `--wait-idle`, `--wait-exit`, and related patterns
- References for examples, key notation, recipes, troubleshooting, and wait strategy
- Guidance for cleanup, unique session naming, and failure recovery

### Changed
- Prefer ordinary shell I/O, PTY shell control, or tmux when those fit better
- Treat `ht` as a specialized tool rather than the default answer for terminal work
- Name the expected install source explicitly: `montanaflynn/headless-terminal`

### Hardening
- Added false-friend warnings for unrelated `ht` packages
- Replaced local/private hostname examples with neutral placeholders
- Added trust guidance around taps, release artifacts, and package ambiguity
- Added stronger caution around privacy-sensitive, remote, or destructive TUI flows


path: references/examples.md
content: # Examples

## Quick decision rules

Use `ht` when the terminal behaves like a screen, not a stream. Avoid hammer/nail overuse: installing `ht` does not make it the default answer for ordinary shell work.

Install the expected CLI from Montana Flynn's headless-terminal project:

```bash
brew install montanaflynn/tap/ht
```

If trust posture matters, name the repo/owner explicitly and prefer the tap or official release artifacts over random packages or pasted scripts.

Known false friends:
- Homebrew core `ht` is HTE, an executable viewer/editor/analyzer.
- npm `headless-terminal` is an old library package and may not install an `ht` command.

Good fits:
- `vim`, `nvim`, `emacs`
- `top`, `htop`, `watch`
- `git add -p`
- SSH sessions that lead into a TUI
- auth or installer flows that redraw the terminal
- REPLs where prompt timing matters

Poor fits:
- `uptime`, `ls`, `cat`, `grep`, `git status`
- one-shot commands with plain line output
- commands where `exec` with `pty=true` is sufficient
- long-lived human workspaces better handled in tmux

## Example: read a file in vim

```bash
ht run --name notes vim /tmp/notes.md
ht view notes
ht send notes ":q<CR>" --wait-exit
ht remove notes
```

## Example: remote `top` over SSH

```bash
ht run --name remote ssh [REDACTED_SECRET]
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
```

## Example: inspect patch mode safely

```bash
ht run --name addp git add -p
ht view addp
```

Then send one response at a time:

```bash
ht send addp "y" --wait-idle 200ms --view
```

or:

```bash
ht send addp "n" --wait-idle 200ms --view
```

## Example: choose a better wait

Prefer:
- `--wait-text "READY"` when a stable marker exists
- `--wait-idle 200ms` after redraw-heavy keys
- `--wait-exit` when closing the program

Avoid piling on arbitrary sleeps unless there is no real signal.

## Example: recover from uncertainty

If the last key had an unclear effect:
1. stop sending input
2. run `ht view <name>`
3. decide from the actual screen state
4. continue with one small keystroke sequence

## Example: avoid stale sessions

Use a name that will not collide with an older run:

```bash
session="vim-notes-$(date +%s)"
ht run --name "$session" vim /tmp/notes.md
ht view "$session"
ht send "$session" ":q<CR>" --wait-exit
ht remove "$session"
```

If cleanup is important, write the command sequence so `ht stop` / `ht remove` still happens after a failure.

## Example: when to abandon `ht`

If the task becomes mostly human-supervised or needs a persistent collaborative shell, switch to tmux.
If the task turns out to be normal command I/O, switch back to `exec` / `process`.

path: references/keys.md
content: # Key notation

Vim-style. Literals pass through; angle-bracket specials are parsed.

## Specials

- `<CR>` `<Enter>` — carriage return
- `<Esc>` — escape
- `<Tab>` `<S-Tab>` — tab / shift-tab
- `<BS>` — backspace
- `<Space>` — space (a literal space also works)
- `<Del>` `<Ins>`
- `<Up>` `<Down>` `<Left>` `<Right>`
- `<Home>` `<End>` `<PageUp>` `<PageDown>`
- `<F1>`..`<F12>`
- `<lt>` — literal `<` character

## Modifiers

- `<C-x>` — ctrl+x (letters a–z only)
- `<M-x>` — meta/alt+x (alias `<A-x>`; prefixes ESC 0x1b)
- `<C-M-x>` — combine modifiers in any order

Names and modifiers are case-insensitive: `<cr>` == `<CR>`, `<c-c>` == `<C-c>`.

## Common sequences

| Action | Keys |
|---|---|
| Save and quit vim | `:wq<CR>` |
| Quit vim without saving | `:q!<CR>` |
| Send SIGINT | `<C-c>` |
| Send EOF (closes stdin) | `<C-d>` |
| Vim: insert, type, escape | `ihello<Esc>` |
| Emacs: save buffer | `<C-x><C-s>` |
| Tmux prefix | `<C-b>` |

## Raw bytes

To bypass vim-style parsing entirely (useful for pasting pre-built escape sequences):

```
ht send --raw S $'\x1b[A'          # literal ESC-[-A (up arrow)
```

Use `--raw` sparingly — notation is easier to read in logs and more portable.

## Multi-argument joining

Multiple positional args concatenate:

```
ht send S "hello" "<CR>"           # same as: ht send S "hello<CR>"
```

Handy when part of the keys comes from a variable and you don't want to worry about quoting `<CR>` through a shell.


path: references/recipes.md
content: # Recipes

## Edit a file with vim

```
ht run --name v vim /tmp/notes.md
ht send --wait-idle 100ms v "ihello world<Esc>:wq<CR>"
ht wait --exit v
ht remove v
cat /tmp/notes.md
```

## Run a REPL, capture each response

```
ht run --name py python3 -i
ht send --wait-text ">>> " --view py "print(2+2)<CR>"
ht send --wait-text ">>> " --view py "import sys; sys.version<CR>"
ht send --wait-exit py "exit()<CR>"
ht remove py
```

The `--view` flag appends a snapshot to the send response. Add `--json` to get structured output (cursor, size, text).

## Drive an interactive installer

```
ht run --name i ./install.sh
ht wait --text "(y/n)" i                     # wait for first prompt
ht send --wait-text "path:" i "y<CR>"
ht send --wait-exit i "/opt/myapp<CR>"
ht remove i
```

`ht wait` (no `send`) is the cleanest way to block on a prompt for an already-running session.

## Drive a turn-based TUI (nethack, roguelikes, any curses game)

```
ht run --size 80x24 --name g nethack -u Agent
ht send --wait-text "\[yn\]" --regex --view g "y"          # dismiss startup prompt
ht send --wait-idle 200ms --view g "Fl"                    # fight left (east)
ht send --wait-text "--More--" --view g "<Space>"          # dismiss message
```

Three things that make this work:

- **One call per turn.** `--view` appends the snapshot to the send result,
  so there is no separate `ht view` step.
- **Deterministic sync.** `--wait-text "\[yn\]" --regex` blocks until the
  next prompt actually renders. `--wait-idle 200ms` alone is racy on fast
  transitions — a short idle window can return between two prompts and
  your next keystroke lands on the wrong one.
- **Cursor in the snapshot.** The trailing `cursor: R,C` line in the plain
  output tells you where the game thinks "you" are. Critical when the map
  has multiple `@` glyphs (player + human-shaped monsters like Medusa).

For death/game-over screens, chain wait-text patterns through each prompt
rather than firing blind keystrokes at idle:

```
ht send --wait-text "Die?"     --view g "y"
ht send --wait-text "identify" --view g "n"
ht send --wait-text "overview" --view g "n"
ht send --wait-text "bones"    --view g "n"
```

Plain `--wait-text` does substring match — no regex escaping needed. Add
`--regex` only when you need alternatives or character classes.

## Dismiss a modal / unexpected prompt

```
ht view S                      # see what's actually on screen
ht send S "<Esc>"              # or <CR>, <C-c>, q — program-dependent
```

When scripted automation hits a dialog you didn't anticipate, view first, guess the dismissal key, verify. Don't blindly `<CR>` — some TUIs treat that as "OK, destroy my data."

## Watch live from another pane while an agent drives

```
# Pane A — blocks until the named session is created, then live-streams it
ht watch demo

# Pane B — agent
ht run --size 100x40 --name demo htop
ht send --wait-duration 1s demo "q"
```

`ht watch` is useful during skill development — you can see exactly what the agent sees.

## Run under a specific terminal size

```
ht run --size 132x50 --name big vim big.txt
```

Default is 80x24. Use a larger size when the TUI's layout depends on having room (e.g. emacs splits, k9s dashboards).

## Pass environment or a working directory

```
ht run --cwd /tmp --env FOO=bar --env DEBUG=1 --name s my-tui
```

`--env` can repeat.

## Record a session to asciicast / GIF

```
ht record --output session.cast S     # start recording; Ctrl-C to stop
agg session.cast session.gif          # render an animated GIF
```

Output is [asciicast v2](https://docs.asciinema.org/manual/asciicast/v2/)
— standard newline-delimited JSON with a header line and `[t, "o", bytes]`
event lines. Works with `asciinema play`, `agg` (Rust, by the asciinema
authors), `svg-term-cli`, and the asciinema web player. Nothing
ht-specific; any tool that reads asciicasts will read ours.

Record a specific window of action by chaining with `send`:

```
ht record --output action.cast S &    # background recorder
REC=$!
ht send --wait-text "Done" S "<commands...>"
kill -INT $REC                        # stop when your action finishes
agg action.cast action.gif
```

## Screenshot a session to PNG

```
ht view --format png --output /tmp/session.png S
```

Rasterizes the current screen with window chrome, macOS-style dots, and a
drop shadow. Handy for:
- Letting yourself *look* at a session (Claude can read PNGs through the
  normal file-read tool).
- Attaching to bug reports or demo write-ups.
- Verifying UI changes — snapshot before and after.

Refuses to write PNG bytes to an interactive terminal (pass `--output FILE`
or redirect stdout). Colors, bold, italics, and inverse all render.

## Extract text from the screen

```
ht view --format plain S | grep -A2 "Summary:"
```

For structured extraction (need cursor position or character attributes):

```
ht view --json S | jq '.text'
```

## Pipe-aware list

```
ht list             # pretty table in a tty
ht list --json      # JSON when piped or scripted
```

`ht list` auto-detects whether stdout is a terminal and switches format — you don't usually need `--json` explicitly unless building a pipeline in a subshell.


path: references/troubleshooting.md
content: # Troubleshooting

## Exit codes

| Code | Meaning | Common cause |
|---|---|---|
| 0 | success | — |
| 1 | runtime error | session missing, daemon unreachable, IO |
| 2 | usage error | bad flags, wrong arg count |
| 3 | wait timeout | condition never fired within `--timeout` |

Scripts should special-case **3** — a timeout isn't the same as a crash. Often the right response is to `ht view` the session, see *why* the condition didn't fire, then decide whether to retry, send a recovery key, or abort.

## "I sent keys but the screen didn't change"

1. `ht view <sid>` — is the program parked on a modal or sub-prompt you didn't account for?
2. Is key notation right? `ht send <sid> "<C-c>"` — if that works, notation parsing is fine.
3. `ht list` — state column will read `exited` if the child already died.

## "ht view shows stale content"

You viewed before the program re-rendered. Always pair `send` with a wait:

```
ht send --wait-idle 100ms --view S "<Down>"
```

Never do `ht send S X && ht view S` as two separate calls — there's no synchronization between them.

## Wait always times out (exit 3)

- **`--wait-text`** times out: pattern isn't on screen. Try without `--regex` (or with, if you need escaping). Check for trailing whitespace or non-ASCII chars — `ht view --json` shows exact text.
- **`--wait-cursor`** times out: rows and columns are **1-indexed**. `1,1` is top-left.
- **`--wait-idle`** times out: the program is redrawing constantly (clock, spinner, progress bar). Switch to `--wait-text` if you have a stable string, or `--wait-duration` as a last resort.

## Stuck / zombie sessions

```
ht list                  # see everything, including exited sessions
ht kill <sid>            # SIGKILL a live session
ht remove --force <sid>  # force-drop the record even if running
```

If the daemon itself seems wedged:

```
ht daemon stop           # next ht command auto-restarts it
```

## Diagnose VT-parsing issues without daemon overhead

```
ht debug <cmd...>
```

Runs the command in the foreground, pipes it through libghostty-vt, prints the final screen when it exits. No session lifecycle, no daemon. Useful for asking "does the VT engine even render this TUI correctly" in isolation from session-management bugs.

## The daemon won't start

`ht` auto-starts the daemon on first use. If that fails:

1. Check the Unix socket path: `ls -la $TMPDIR/ht-*.sock` (macOS) or `/tmp/ht-*.sock` (Linux).
2. Stale socket from a crashed daemon? `ht daemon stop` is a no-op if it's already dead — remove the socket file manually, then retry.
3. Permissions: the socket inherits your UID's umask. If another user owns a leftover socket, you can't reuse it.


path: references/waits.md
content: # Wait strategies

The single hardest thing about driving a TUI is knowing when the program has finished reacting to your keystrokes. `ht` gives you six mechanisms; pick the most deterministic one that fits the situation.

## Decision tree

```
Does a specific string appear on screen when the program is ready?
  └─ YES →  --wait-text "PATTERN"      (add --regex for RE2)

Does the cursor land at a known position when ready?
  └─ YES →  --wait-cursor ROW,COL      (1-indexed)

Will the session exit when the work is done?
  └─ YES →  --wait-exit

Output reliably goes quiet for a predictable interval after ready?
  └─ YES →  --wait-idle 200ms          (most common fallback)

You just need *any* output to confirm the key was processed?
  └─ YES →  --wait-change

None of the above work?
  └─ --wait-duration 500ms             (last resort: unconditional sleep)
```

## Composing

Wait flags AND together. A useful combo:

```
ht send --wait-text "READY" --wait-idle 150ms S "<CR>"
```

Waits until `READY` appears AND output has been quiet for 150ms — handles the case where "READY" flashes onto the screen mid-redraw before the final state stabilizes.

## Timeout

All waits default to `--timeout 5s`. Exit code **3** means timeout. Bump for slow startup:

```
ht send --wait-text "Prompt>" --timeout 30s S "<CR>"
```

Set `--timeout 0` to disable it entirely (dangerous — wedges indefinitely if the condition never fires).

## --wait-duration vs --wait-idle

These are easy to confuse:

- `--wait-duration 300ms` — unconditionally sleep 300ms. Use when output isn't a reliable readiness signal (e.g. a persistent animation or spinner).
- `--wait-idle 300ms` — wait *until* output has been quiet for 300ms. Adaptive: slow machines naturally wait longer, fast machines return sooner.

**Prefer `--wait-idle`.** Use `--wait-duration` only when idle-detection false-fires (e.g. programs with periodic redraws or slow key echo).

## Standalone wait

The same conditions are available on `ht wait` for use outside a `send`:

```
ht wait --text "Install complete" --timeout 5m S
```

Use when you've already sent keys and want to run unrelated work before blocking on a condition. The `ht wait` flags drop the `--wait-` prefix: `--idle`, `--text`, `--cursor`, `--exit`, `--regex`, `--timeout`.

## Default send pacing

`ht send` writes one keystroke at a time with a 20ms gap by default. For most TUIs this alone is enough for echo to land correctly. Override:

```
ht send --rate 0 S "lots of text"     # send as one chunk, no pacing
ht send --rate 10ms S "..."           # tighter pacing
```

Don't tune `--rate` unless you have a specific reason — the default is picked to work across the common case.


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)