Reading agent status out of Claude Code's hooks

yoanwaidev1 pts0 comments

Reading agent status out of Claude Code's hooks

Writing

Reading agent status out of Claude Code's hooks

I wanted a list of running agents that told me which one is working, which is blocked, and<br>which is done, without me looking at any of them.

The obvious way is to read the screen. Capture the pane, run some regexes, guess. That works<br>for any CLI, which is why I still do it for every tool I support. But it is guessing. A<br>spinner drops for one frame and the agent looks idle. Something prints a line that looks like<br>a spinner and it looks busy.

Claude Code has hooks, so for those sessions the guessing should not be necessary. Mostly it<br>is not. This post is about the places where the event stream does not map cleanly onto a<br>status, and the three gaps I still cover with the pane. Everything below is against Claude<br>Code 2.1.220.

The setup#

Each managed session starts with a generated settings file (--settings) and one<br>env var pointing at a status file:

environmentCOPY

AGENT_MANAGER_STATUS_FILE=/…/hooks/.status

Every hook is a one liner that writes a word into that file:

hook commandCOPY

[ -z "$AGENT_MANAGER_STATUS_FILE" ] || printf working > "$AGENT_MANAGER_STATUS_FILE"

The guard matters. If Claude ever loads that settings file outside a managed session, the<br>variable is unset, the command exits 0, and nothing happens. A hook that can fail will break<br>someone else's agent.

EventWrites

UserPromptSubmitworking<br>PreToolUse, PostToolUseworking<br>Notificationwaiting (see below)<br>Stopfinished<br>SessionStartidle<br>SessionEnddeletes the file

A poller reads the file every two seconds by default. For a normal turn that is enough and you<br>never need the regexes. Then the edges show up.

Notification is not one thing#

It fires for a permission prompt. It also fires for the idle nudge on a quiet input box<br>(idle_prompt), for auth success, for MCP elicitation, and when an agent finishes.<br>Only some of those mean you are stuck.

The two I care about are a permission dialog and an MCP elicitation form:<br>permission_prompt and elicitation_dialog.

I learned this the hard way. I took Notification at face value, and every session<br>I walked away from eventually claimed to be blocked because of the idle nudge. First fix was<br>grepping stdin for English:

Notification hookCOPY

[ -z "$AGENT_MANAGER_STATUS_FILE" ] || grep -q "waiting for your input" \<br>|| printf waiting > "$AGENT_MANAGER_STATUS_FILE"

That works. It is also the wrong layer. Notification has a matcher, so you can<br>subscribe to the blocking types and never see the rest:

claude-settings.jsonCOPY

"Notification": [{ "matcher": "permission_prompt|elicitation_dialog", "hooks": [ … ] }]

Full list from the docs: permission_prompt, idle_prompt,<br>auth_success, elicitation_dialog,<br>elicitation_complete, elicitation_response,<br>agent_needs_input, agent_completed. Grepping English is matching a<br>string someone will reword. The matcher is the same decision by name. I do not match<br>agent_needs_input right now. That is another wait type if you care about the<br>agent view.

A question looks like a finished turn#

If the agent ends with "should I also update the tests?", the event stream treats that as a<br>completed turn. Stop fires. Nothing on the event says it ended on a question. The<br>list shows finished on a session that will sit there forever waiting for a one word answer.

The text is not gone. Stop has last_assistant_message, and that is<br>the field you want if you care about the prose. The transcript path is async and can lag, so<br>reading the transcript at Stop time can miss the message you just got.<br>MessageDisplay also fires while assistant text streams.

My hooks do not parse any of that. They only printf a status word. The pane still<br>decides whether the text was a question. Same guess either way, just a cleaner source if you<br>wire it up later.

Esc leaves you stuck on working#

Interrupt a turn and Stop does not fire. That is in the docs:<br>Stop does not run when the stoppage is a user interrupt. There is no separate<br>interrupt event either. Last write was working, so the file keeps saying<br>working until you type something. None of the hooks I wire fire on Esc.

Wrong status while you are not looking is worse than no status. That is exactly when you trust<br>the list.

Stop is the main loop, not the work#

Stop means the main agent stopped responding. Work it started can still run: a<br>background shell, something queued elsewhere. So the file says finished while the repo under<br>review is still changing.

Subagents got me for a while. They write working via PreToolUse /<br>PostToolUse and never fire the main Stop, so one status file stays<br>on working after they finish. They do have SubagentStart and<br>SubagentStop, with agent_id and agent_type. One file<br>per session still cannot say "three subagents running, one done". That needs a richer model,<br>not a missing event.

SessionStart can fire mid turn#

Matchers: startup, resume, clear,<br>compact, fork. Fork is a new session, not the trap. Compact is. It<br>fires...

status file agent hooks stop claude

Related Articles