# SessionStart inbox-monitor injector hook (session-inbox-monitor skill). # # Two jobs, run on every SessionStart (startup / resume / clear / compact): # (a) SWEEP - kill orphaned inbox-monitor OS processes of THIS project. # A `/clear` does NOT fire SessionEnd, so a Monitor's underlying # poll process can outlive the session it belonged to. Without a # sweep, re-raising would stack duplicates. Match is by a sentinel # string (CLAUDE_INBOX_MONITOR) baked into the poll command PLUS # this project's inbox path - so we never touch unrelated processes. # (b) INJECT - additionalContext telling the agent to raise a persistent # Monitor (Monitor TOOL, not background Bash) on /.claude-inbox. # # Opt-in per project: fires only when the project has a `.claude-inbox/` dir OR a # CLAUDE.md line `inbox monitor: raise on start`. # # Headless (`claude -p`): there is NO reliable hook-level signal to detect it # (verified 2026-06-17 - `source` and CLAUDE_* env vars don't distinguish it). # So the hook injects unconditionally and the SKILL instructs the agent to skip # when headless. A Monitor raised in headless is harmless (killed ~5s after the # run ends); a false-skip in an interactive session would silently lose the # feature - so the default errs toward raising. # # Twin pattern: poller-interactive-lock-writer (interactive-lock.ps1). # Machine-local deploy target: ~/.claude/hooks/inbox-monitor.ps1 (registered in # ~/.claude/settings.json SessionStart). Versioned here for multi-machine rollout. param( [string]$ProjectDir = $env:CLAUDE_PROJECT_DIR ) if (-not $ProjectDir) { exit 0 } $inbox = Join-Path $ProjectDir '.claude-inbox' $claudeMd = Join-Path $ProjectDir 'CLAUDE.md' # --- opt-in gate ----------------------------------------------------------- $optedIn = $false if (Test-Path $inbox) { $optedIn = $true } elseif (Test-Path $claudeMd) { if (Select-String -Path $claudeMd -SimpleMatch 'inbox monitor: raise on start' -Quiet -ErrorAction SilentlyContinue) { $optedIn = $true } } if (-not $optedIn) { exit 0 } # Forward-slash inbox path: the Monitor poll command (Git Bash) uses this form, # so both the sweep match and the injected command share one literal. $inboxFwd = ($inbox -replace '\\', '/') # --- (a) sweep orphaned monitors of THIS inbox ----------------------------- # Match = sentinel AND this inbox's path in the same process command line. try { Get-CimInstance Win32_Process -ErrorAction Stop | Where-Object { $_.CommandLine -and $_.CommandLine -match 'CLAUDE_INBOX_MONITOR' -and $_.CommandLine -like "*$inboxFwd*" } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } } catch { } # --- (b) build the canonical Monitor poll command -------------------------- # `: CLAUDE_INBOX_MONITOR` is a bash no-op carrying the sweep sentinel in the # process command line without polluting the event stream. De-dups by filename # so a sitting message pages once, not every 15s (a noisy monitor is auto-stopped). $cmd = @' : CLAUDE_INBOX_MONITOR; d='__INBOX__'; s=' '; while true; do for f in "$d"/*.md; do [ -e "$f" ] || continue; n=$(basename "$f"); case "$s" in *" $n "*) continue;; esac; s="$s$n "; echo "New inter-session message in inbox: $n - read .claude-inbox/ and handle it now"; done; sleep 15; done '@ $cmd = $cmd.Trim().Replace('__INBOX__', $inboxFwd) # --- (c) inject the raise-instruction -------------------------------------- $ctx = @" [session-inbox-monitor] This project participates in inter-session messaging. As your FIRST action, raise a persistent inbox watcher so messages from other sessions page you in real time. Use the Monitor tool with persistent: true, description "inbox watcher", and this EXACT command: $cmd Do NOT use a background Bash for this - it leaks across /clear. The Monitor tool is session-bound and tears down on its own at session end. The paired SessionStart hook already swept any orphaned watcher before this, so raise exactly one. If you are running headless (claude -p / non-interactive), SKIP this - the Stop-hook inbox pickup plus Notify cover delivery there. See the session-inbox-monitor skill for the full contract. "@ @{ hookSpecificOutput = @{ hookEventName = 'SessionStart'; additionalContext = $ctx } } | ConvertTo-Json -Compress -Depth 5 exit 0