I was three projects deep. Project A had a review open in one VS Code window. Project B was running under Claude Code with the superpowers
skill set, which means TDD: red, green, refactor, repeat β a mode that produces solid code but stretches a task from minutes into an hour or more. Somewhere in that hour, Claude hit a point where it needed my sign-off to keep going. I didn't notice. I was in Project A. By the time I switched back to Project B, it had been sitting there, blocked, for the better part of twenty minutes β doing nothing, waiting on me.
That's the first problem: long, TDD-driven tasks create real decision points, and if you're not watching, you bleed time waiting for nobody.
The second problem is the same failure wearing a different hat. I run multiple projects in parallel, each in its own VS Code window, each with its own Claude Code session. "Is anything done? Is anything stuck?" isn't a question I can answer without clicking through every window and every session, one at a time. Multiply that by a normal afternoon and it's a meaningful amount of dead time spent just checking.
Both problems have the same shape: Claude Code knows when it's blocked or finished. I don't, because I'm not watching. So I built claude-code-notify
β a tool that pings me on Telegram at exactly the moments that matter: when a turn is truly done, and when it needs me.
The obvious fix is a Stop
hook: Claude Code exposes one, you wire it to a script, the script fires a Telegram message. I did exactly that, first. It worked β until it didn't.
The bug showed up as a notification that arrived too early. I'd get "finished," switch over, and find the terminal still working. The cause, once I found it, was almost embarrassing: Claude Code's Bash
tool, when run with run_in_background=true
, immediately returns an acknowledgment β something like "Command running in background with ID: β¦"
β the instant the command is dispatched, not when it completes. A naive Stop
hook that treats any matching tool result as "resolved" reads that ack as done. It isn't. The command is still running. Background Agent
subagents have the same issue from a different angle: they don't emit an ack at all, so a hook that only tracks explicit "finished" signals just never sees them.
So the naive hook was worse than useless for exactly the cases I cared about most β the long, multi-step, background-heavy turns that made me build this in the first place.
The fix is a single rule: a background task β a subagent or a backgrounded Bash
command β counts as resolved only when a <task-notification>
shows up in the session transcript carrying that task's original tool_use_id
. An immediate acknowledgment never counts. At Stop
time, the hook computes pending = launched β resolved
; if anything is still pending, it stays silent. Only once every background task has actually reported back does it check a rate limit and send.
I'd expected to just use Claude Code's native SubagentStop
hook for this and skip the transcript parsing entirely. It turns out that hook can't do the job: background agents (run_in_background=true
) bypass Stop
/SubagentStop
entirely, subagent completion isn't reliably reported through it, and even when it does fire, it carries the parent session's shared session_id
with no way to tell which subagent finished. All three are open issues against Claude Code itself, not quirks of my setup. Transcript-based matching on tool_use_id
is the only signal that's actually reliable today.
I added the error hook almost as an afterthought β a turn can die for any reason, and a silent dead turn is the same lie as an early "finished." Then one afternoon it turned out to be the most useful of the three.
I had three sessions running: two long autonomous turns and a review. Somewhere in there my account hit its usage limit. A usage limit doesn't stop one turn β it stops all of them, at once, mid-task, in work I couldn't see. On the old workflow I'd have found out the way I always did: clicking into each window and hitting a wall of red where I'd expected progress.
Instead my phone buzzed three times:
Claude Code stopped with error | Refactor the transcript parser | ~/work/foo | 11/07/2026 10:29:28
β and two more like it. Each ping named the project it came from and the turn's own title, because the error notification carries the same identifying fields as every other one: Claude Code's title for the turn, the working directory, and the time.
That's exactly the information a limit erases from your head. When it resets a few hours later, the question isn't "did something break" β everything broke β it's "where was I, in each project, when it did." The three messages sitting in my Telegram history answered that without my opening a single window: which projects, which tasks β enough to walk straight back into each one instead of reconstructing from scratch where I'd left off. The parallel-checking problem from the top of this post, in its worst form β several stalled sessions at once β collapses into a scrollback I can read on my phone while I wait for the limit to lift.
I kick off a long task, switch to another project, and stop thinking about it. One message arrives β on Telegram, so it reaches me regardless of which window has focus β and only for one of three reasons: the turn is genuinely finished, background work and all; it needs my input; or it errored out. No early "finished" while something's still churning, no manually checking five sessions to find the one that's stuck.
Both of my original problems turn out to be the same problem, and this is the same fix: stop relying on me to notice, and make the notification itself trustworthy.
curl -fsSL https://raw.githubusercontent.com/Jeromefromcn/claude-code-notify/main/install.sh | bash
One dependency (python3
), and config lives outside settings.json
so your token never gets committed by accident. Changed your mind? The same line with --uninstall
pulls the hooks and code back out (it leaves your config.env
alone, so reinstalling later doesn't mean re-entering your token):
curl -fsSL https://raw.githubusercontent.com/Jeromefromcn/claude-code-notify/main/install.sh | bash -s -- --uninstall
It's MIT-licensed: github.com/Jeromefromcn/claude-code-notify.
If you run Claude Code across parallel sessions or lean on long autonomous turns, I'd genuinely like to know if this solves the same problem for you β or if you hit a case it doesn't handle.