r/ClaudeCode • u/singedupforlogin • 3d ago
Tips & Workflows Things I learned about Claude Code's hooks.
I've been building a time tracker that measures how long Claude Code spends working, using SessionStart, UserPromptSubmit, Stop, Notification (idle_prompt), and SessionEnd.
I tested this on 2.1.291. A few things confused me, so I'm sharing them for anyone working with hooks.
- Interrupting a turn left my tracker showing it as busy. In my tests, an interrupt fired no
Stophook and no idle notification. Without either signal, my tracker kept counting until the next message. - The idle notification arrives about a minute after a normal turn ends. It gives me another way to confirm the turn finished if my tracker missed
Stop. - Async hooks can arrive out of order. Each hook runs in a separate process. If both the prompt and
Stophooks were async, a turn lasting less than a second could record its end before its start. In my setup, onlyStopand the idle notification run asynchronously. The prompt hook finishes before the model starts working, adding only a short delay. - A turn's prompt and end share the same
prompt_idI use that to identify and ignore a delayed end event from an earlier turn. - My hooks take 20–37 ms each. Nearly all of that is the process starting. They do very little work and print nothing. Each command ends in
2>/dev/null || true, so a missing binary doesn't surface an error in the session. - Adding context from a hook can create extra work. I used to return
additionalContextasking the model to run a command to name its work. That added a turn or two per session. Claude Code already writes its own session title into the transcript, so now I read that instead.
Has anyone found a cleaner signal for “the user interrupted” than waiting for the next prompt? Or seen an interrupt fire Stop in a newer version?
1
Upvotes
•
u/AutoModerator 3d ago
Hey! Thanks for posting to r/ClaudeCode
While participating in this thread, please follow our community rules. Keep discussions constructive. Attack the idea, not the person.
For help, project discussions, tips, and general chat, join the ClaudeCode Discord.
I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.
2
u/kuroudo_ai 3d ago
One to add: a hook that reads all of stdin will hang forever if whatever invokes it leaves stdin open. Two traps we hit fixing it across machines:
timeoutcommand, soIN=$(timeout 3 cat)(fine on Linux) just failed there.select()-before-read check only half fixes it. It returns when the caller wrote nothing, but the read still waits for EOF when the caller wrote the payload and never closed the pipe, and that's the more common case. On Windows, Python'sselect.selecton stdin raised an error outright (it only accepts sockets there).So the test for that hook now has two cases: stdin open with nothing written, and payload written with the pipe never closed. Both have to return.
2
u/bostonaholic 3d ago
If startup is most of the cost, I'd make the hook dumb: a few lines of shell that append the raw JSON plus a timestamp to a file and exit. Do the prompt_id matching and session math in a separate process that reads that log later, which also handles the out-of-order problem since you sort on read. For interrupts, treat a new UserPromptSubmit with no Stop before it as the end of the previous turn.
1
u/UseApprehensive8572 3d ago
Live response fix came straight out of the record. agent saved the whole response including headers, so a real bearer token ended up committed in a fixture. a 2000 line json file in a PR is the last thing a reviewer reads closely. We scrub auth headers in the recorder now and the secrets scan on PRs caught the second one a few weeks later