Skip to content

Notch stuck on 'Processing...' after run_in_background:true Bash — PostToolUse races after Stop #98

Description

@LurenKT

Summary

When Claude Code invokes Bash with run_in_background: true (e.g. starting a local dev server) and then immediately ends its turn, the notch stays stuck showing Processing... even though Claude is actually idle and waiting for user input.

The root cause is a race between the Stop hook and the late-arriving PostToolUse hook for the backgrounded Bash. PostToolUse is mapped to status="processing", and the SessionPhase state machine allows .waitingForInput → .processing, so the late PostToolUse flips an already-correct .waitingForInput back to .processing.

Environment

  • macOS 15.x (Darwin 25.4.0)
  • Vibe Notch v1.3 (built from main)
  • Claude Code 2.x (claude --version ≥ 2.1)
  • Hook script: ~/.claude/hooks/claude-island-state.py (auto-installed)

Reproduction

  1. Run any Claude Code session with vibe-notch attached.
  2. Ask Claude to do something that ends with starting a backgrounded shell, e.g. sleep 60 with run_in_background: true, or python -m http.server in the background.
  3. Claude finishes its turn and the notch should turn green ("Ready for input"). Instead it stays cyan ("Processing...") indefinitely until the next PreToolUse arrives — which never happens because Claude is idle.

Diagnostic evidence

Captured live via:

log stream --predicate 'subsystem == "com.claudeisland"' --info --debug --style compact

Real hook arrival order from a reproduction (session 8bbb736d, all events on the same session):

13:28:44.729  [Hooks] Received: PreToolUse  for 8bbb736d
13:28:46.253  [Hooks] Received: Stop        for 8bbb736d   ← turn ended
13:28:47.365  [Hooks] Received: PostToolUse for 8bbb736d   ← arrives 1.1s AFTER Stop

The Stop → PostToolUse ordering is reproducible: run_in_background:true Bash measured a consistent ~2s gap between PreToolUse and PostToolUse in my logs (13:27:58.902 PreToolUse → 13:28:01.019 PostToolUse). When Claude finishes its assistant turn during that gap, Stop fires first.

Root cause (file:line)

Three pieces interact:

  1. ClaudeIsland/Resources/claude-island-state.py:113PostToolUse is mapped to state["status"] = "processing".
  2. ClaudeIsland/Models/SessionEvent.swift:151-162determinePhase() maps "processing" to .processing.
  3. ClaudeIsland/Models/SessionPhase.swift:117 — the state machine allows .waitingForInput → .processing (intended for "user replied, Claude resumes work" path, but it also fires on the late PostToolUse).

So Stop correctly transitions to .waitingForInput, then the delayed PostToolUse legally re-transitions to .processing and the notch never recovers until the next user prompt.

Proposed fix

Narrow the rule in ClaudeIsland/Services/State/SessionStore.swift:150-156: PostToolUse / PostToolUseFailure should only be allowed to flip the session phase when we're recovering from .waitingForApproval (terminal-side approve, which doesn't go through the socket). In all other cases they should only update tool status, not the session phase.

// Before
let newPhase = event.determinePhase()
if session.phase.canTransition(to: newPhase) {
    session.phase = newPhase
}

// After
let newPhase = event.determinePhase()
let allowPhaseChange: Bool = {
    if event.event == "PostToolUse" || event.event == "PostToolUseFailure" {
        // PostToolUse can race after Stop for run_in_background bash.
        // Only flip phase if recovering from terminal-approved permission.
        return session.phase.isWaitingForApproval
    }
    return true
}()
if allowPhaseChange, session.phase.canTransition(to: newPhase) {
    session.phase = newPhase
}

Scenario matrix

Scenario Behavior after fix
Normal turn (Pre→Post→Stop) Pre→.processing, Post→no-op phase, Stop→.waitingForInput ✅
Multiple sequential tools Pre→Post(no-op)→Pre→Post(no-op)→Stop→.waitingForInput ✅
bg bash, PostToolUse arrives after Stop (this bug) Pre→.processing, Stop→.waitingForInput, Post no-op
Terminal-approved permission (no socket reply) PermissionRequest→.waitingForApproval, Post sees .waitingForApproval→flips to .processing ✅ (preserved)
User replies, Claude resumes UserPromptSubmit→.processing via status="processing" path ✅ (independent of PostToolUse)
Permission denied then Stop PermissionRequest→.waitingForApproval, PostToolUseFailure→.processing (recovery), Stop→.waitingForInput ✅

Notes

  • This is purely a vibe-notch state machine fix — no upstream Claude Code change is needed.
  • I have a working patch and have verified the fix locally against the reproduction. Happy to send a PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions