Skip to content

feat(hermes): answer Hermes's dangerous-command approval with a Chat card - #2888

Draft
devswha wants to merge 5 commits into
mainfrom
lane/issue-857
Draft

devswha wants to merge 5 commits into
mainfrom
lane/issue-857

Conversation

@devswha

@devswha devswha commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

Summary

Chat now answers Hermes's dangerous-command approval with the existing approval card. Before this, Chat showed the generic "Waiting for input" card.

Hermes's classic CLI is its default interface (0.21.5). It draws the approval as a boxed panel over its input. No reader recognized that panel, so a blocked Hermes pane fell through to the generic fallback card:

  • its body was a dump of the screen;
  • its options were raw ↑, ↓, Enter, Esc (Enter takes whichever row is highlighted, which is "Allow once" when the panel opens);
  • its id changed on every poll, because the panel's countdown and the status bar ticked inside it.

A new reader in server/dialogs/hermes.ts turns the panel into an approval card:

  • title: the panel's own title, "Dangerous Command";
  • question: Hermes's own warning (the description, e.g. "recursive delete");
  • body: the command, line by line as the panel wraps it, nothing re-joined;
  • options: Hermes's own choices in its order: Allow once, Allow for this session, Add to permanent allowlist, Deny. The shorter menus Hermes offers also read (once/session/deny, once/deny). "Show full command" only redraws the panel, so it is left off the card.
  • answer: moves to the chosen row with Hermes's own ↑/↓, then presses Enter. A digit is never sent, because in Hermes a digit selects and confirms at once. The existing answer path re-reads the screen before the Enter and goes on only when the card id is unchanged and the cursor is on the chosen row; otherwise it returns 409 prompt_changed. The card id doesn't include the cursor or the ticking countdown.

The card is offered only while Hermes is the pane's foreground process (pane.process_info). When Hermes is suspended with Ctrl+Z, herdr still names the pane hermes while a shell holds it. Every read the answer makes before a key repeats this check. On Windows, which lists no foreground process, it reads as before, the same as omp and pi.

It fails closed. These get no card, so the Terminal answers:

  • a command Hermes cut (… (choose Show full command), … (command truncated — …));
  • a description Hermes cut (… (description truncated)), or dropped without a marker on a short pane;
  • a box whose rows are out of line (a newline or tab in a command breaks them), or whose top is clipped;
  • labels other than Hermes's English ones, a numbering gap, or not exactly one cursor;
  • anything under the panel other than Hermes's own spinner line, hint, status bar, rules and the ⚠ input line it holds. That covers text typed into the input, an extra widget, and output under the input.

None of them get the generic card either. registry.waits is true whenever Hermes's input line starts with the ⚠ Hermes puts there while one of its modals holds the input. That mark is hard-coded, so this works in every language, with a draft typed in, with a hint cut by a pane narrower than 33 columns, and for a command's own confirmation (/clear and the like). That card was unusable anyway: the countdown on the hint changes its id every second. For the same reason, a Chat send is refused with agent_blocked while such a modal holds the input; herdr's blocked status mostly did that already.

Scope, for the lead to confirm (the issue asks that the supported layouts be agreed):

  • Supported: the classic CLI, English, default skin.
  • Other languages get no card and no generic card; they stay for the Terminal.
  • Unchanged from main: Hermes's Ink TUI (hermes --tui) draws a different approval box and has no ⚠ input line, so it still gets the generic card.
  • No new setting, no client or UI change, no HTTP/WS contract change. The card is the existing kind: "approval" InteractivePrompt; only a server-internal responder id is added.

Thanks to @RikiSanjayaa for the request and the clear safety requirements in #857.

Fixes #857

Repro (origin/main 50c348b)

Real Hermes 0.21.5 ran in an isolated setup:

  • herdr session lane857 under its own XDG_CONFIG_HOME;
  • a mkdtemp HERMES_HOME with approvals.mode: manual;
  • a local fake OpenAI-compatible endpoint that asks for terminal → rm -rf ./scratch;
  • an Ovis server with a mkdtemp state dir on 127.0.0.1:0.

herdr reported the pane blocked. GET /api/pane/prompt returned kind: "menu", fallback: true, options ↑ ↓ Enter Esc. Three polls 1.5 s apart gave three different ids (a35c3cc0446f, ce8a5c2da848, f055f34631ab). The server logged is blocked on a screen no reader knows; fallback card.

