Skip to content

SOPs

Voice Nudges

James runs three or more Claude Code sessions at once, all on one ultrawide. He looks away. A session finishes work and asks “Want me to commit this?” then sits. Without help, that question sits in the chat panel forever and the whole setup loses its leverage — parallelism becomes a stack of stuck sessions instead of multiplied throughput.

The voice nudge system fixes that. When a session is genuinely waiting on James, it speaks up after a few minutes of silence, names itself, says what it needs, and shakes its own window. James turns his head, sees which slot is flashing, gives the answer, and the nudge goes silent.

Nudges should only fire when Claude is blocked on James. Not when the work finished cleanly. Not when a question was rhetorical. Not when Claude just said “let me know” as a conversational closer. One-and-done responses must never produce a nudge.

When a nudge does fire, it should tell James three things:

  1. Which session — “the YouTube auto commenter session.”
  2. Which screen slot — “the green one” (for fast visual location on the ultrawide grid).
  3. What it needs — the exact question, read verbatim.

Everything else — the escalating schedule, the auto-clear on reply, the visual flash — exists to serve that one rule: speak up only when James is the one blocking progress, and make it easy to unblock.

Six pieces. Two places that write a nudge. Three places that clear it. One daemon that speaks it.

Nudges live in ~/.claude/nudges/:

FileContents
{session_id}The spoken text. What James hears when the nudge fires.
{session_id}.nameFriendly session name (first 4 words of the VS Code ai-title).
~/.claude/nudge-attempts/{session_id}Attempt counter (0, 1, 2, 3). Caps at 3.
~/.claude/nudge-attempts/{session_id}.tsUnix timestamp of when the wait started. Used to compute escalation.

1. Automatic (regex). ~/apps/cc/hooks/auto-nudge.sh fires on every PostToolUse. It reads the last 500 lines of the session transcript, finds the last assistant message, and runs it through the shared matcher at ~/apps/cc/hooks/nudge_waiting_check.py. If the matcher returns WAIT:<tail>, the script writes <tail> to the session’s nudge file. If the matcher returns DONE, it removes the file.

2. Manual (LLM). Claude can write the nudge file itself when it knows it’s about to wait. The Super CLAUDE.md carries the rule:

Terminal window
SID=$(cat ~/.claude/current-session-id 2>/dev/null)
cat > ~/.claude/nudges/$SID << 'EOF'
Hey James, over here on the [session name]. [What you need from him.]
EOF

This runs before the final response, so by the time James has the ball, the nudge is already staged. Manual nudges give Claude a tighter, more specific message than the regex-extracted one — but they can go stale if the final response turns out to be a “done” rather than a “waiting” (which is where the Stop-hook clear path comes in).

1. UserPromptSubmit. When James types a reply, a UserPromptSubmit hook deletes the nudge file, the attempt counter, and the timestamp in one shot. This is the happy path — James responded, the wait is over, stop talking.

2. PostToolUse (auto-nudge.sh). On every tool call, the regex matcher re-evaluates. If the most recent assistant message looks like a completion (“Done.”, “Copied to clipboard.”, “Shipped.”), the nudge file is removed even if a prior turn wrote one.

3. Stop hook (nudge-clear-on-stop.sh). PostToolUse only fires when a tool call happens. But many turns end with a text-only message — a final answer, a “done” confirmation. Those turns never trigger the matcher, so a stale nudge would survive until the daemon spoke it five minutes later. The Stop hook closes that dead zone. It runs the exact same shared matcher at end-of-turn and clears the file if nothing is genuinely waiting.

~/apps/cc/james-nudge.sh launches at session start and loops every 15 seconds. For each file in ~/.claude/nudges/:

  1. If the owning session’s process has died, clear the file immediately.
  2. Read the attempt counter. If >= 3, clear and stop nudging this session.
  3. Check the elapsed wait. Escalate on 5 / 10 / 15 minute thresholds.
  4. When a threshold fires: look up the session’s VS Code window position, compute a grid color (red / green / blue / orange / purple / yellow based on the 2x3 ultrawide slot), and speak the line:

“5-minute reminder. Hey James, back over here on the [session] session, the [color] one. [nudge content]”

