SOPs
Voice Nudges
The Problem
Section titled “The Problem”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.
The Vision
Section titled “The Vision”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:
- Which session — “the YouTube auto commenter session.”
- Which screen slot — “the green one” (for fast visual location on the ultrawide grid).
- 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.
The Architecture
Section titled “The Architecture”Six pieces. Two places that write a nudge. Three places that clear it. One daemon that speaks it.
The Files
Section titled “The Files”Nudges live in ~/.claude/nudges/:
| File | Contents |
|---|---|
{session_id} | The spoken text. What James hears when the nudge fires. |
{session_id}.name | Friendly 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}.ts | Unix timestamp of when the wait started. Used to compute escalation. |
The Two Write Paths
Section titled “The Two Write Paths”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:
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.]EOFThis 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).
The Three Clear Paths
Section titled “The Three Clear Paths”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.
The Daemon
Section titled “The Daemon”~/apps/cc/james-nudge.sh launches at session start and loops every 15 seconds. For each file in ~/.claude/nudges/:
- If the owning session’s process has died, clear the file immediately.
- Read the attempt counter. If >= 3, clear and stop nudging this session.
- Check the elapsed wait. Escalate on 5 / 10 / 15 minute thresholds.
- 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.
Respects Voice Mute
Section titled “Respects Voice Mute”If ~/apps/cc/voice-muted exists, the daemon stays silent.
The Shared Matcher
Section titled “The Shared Matcher”~/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.
The Rules (Ordered)
Section titled “The Rules (Ordered)”- Find the last assistant text message in the transcript.
- Take the last two sentences — that’s the “tail” James would hear.
- Check the completion deny-list against the final sentence. If any of these match, return
DONEunconditionally:done,all set,that is it,that's itcopied to clipboard,found it,here you go,here's your,here is yourshipped,deployedcheckpoint saved,committed and pushed,build [number]:
- Check for a question mark in the final sentence. If present, return
WAIT:<tail>. - 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 youryour call?,confirm?
- 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.
When It Misbehaves
Section titled “When It Misbehaves”Nudge fires on completed work
Section titled “Nudge fires on completed work”Check the current state:
ls ~/.claude/nudges/cat ~/.claude/nudges/{session_id} # what would be spokencat ~/.claude/nudge-attempts/{session_id} # how many times already firedIf 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:
tail -n 500 ~/.claude/projects/*/session/{session_id}.jsonl | python3 ~/apps/cc/hooks/nudge_waiting_check.pyIf 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:
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:
nohup bash ~/apps/cc/james-nudge.sh > /dev/null 2>&1 &Stale .name files linger
Section titled “Stale .name files linger”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.
File Map
Section titled “File Map”| Path | Purpose |
|---|---|
~/apps/cc/james-nudge.sh | The daemon. Launched by SessionStart, loops forever. |
~/apps/cc/hooks/auto-nudge.sh | PostToolUse writer/clearer. |
~/apps/cc/hooks/nudge-clear-on-stop.sh | Stop hook clearer. Closes the end-of-turn dead zone. |
~/apps/cc/hooks/nudge-reminder.sh | Injects a system reminder telling Claude to hand-write a nudge if it’s about to wait. |
~/apps/cc/hooks/nudge_waiting_check.py | Shared matcher. Single source of truth. |
~/.claude/nudges/ | Active nudge files. |
~/.claude/nudge-attempts/ | Attempt counters and wait-start timestamps. |
~/.claude/current-session-id | Written by SessionStart so hooks know which session is theirs. |