After (same setup, this branch)

  • Panes 120 and 38 columns wide, a 15-row pane and a phone-attached 41-column pane all read as approval cards. A 14-row pane, where Hermes drops the warning, gets no card and no fallback.
  • The id stays the same across polls (fb03ee811b40 three times).
  • Real answers through POST /api/pane/prompt/answer:
    • Deny: Hermes printed → denied.
    • Allow once: → allowed once, and ./scratch was removed.
    • Allow for this session, starting with the cursor on Deny (two moves up, then Enter): → allowed for session.
  • Tapping Deny on the card in a 390-px browser closed the card, and Hermes printed → denied.
1280 390
Before (main) before 1280 before 390
After after 1280 after 390

All shots come from the isolated test session.

Tests

  • server/dialogs/hermes-approval.test.ts runs on nine screens recorded from the real panes, in server/fixtures/hermes-approval/, plus variants derived from them for each refusal above and for the shorter menus. These tests failed on main (7 of 10 at the time; the other 3 assert "no card", which main already met).

  • server/prompt.test.ts adds a route-level test with withPane:

    • Deny sends down down down enter, each move seen before the Enter;
    • a changed command gets 409 with no key sent;
    • a cut panel gets no card at all;
    • the panel's text behind a suspended Hermes gets no card;
    • Hermes suspended between the card and the answer gets 409 with no key sent.

    It failed on main, which returned kind: "menu". Each later case also failed before its fix.

  • server/dialogs/registry.test.ts snapshot: the new reader added.

