CLI Prompt-Shape Profiles
git-paw’s supervisor and prompt detectors recognise an agent CLI’s terminal surface — its “ready” banner, its permission-prompt wording, its mid-response footer — by matching known markers against captured pane text. A prompt-shape profile is the data that describes what one CLI’s terminal looks like. It never describes what that CLI is allowed to do — see What a profile is not below.
git-paw ships one profile, embedded in the binary: Claude Code. Every detector falls back to it, field by field, so an installation with no profile configuration behaves exactly as it always has.
Why this exists
Before profiles, the markers describing Claude Code’s terminal were
compiled into half a dozen Rust modules plus a bash mirror in the bundled
sweep.sh helper. For a non-Claude CLI this was not cosmetic — an approval
marker that never matched meant command extraction silently failed, and a
three-option prompt with different wording was read as a two-option one. A
prompt-shape profile gives each of those literals one home, so adding a new
CLI’s markers is a data change, not a code change across six files.
Resolution order
For a given CLI name, each field resolves independently:
- The field’s value in that CLI’s
[clis.<name>].prompt_profiletable, if set. - Otherwise, the embedded Claude Code default.
This is per-field, not per-profile: a [clis.<name>].prompt_profile
table that sets only approval_markers still gets the embedded default for
every other field (readiness markers, mode markers, and so on). A partial
profile can never silently disable detection for the fields it omits, and a
CLI with no prompt_profile table at all — including one git-paw has never
heard of — resolves the full embedded default rather than an empty profile.
Fields
| Field | Matches | Used by |
|---|---|---|
readiness_markers | Substrings that positively identify the CLI’s interactive ready state (as opposed to a bare shell prompt), gating boot-block injection. | tmux::readiness::classify_pane_readiness (boot-prompt injection gate) |
approval_markers | Substrings indicating the CLI is waiting for an approval decision. | supervisor::permission_prompt approval-marker scan (reports only that a prompt is present) |
live_prompt_markers | The confirmation-question lead-in ("do you want to") and the cancel footer ("esc to cancel") that anchor a LIVE (not scrolled-away) permission prompt. Detection searches upward from the end of the capture, tolerating up to 13 non-blank trailing lines of non-prompt content (a task list, a status strip) below the modal — but once the anchor has shifted past the capture’s true end, liveness there requires the modal’s actual SHAPE: the footer anchoring the START of a line (not merely appearing anywhere in it) together with a numbered option line nearby. A bare confirmation question, or the footer text only mentioned mid-sentence (narration, grep-style tool output), cannot re-confirm liveness there — an already-answered prompt can leave either behind followed by ordinary output. | supervisor::permission_prompt::is_live_prompt, the sweep.sh approve re-confirm |
mid_response_markers | Substrings identifying a pane actively producing a response (its interrupt footer showing) rather than sitting at its input box. | supervisor::drive::pane_is_mid_response (unattended drive loop) |
prompt_boilerplate_markers | Lower-cased substrings marking a captured line as prompt boilerplate (the question / choices) rather than the command awaiting a decision. | supervisor::manual_approvals pattern extraction |
command_header_markers | Lead-in forms of a command-confirmation header — a whole-line header form (e.g. "Bash command") or an inline-embedding form ending in ( (e.g. "Bash("). | supervisor::permission_prompt::extract_command_slice |
file_prompt_pattern | Regex source matching the lead-in of a filesystem-operation permission prompt, capturing the target path. | supervisor::permission_prompt::extract_path_from_file_prompt |
option_line_pattern | Regex source matching a numbered option line ("1. …", "2) …") once leading decoration is stripped. | supervisor::permission_prompt::is_option_line |
broad_grant_marker | Substring identifying a durable (“don’t ask again”) broad-grant option in a multi-option prompt. | The sweep.sh approve option-1 guard: if the prompt’s first option carries this wording, pressing it would write a permanent permission rule, so the helper sends nothing. |
input_sigils | Prefixes identifying the CLI’s input-box sigil (e.g. ">", "❯"). | supervisor::drive::input_box_text (stranded-input recovery) |
mode_accept_edits_markers | Substrings identifying an accept-edits / bypass-permissions mode footer. | coordination::inventory::detect_mode |
mode_interactive_markers | Substrings identifying a visible interactive prompt. | coordination::inventory::detect_mode |
stream_error_markers | Phrases indicating the CLI’s API call failed mid-turn (transport error, timeout, disconnect). | the bundled sweep.sh helper’s stuck-shape detector |
context_bloat_pattern | Regex source matching a context-bloat clear/compact hint, capturing the token count in thousands. | the bundled sweep.sh helper’s stuck-shape detector |
paste_buffer_pattern | Regex source matching a paste-aware CLI’s “pasted text” acknowledgement. | the bundled sweep.sh helper’s stuck-shape detector |
cli_unavailable_markers | Substrings/phrases indicating the CLI itself has stopped serving — a usage/session limit ("You've hit your session limit"), an organisation spend limit ("org's monthly spend limit"), or an expired/missing login — rather than a worker being merely stuck. | the unattended drive loop (supervisor::drive::cli_unavailable_line), resolved per pane for its own CLI: an orchestrator-pane match is an immediate cli-unavailable exit, a coding-pane match a deduped escalation |
Every marker-list field is matched as a case-insensitive substring, mirroring
the behaviour each site had before profiles existed. Pattern fields are
Rust regex crate syntax.
Authoring a profile for a new CLI
Add a prompt_profile table under that CLI’s [clis.<name>] entry,
setting only the fields that differ from Claude’s terminal surface:
[clis.my-agent]
command = "my-agent"
[clis.my-agent.prompt_profile]
readiness_markers = ["my-agent ready"]
approval_markers = ["proceed? (y/n)"]
broad_grant_marker = "remember this choice"
Every field you omit falls back to the embedded Claude default (see Resolution order) — start with just the fields you know differ, and add more once you observe a detection gap in practice.
What a profile is not
A prompt-shape profile describes what a CLI’s terminal looks like. It
has no field through which it could express what a command is
allowed to do — git-paw decides that for no command anyway. What a
profile also cannot touch is the discipline around sending a keystroke.
The following stay compiled (or mirrored in the bundled sweep.sh), are
never read from a profile, and cannot be widened by one:
- the live-prompt gate’s anchoring windows and trailing-region bound — a profile supplies the marker text, never how far from the end of a capture a prompt may sit and still count as live
- the approval send gate’s fresh-capture re-confirm
- the pane-0 refusal, which holds whatever a capture contains
- the
sweep.sh approveoption-1 rule: it always answers with option1and never another digit. A profile’sbroad_grant_markercan only make the helper refuse to send (by marking option 1 as a durable grant), never make it press something else.
This boundary exists because a profile is configuration a coding agent’s own
worktree can influence (via .git-paw/config.toml); if a profile could
alter where a keystroke lands or when one is allowed, an agent could widen
the very gate meant to constrain it. If you find yourself wanting a profile
field to affect whether or where a keystroke is sent rather than how
the prompt looks, that is a sign that a compiled rule change is needed
instead.
Do not ship a guessed profile
git-paw ships only the Claude Code profile — the one CLI its detection is tested against. A profile for a CLI nobody has verified against live output is worse than no profile at all: it produces confident misdetection instead of an obvious fallback to Claude’s markers. If you author a profile for another CLI, verify it against that CLI’s actual terminal output before relying on it, and expect to iterate.
sweep.sh reads the same profile
The bundled supervisor helper (sweep.sh) resolves its stuck-shape and
live-prompt regexes via the internal git paw __prompt-profile command
rather than carrying its own copies of the markers — the same profile every
Rust detector reads. This means an authored profile applies to both the
in-process supervisor and the shell helper without being declared twice.