Then a desktop notification appears.

The escalating schedule is intentional — one reminder is easy to miss, three reminders spread over 30 minutes is hard to miss without being obnoxious. After the third attempt, the nudge is retired even if James never responded. That’s the guardrail against a runaway “4-hour reminder” on a forgotten session.

If ~/apps/cc/voice-muted exists, the daemon stays silent.

~/apps/cc/hooks/nudge_waiting_check.py is the single source of truth for “is this session waiting?” Both auto-nudge.sh (PostToolUse) and nudge-clear-on-stop.sh (Stop) pipe the transcript through it and act on the verdict. Having one shared file prevents drift between the two hooks.

  1. Find the last assistant text message in the transcript.
  2. Take the last two sentences — that’s the “tail” James would hear.
  3. Check the completion deny-list against the final sentence. If any of these match, return DONE unconditionally:
    • done, all set, that is it, that's it
    • copied to clipboard, found it, here you go, here's your, here is your
    • shipped, deployed
    • checkpoint saved, committed and pushed, build [number]:
  4. Check for a question mark in the final sentence. If present, return WAIT:<tail>.
  5. Check the waiting-signal allow-list against the final sentence:
    • want me to, should I, approve | approval, go ahead?
    • ready when you are, waiting for, need your
    • your call?, confirm?
  6. Otherwise return DONE.

The earlier version of this matcher scanned both sentences for \?, let me know, and sound good. Those patterns match conversational closers that Claude uses even when the work is complete (“Let me know if you need anything else.”), so the matcher over-fired. The fix was twofold: restrict the question-mark rule to the final sentence only, and replace loose phrases with a completion deny-list that takes priority.

Check the current state:

Terminal window
ls ~/.claude/nudges/
cat ~/.claude/nudges/{session_id} # what would be spoken
cat ~/.claude/nudge-attempts/{session_id} # how many times already fired

If a nudge file exists but the session is done, either (a) the LLM wrote one and the Stop hook didn’t clear it (unlikely now — verify the Stop hook is wired in ~/.claude/settings.json), or (b) the regex caught a false positive. Run the shared matcher manually:

Terminal window
tail -n 500 ~/.claude/projects/*/session/{session_id}.jsonl | python3 ~/apps/cc/hooks/nudge_waiting_check.py

If it prints WAIT:... for a message that’s clearly a completion, add the missing phrase to the deny-list in nudge_waiting_check.py.

Nudge never fires even though Claude is waiting

Section titled “Nudge never fires even though Claude is waiting”

The daemon needs two things: the nudge file must exist and 5 minutes must elapse without a tool call. Check:

Terminal window
pgrep -laf james-nudge.sh # is the daemon running?
cat ~/.claude/nudges/{session_id} # is there content to speak?
stat -f %m ~/.claude/nudge-attempts/{session_id}.ts # when did the wait start?

If the daemon isn’t running, the SessionStart hook failed to launch it. Restart manually:

Terminal window
nohup bash ~/apps/cc/james-nudge.sh > /dev/null 2>&1 &

Orphan .name files appear in ~/.claude/nudges/ when a session ends without cleanup. The daemon cleans them up on its next iteration — the sweep at the bottom of james-nudge.sh removes any .name whose sibling nudge file no longer exists. Safe to leave alone; they’ll clear within 15 seconds of the owning session exiting.

PathPurpose
~/apps/cc/james-nudge.shThe daemon. Launched by SessionStart, loops forever.
~/apps/cc/hooks/auto-nudge.shPostToolUse writer/clearer.
~/apps/cc/hooks/nudge-clear-on-stop.shStop hook clearer. Closes the end-of-turn dead zone.
~/apps/cc/hooks/nudge-reminder.shInjects a system reminder telling Claude to hand-write a nudge if it’s about to wait.
~/apps/cc/hooks/nudge_waiting_check.pyShared matcher. Single source of truth.
~/.claude/nudges/Active nudge files.
~/.claude/nudge-attempts/Attempt counters and wait-start timestamps.
~/.claude/current-session-idWritten by SessionStart so hooks know which session is theirs.