Validation

  • bun run check fast passed on the pushed head 2e359ce1, rebased on main e6977348: workflow syntax, generated types and themes, typecheck, build, and unit tests (11623 pass, 0 fail, 46 skip).
  • The first CI run failed on two things outside this PR: src/lib/i18n part files over their line cap (main then, fixed by fix(i18n): keep settings part files under the line cap #2886), and server/pending-input.contract.test.ts "checks a new blocked state after the typed ESC's settling gap", a Claude-agent timing test this change doesn't reach. After the rebase, that test passes locally on the isolated test herdr (bun run check run bun test ./server/pending-input.contract.test.ts -t "settling gap", 2 pass).
  • Live, after the review fixes: Hermes suspended with Ctrl+Z (herdr still agent: hermes), with cat <panel>; sleep in its shell, got no card. On main it got a full approval card. The real pending approval in another pane still got its card.
  • Head 64f40926 (origin/main 0c414311 merged in as 707926d3, plus one comment-only commit): bun run check fast passed (unit tests 11922 pass, 0 fail, 46 skip; workflow syntax, generated types and themes, typecheck, build ok).
  • Also ran bun test server/dialogs/ server/hermes*.test.ts server/prompt.test.ts server/question-steps.test.ts. All pass except one test: server/hermes.contract.test.ts "selects the pane process's store…" (Hermes stand-in did not become the pane's foreground process). It fails the same way with this change stashed, so it's an environment issue on this host and not part of check fast.
  • Skipped: bun run check full, and the Windows and Mac checks. Nothing here is platform-specific: one server-side parser and the fallback gate.

Merge danger

Door: two-way. No state, setting or contract change.

Blast radius: Hermes panes whose input a modal holds (an approval or a command's confirmation). A screen the reader doesn't fully recognize now gets no card, where it used to get the generic card, which flickered and couldn't be answered. One more herdr pane.process_info call is made per read of a Hermes approval card. Ink-TUI Hermes and every other agent are unchanged.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 11 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 0c41431 and 64f4092.

Files selected for processing: 19

  • changelog.d/hermes-approval-card.md
  • server/dialogs/hermes-approval.test.ts
  • server/dialogs/hermes.ts
  • server/dialogs/live.ts
  • server/dialogs/registry.test.ts
  • server/dialogs/registry.ts
  • server/dialogs/shared.ts
  • server/fixtures/hermes-approval/compact-14-rows.txt
  • server/fixtures/hermes-approval/compact-15-rows.txt
  • server/fixtures/hermes-approval/cut-38.txt
  • server/fixtures/hermes-approval/expanded-38.txt
  • server/fixtures/hermes-approval/long-38.txt
  • server/fixtures/hermes-approval/long-cursor-38.txt
  • server/fixtures/hermes-approval/once-120.txt
  • server/fixtures/hermes-approval/once-38.txt
  • server/fixtures/hermes-approval/tall-41.txt
  • server/hermes.ts
  • server/prompt.test.ts
  • server/prompt.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

devswha added a commit that referenced this pull request Nov 5, 2026
Review of #2888. Suspended with Ctrl+Z, Hermes leaves its shell in front
while herdr still names the pane hermes: a program there that showed the
panel's text got a card whose keys went to that program. The card is now
offered only while Hermes is the pane's foreground process, checked again
on every read the answer makes before a key.

The generic card was still offered over an approval in another language
or a pane too narrow for the hint, since the check needed the English hint.
It now goes by the `⚠` Hermes puts on its input line while any of its
modals holds it, in every language and with a draft typed there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@devswha

devswha commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

Review findings and how each was resolved

An adversarial Codex review (gpt-6.1-terra, xhigh, read-only) ran on head 47e33a79. It reported three HIGH findings and no MEDIUM or LOW. Each was reproduced before any change. Fixes are in ca50c383, which is the one review round.

1. HIGH: a program can show the panel's text while it owns the pane. Fixed.
Reproduced live:

  • Hermes 0.21.5 was suspended with Ctrl+Z; herdr kept reporting agent: hermes, with the shell in the foreground.
  • Running cat <recorded panel>; sleep 900 in that shell got a full approval card. Its keys would have gone to sleep.

Fix:

  • The card is offered only while Hermes is the pane's foreground process (hermesOwnsInput via pane.process_info and isHermesProcess), the same pattern as ompOwnsInput and piOwnsInput.
  • The answer path re-reads through the same check before every key, so Hermes suspended between the card and the answer gets 409 and no key.

Verified:

  • Live: same setup, no card; the real pending approval in another pane still got its card.
  • Route tests: hermesStopped in withPane.

Not reproducible:

  • Hermes prints ! command output and tool output into the transcript above its live area, so while Hermes holds the pane, the screen's bottom is always its own chrome.
  • After /quit, herdr no longer names the pane hermes.

2. HIGH: the generic card could still reach an approval the reader leaves out. Fixed.
The "no generic card" check needed the exact English hint, so these still got the raw-key card, whose Enter takes "Allow once":

  • Hermes in another language: 17 locales ship cli.tui.hint_approval, e.g. es: ↑/↓ para seleccionar, Enter para confirmar;
  • a pane narrower than 33 columns, which cuts the hint.

Reproduced with unit variants built from locales/es.yaml strings and a cut hint.

Fix: the check now goes by the ⚠ Hermes puts on its input line while any of its modals holds the input. It's hard-coded (_get_tui_prompt_fragments), so the same in every language, and it also covers a typed draft. A command's own confirmation (/clear and the like) also loses the generic card. That card was unusable anyway: the countdown on its hint changed its id every second.

3. HIGH (as reported): what is drawn is lossy (tabs expanded, wraps shown as line breaks, trailing spaces gone), so two commands can draw the same and share a card id. Not changed. I'd rate it LOW for this PR.
The claim is true, but it describes Hermes's own panel, which the card reproduces line for line:

  • Hermes's textwrap expands tabs before drawing.
  • The wraps are Hermes's wraps, shown as drawn in the card's <pre>, not re-joined. Re-joining would add or lose spaces at the break.
  • Trailing spaces are invisible in the terminal too.

A user reading the terminal can't tell such commands apart either. The card's id changes whenever the drawn command or warning changes, and every key is checked against it.

A lossless source would need Hermes to expose the pending approval itself. That is the kind of agent-specific endpoint #857 rules out; it could be a later Hermes-side request.

Also found in self-review and fixed in ca50c383: a draft typed into Hermes's input while the approval waits. The arrows might move through the draft instead of the menu, so there's no card and no generic card.

Not run here: the omo review. Its CLI isn't on this machine, so the lead is running it.

devswha and others added 2 commits November 5, 2026 16:01
…card

Hermes's classic CLI (0.21.5, its default interface) draws a boxed approval
panel over its input. No reader knew it, so Chat showed the generic
"Waiting for input" card: the screen dumped into its body, raw arrow, Enter
and Esc keys (Enter takes the highlighted Allow once), and an id that changed
with every tick of the panel's countdown.

A reader now turns the panel into an approval card: the command as the panel
wraps it, Hermes's warning, and its own choices, answered by moving to the
row and pressing Enter (never a digit, which picks and confirms at once).
It reads only a complete, English panel that holds the input: a command or
description cut or dropped, a box out of line, other labels, or anything
under it but Hermes's own lines is no card, and registry.waits keeps the
generic card off it too, so the terminal answers it.

Fixes #857

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #2888. Suspended with Ctrl+Z, Hermes leaves its shell in front
while herdr still names the pane hermes: a program there that showed the
panel's text got a card whose keys went to that program. The card is now
offered only while Hermes is the pane's foreground process, checked again
on every read the answer makes before a key.

The generic card was still offered over an approval in another language
or a pane too narrow for the hint, since the check needed the English hint.
It now goes by the `⚠` Hermes puts on its input line while any of its
modals holds it, in every language and with a draft typed there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Deploying ovis with  Cloudflare Pages  Cloudflare Pages

Latest commit: 64f4092
Status: ✅  Deploy successful!
Preview URL: https://59d6ca82.ovis.pages.dev
Branch Preview URL: https://lane-issue-857.ovis.pages.dev

View logs

@devswha

devswha commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

Merge note

Merged origin/main (1dd4f3fe) into the branch (on top of the rebased 2e359ce1) with no conflicts; none of main's new commits touch this PR's files, and the PR's 19 files are byte-identical to the reviewed ca50c383. No new Codex round: nothing in the PR's own change moved.

Validation: bun run check fast passed on 081749f3 (the same merge on the pre-rebase history), whose tree is identical to 7d88b404 (5f5474d6): workflow syntax, generated types and themes, typecheck, build; unit 11674 pass, 0 fail, 46 skip.

CodeRabbit did not review this PR (rate limit, see its "Review limit reached" comment); the Codex review and its fix round above stand in for it. No review threads are open.

@devswha

devswha commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

Final-head review round

Merged origin/main (0c414311) with no conflicts (707926d3; the PR's 19 files are unchanged), then added one comment-only commit: head 64f40926.

Adversarial Codex review (gpt-6.1-terra, xhigh, read-only) at 707926d3: three HIGH findings. All three concern the foreground check that keeps the approval card's keys away from anything but Hermes.

1. The foreground look runs before the read that settles each key. Reproduced; the attempted fix was dropped; the order is documented (64f40926).

  • In verifyThenSend, a Hermes approval answer goes: the answer's read, the look at the pane's foreground, the settling read, then the key. A Hermes suspended with Ctrl+Z during the settling read leaves its panel drawn over the shell, and that one key reaches the shell.
  • A test that suspends Hermes at that read reproduced it: the Enter was sent.
  • I tried a second foreground look after the settling read, as pi's select and confirm do. Codex's re-review showed that it opens a worse window. It puts an await between the check of the highlighted row and the key, so a cursor moved in the terminal during that look would take the Enter on another row (Allow instead of Deny). The key could also leave after the read's deadline, which verifyThenSend otherwise refuses.
  • Both windows are inherent: herdr's send_keys takes no condition on the screen or the foreground (see beforeKey). Of the two, the shell side is harmless. After Ctrl+Z the shell shows a fresh, empty prompt, so a stray ↑, ↓ or Enter runs nothing, and the next key's look refuses. A confirmed wrong row is not harmless. So the look stays before the settling read, and 64f40926 says why in readKnownPrompt.
  • The dropped attempt is kept on the local branch adopt-c/857-postread-check (not pushed).

2. On Windows, hermesOwnsInput returns true. Kept.

  • This is the same as ompOwnsInput and piOwnsInput: Windows lists no foreground process group.
  • The reproduced spoof needs a Hermes stopped behind its shell while herdr still names the pane Hermes. A Windows console has no Ctrl+Z job control to produce that.
  • Before this PR, the same panel on Windows got the generic card, whose Enter takes whichever row is highlighted. That is weaker than this card's verified row.
  • Codex disagrees on re-review: no signal proves Hermes holds a Windows pane. That stays open for the maintainer.

3. isHermesProcess accepts a Python process with a hermes argument anywhere. Kept.

  • python3 /tmp/x.py hermes matches. That process is then the pane's foreground process: the keys go to the program that drew the panel and waits for the answer, not to a different program behind it.
  • The panel must also sit over Hermes's own chrome, or the reader refuses it.
  • The predicate is the one main already uses to find a pane's Hermes (server/hermes.ts). Narrowing it to the Python entrypoint risks missing real launches (python -X utf8 …/bin/hermes, wrappers).
  • Codex disagrees on re-review: such a program is not Hermes. That stays open for the maintainer.

Validation at 64f40926: bun run check fast passed (unit 11922 pass, 0 fail, 46 skip; workflow syntax, generated types and themes, typecheck, build ok). bun test server/prompt.test.ts server/dialogs gave 831 pass, 0 fail on the attempted fix; the shipped code is that of 707926d3 plus a comment.

No review threads were open. CodeRabbit ran no review on this PR (its "Review limit reached" comment).

@devswha
devswha marked this pull request as draft November 5, 2026 12:37
@devswha

devswha commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

Parking as a draft. One HIGH is reproduced: if Hermes is suspended (Ctrl+Z) at just the wrong moment, one answer key can reach a fresh shell prompt. The attempted fix made it worse (a moved cursor could put the Enter on a different row, Allow instead of Deny), so it was dropped. An approval answer must never be able to land on the wrong row or process; this comes back with a design that confirms the Hermes prompt is still in front at the moment of sending (or answers through a Hermes API, if one exists).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: native Hermes approval cards in Chat

1 participant