Skip to content

feat(core): agent states from Claude Code hooks, a Failed mark (#131) - #132

Merged
Maxaubert merged 4 commits into
mainfrom
feat/131-agent-hooks
Oct 4, 2026
Merged

Maxaubert merged 4 commits into
mainfrom
feat/131-agent-hooks

Conversation

@Maxaubert

@Maxaubert Maxaubert commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Closes #131.

What and why

Today the tab indicator guesses an agent's state from the outside: the title, a process poll, output activity, and Claude's English footer text for questions. That guessing has two weak spots:

  • "Waiting on you" is fragile. If a Claude update rewords the footer, the Question line and the taskbar count go quiet, and nothing reports an error.
  • A failed turn is invisible. A rate limit or an overload looks the same as a finished turn.

Claude Code hooks report these states exactly. This PR adds a small Claude Code plugin to the core, core/claude-plugin. Each hook runs hook.cmd, which just echoes a fixed line (no node, 20-100 ms measured). Claude writes that line's terminalSequence into its own terminal: ESC]777;prism-agent;state=...[;kind=...]BEL. So:

  • no listener, network or tab ids are needed;
  • claude -p and SDK runs never write the sequence (measured), so nested runs can't light up a tab.

Main gives a new shell the plugin through CLAUDE_CODE_PLUGIN_DIRS:

  • any value the user already set is kept;
  • a copy of our plugin inherited from another copy of the app is dropped;
  • it happens only when the host passes claudePluginDir and Settings > Appearance > Exact status from Claude Code is on (the default).

The core reads the OSC 777, parses it purely, and passes it on through termBus to useAgentIndicator.

Claude Code event state= Tab
UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, SubagentStart, PreCompact working Working
PermissionRequest, Elicitation; Notification permission_prompt / elicitation_dialog / elicitation_url_dialog question Question line
Stop done Finished line
StopFailure, one hook per error value plus a catch-all failed;kind=<error> Failed line (new), tooltip "Failed: rate limit"

Failed is a new 3 px line:

  • Switch: agent-failed-on, on by default.
  • Colour: the theme's red, kept apart from the accent and the green by hue (marksApart).
  • Rank: it outranks Finished and is outranked by Question.
  • Badge: the taskbar badge counts it.
  • Switch off: the tab shows Finished as before (a failure raises Finished underneath).

Codex's [ ! ] Action Required title now counts as its Question.

What stays of the old method

  • Esc: no hook fires on an Esc, so the title still ends Working. An idle title after working with no Stop means idle and no Finished line. A Stop that lands after the idle title (the two race; seen in the real run) still marks Finished.
  • Presence: the process poll still decides whether an agent is there at all.
  • Hooked sessions: once a session has sent a hook signal, its output is never scored and its screen is never read for a question.
  • Sessions with no signal keep the whole old method: Codex, a claude started before the update, a folder not yet trusted, plugins blocked by policy, or the setting off.

Prism impact (core change: 0.24.0, a bump PR follows)

  • What Prism gets with the bump:
    • the reader;
    • the Failed state;
    • the two new switches. Prism's settings page builds on the core's AttentionSettings, so its termOptions check sees them automatically.
  • Prism's own work: its tab strip still has to draw failedIds, and its themedAgentColors may name failed (otherwise FAILED_RED).
  • The plugin: Prism gets it only once it ships core/claude-plugin and passes claudePluginDir. Until then no shell's environment changes in Prism. A unit test checks that ptyEnv with no plugin option is byte for byte what it was.
  • API changes:
    • The preload and TermApi gain an optional hooks argument on termSpawn and termPrewarm.
    • Prism's prewarm passes none and its main has no dir, so warm-shell adoption is unchanged there.
  • Lint wall: the core lint wall passes.

Gate

  • npm run typecheck: 0 errors.
  • npm run lint: 0 errors, 12 warnings (the existing react-refresh ones).
  • npm test: 88 files, 1323 tests passed.
  • npm run e2e (full): 46 scenarios, 684 checks, 0 fails.
  • New agentHooks e2e scenario (22 checks): a real pwsh prints the OSC 777 sequences. It covers:
    • the plugin in the environment, and gone with the setting off;
    • Working, Question, Done, and Failed with its tooltip;
    • the badge count;
    • an Esc;
    • the Failed switch;
    • another OSC 777 payload being ignored;
    • a hooked session's screen not being read for questions.
  • The scenario fails on main's source: with core/ and src/ restored from origin/main it showed 11 FAILs and a throw.
  • Packaged: npm run package ships resources/claude-plugin (.claude-plugin/plugin.json, hook.cmd, hooks/hooks.json), and PT_E2E_PACKAGED=1 agentHooks passes 22/22.

Real Claude Code check (2.1.289, the built app, parked like the e2e)

  • Invalid model (ANTHROPIC_MODEL=claude-bogus-model-0, no tokens): the tab went working, then failed. Tooltip: ...agent-hooks\nFailed: model not found. The screen showed Claude's "There's an issue with the selected model".
  • Permission prompt (--model haiku --permission-mode default, a mkdir in a scratch folder, one tiny-model turn): the tab went working, then question while "Do you want to proceed?" was on screen. After approval it went working, then done, and the folder was created.
  • Both runs showed about 100 ms with no state between Working and the final mark. That is the idle title arriving before the hook, the race the design allows for.

Not done here

The hands-on install on this machine still waits for the owner's go. The installer is unsigned, as are all releases until SignPath enrolment.

Update after review (94aabe1):

  • No shell: every hook runs with no shell, as cmd.exe /d /c call ${CLAUDE_PLUGIN_ROOT}/hook.cmd <state> [kind] in exec form. The earlier command string failed under PowerShell, which is what Claude Code uses on a Windows with no Git Bash: every event raised a hook error. Checked in real Claude, including a folder with spaces and brackets.
  • Warm shells: a warm shell started under the other setting value is replaced.
  • Line endings for Prism: core/.gitattributes keeps hook.cmd CRLF in core-dist.

Hands-on: the packaged 0.30.0 is installed on the owner's machine with resources/claude-plugin present.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t

Maxaubert and others added 4 commits October 5, 2026 00:38
A bundled Claude Code plugin prints a fixed OSC 777 through terminalSequence
for each hook event; the core reads it as the agent's own word: working,
question, done, failed with its kind. Failed is a new attention line, the
Exact status setting hands new shells the plugin. Codex's Action Required
title is a question.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
…rms (#131)

Review of #131, each MEASURED on Claude Code 2.1.289:
- Without Git Bash, Claude Code runs a hook's command string through
  PowerShell, which reads `"<root>/hook.cmd" working` as a ParserError: no
  states, and a visible hook error on every event. The hooks now use exec
  form (`cmd.exe` + args, no shell, available since 2.1.139); `call` keeps
  cmd from stripping the quotes off a plugin folder with brackets in it.
  The unit test runs every hook as written from such a folder.
- A warm shell started with the other plugin answer was never adopted and
  never replaced, so every new tab in that folder started cold after the
  setting was switched. It now makes way.
- core/.gitattributes carries the CRLF rule for hook.cmd into core-dist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
@Maxaubert
Maxaubert merged commit 6266aa4 into main Oct 4, 2026
2 checks passed
@Maxaubert
Maxaubert deleted the feat/131-agent-hooks branch October 4, 2026 23:52
github-actions Bot pushed a commit that referenced this pull request Oct 4, 2026
…#132)

* docs(spec): agent states from Claude Code hooks, design and plan (#131)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t

* feat(core): agent states from Claude Code hooks, a Failed mark (#131)

A bundled Claude Code plugin prints a fixed OSC 777 through terminalSequence
for each hook event; the core reads it as the agent's own word: working,
question, done, failed with its kind. Failed is a new attention line, the
Exact status setting hands new shells the plugin. Codex's Action Required
title is a question.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t

* docs(core): hooks rules, privacy line, core 0.24.0 and app 0.30.0 (#131)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t

* fix(core): plugin hooks run without a shell, a switched setting re-warms (#131)

Review of #131, each MEASURED on Claude Code 2.1.289:
- Without Git Bash, Claude Code runs a hook's command string through
  PowerShell, which reads `"<root>/hook.cmd" working` as a ParserError: no
  states, and a visible hook error on every event. The hooks now use exec
  form (`cmd.exe` + args, no shell, available since 2.1.139); `call` keeps
  cmd from stripping the quotes off a plugin folder with brackets in it.
  The unit test runs every hook as written from such a folder.
- A warm shell started with the other plugin answer was never adopted and
  never replaced, so every new tab in that folder started cold after the
  setting was switched. It now makes way.
- core/.gitattributes carries the CRLF rule for hook.cmd into core-dist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

Agent indicator from Claude Code hooks (exact state), title and process poll as fallback

1 participant