Closes session-inbox-monitor-encoding-guard-followup (finding from -review structural audit item E, CONCERN). inbox-monitor.ps1 emitted ConvertTo-Json (incl. the interpolated inbox path) to a redirected pipe under WinPS 5.1 without setting [Console]::OutputEncoding — the same context that mojibaked stop-dispatcher. ASCII-safe today, but the inbox path is user-data, so a non-ASCII path/content would mangle the inject. - Add [Console]::OutputEncoding + $OutputEncoding = UTF8 after the $ProjectDir gate (mirror of stop-dispatcher.ps1). Comment text kept pure ASCII. - Regression under WinPS 5.1: parse 0 errors; ran hook against a Cyrillic-path project, read raw stdout bytes as no-BOM UTF-8 -> JSON valid, Cyrillic path round-trips intact. - Re-deployed to ~/.claude/hooks/inbox-monitor.ps1, SHA256 byte-identical. - SKILL.md Mojibake failure-mode extended; version 0.2.1 -> 0.2.2 (PATCH). install/hermes/dist not rebuilt — PATCH needs only reload-plugins; the runtime hook is deployed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.9 KiB
7.9 KiB
name, version, description
| name | version | description |
|---|---|---|
| session-inbox-monitor | 0.2.2 | Raises a persistent Monitor (Monitor tool, NOT background Bash) on the project's `.claude-inbox/` at the start of an interactive session, so inter-session messages page the session in real time; the monitor dies on session end on its own. A paired SessionStart hook injects the raise-instruction and first sweeps orphaned monitors of this inbox (a `/clear` leaves them running → re-raise would stack duplicates). Triggers: CLAUDE.md line `inbox monitor: raise on start`, or «подними монитор почты», «настрой авто-монитор инбокса», «raise inbox monitor», «auto-arm inbox watcher». Headless (`claude -p`): does NOT raise — Monitor doesn't work there; rely on the Stop-hook inbox pickup + Notify/ntfy. NOT for how to handle a received message (→ inter-session-peer-discipline) nor the multi-machine inbox backend (→ cross-machine-inbox design). |
session-inbox-monitor
Auto-raises a session-length Monitor on .claude-inbox/ at interactive-session
start (via a paired SessionStart hook that injects the instruction and sweeps
orphans), so inter-session messages page the session in real time. Tears down
for free on session end. Headless sessions skip it and rely on the pull-model
(Stop-hook pickup + Notify).
When to use
- Automatic (the common path). The paired SessionStart hook injects an instruction at the start of every interactive session of an opted-in project. You act on that injection — raise the monitor as your first action — without a user phrase.
- On request. CLAUDE.md line
inbox monitor: raise on start, or «подними монитор почты», «настрой авто-монитор инбокса», «raise inbox monitor», «auto-arm inbox watcher». - NOT for handling the content of a received message (→
inter-session-peer-discipline), nor the multi-machine delivery backend (→cross-machine-inbox). This skill is only the monitor's lifecycle on one machine.
Inputs
<project>/.claude-inbox/— the watched directory. Direct-child*.mdfiles are inbox messages (the Stop-hook moves them to.read/once handled).- The SessionStart hook supplies the exact Monitor command to run, with the
sweep sentinel (
CLAUDE_INBOX_MONITOR) and the absolute inbox path baked in. Use it verbatim — do not hand-author a different poll command, or the sweep won't recognise the process it spawns.
Steps
- Mode check. If this is a headless / non-interactive run (
claude -p), STOP — do not raise a monitor. The Stop-hook inbox pickup plusNotify:/ ntfy cover delivery there; a Monitor can't idle-watch in headless and is killed ~5s after the run. There is no hook-level headless signal, so this is your judgement call from the run context. - Raise exactly one persistent Monitor using the command the hook injected:
the Monitor tool with
persistent: true,description: "inbox watcher". The hook already swept any orphan before injecting, so you start from a clean slate — raise one, not more. - Do not sweep yourself. Killing orphans is the hook's job (it runs before you, at SessionStart, when no other session activity is live).
- On an event (
New inter-session message in inbox: <name>), read.claude-inbox/and handle the message perinter-session-peer-discipline. The Stop-hook also force-delivers any inbox messages at end of turn as a backstop, so nothing is lost if the monitor missed a beat. - Teardown is automatic. The Monitor dies at session end. Do not add a
SessionEnd teardown — and note
/cleardoes not fire SessionEnd anyway (that's why the sweep lives in SessionStart, not SessionEnd).
Deployment (machine-local)
- Hook script:
skills/session-inbox-monitor/hooks/inbox-monitor.ps1(versioned here) → deploy to~/.claude/hooks/inbox-monitor.ps1. - Register in
~/.claude/settings.jsonunderhooks.SessionStart(no matcher → fires on startup/resume/clear/compact), e.g.:{ "hooks": [ { "type": "command", "command": "powershell -NoProfile -ExecutionPolicy Bypass -File \"C:\\Users\\<you>\\.claude\\hooks\\inbox-monitor.ps1\"", "timeout": 15, "statusMessage": "inbox-monitor" } ] } - Twin pattern:
poller-interactive-lock-writer(interactive-lock.ps1). - Opt-in per project: the hook fires only when the project has a
.claude-inbox/directory or a CLAUDE.md lineinbox monitor: raise on start.
Failure modes
- No inbox, no opt-in line → the hook injects nothing; no monitor. Expected for projects that don't use inter-session messaging.
- Two live interactive sessions on the same project → the second session's
SessionStart sweep kills the first session's monitor (the match is
per-inbox-path, not per-session). Known limitation; the deliberate invariant is
"exactly one monitor per inbox per machine." If the first session is still
active, its next Stop-hook turn still delivers inbox mail — only the real-time
paging is lost until it re-raises. See
inter-session-peer-discipline. - Sweep over-match → any live process whose command line contains both the
sentinel
CLAUDE_INBOX_MONITORand the inbox path is killed. At a real SessionStart no agent/tool processes are running yet, so only the orphaned monitor matches. Don't echo or run a command carrying that sentinel+path during a session's startup. - Headless didn't skip → a monitor raised in headless is a harmless no-op, killed ~5s after the run ends. The default errs toward raising because a false-skip in an interactive session would silently lose the feature.
- Monitor auto-stopped → the harness stops monitors that emit too many events. The injected poll command de-dups by filename (pages once per message, not every 15s) to stay under that bar.
- Mojibake on force-delivery → the Stop-hook (
stop-dispatcher.ps1) injects message bodies to stdout; WinPS 5.1 must set[Console]::OutputEncoding = [System.Text.Encoding]::UTF8or non-ASCII (Cyrillic) bodies arrive mangled (it emits in the OEM code page under a harness-spawned redirected pipe). Inbox messages must be written as no-BOM UTF-8, LF — the Write tool does this; PowerShell writers must use[IO.File]::WriteAllText($p,$t,[Text.UTF8Encoding]::new($false)), NOTSet-Content/Out-File -Encoding utf8(which adds a BOM under 5.1). Same WinPS-5.1 encoding class as the hook-source em-dash gotcha. Fixed + in-situ verified 2026-06-17. The SessionStart injector (inbox-monitor.ps1) carries the same[Console]::OutputEncodingUTF-8 guard as a forward-protection (v0.2.2): it interpolates the inbox path into the injected JSON, so a non-ASCII path oradditionalContextwould otherwise mangle the same way — the guard is preventive (today's$ctxis ASCII) but cheaper than an "ASCII-only" invariant.
Side effects
- Spawns one Monitor (and its backing Git-Bash poll process) per interactive session; both die at session end.
- Force-kills orphaned monitor processes of this inbox at every SessionStart.
- No repo writes. The hook and its
~/.claude/settings.jsonregistration are machine-local; only this skill (docs) and.claude-inbox/activity are in play.
What NOT to do
- Don't watch the inbox with a background Bash (
run_in_background) — it leaks across/clearand accumulates zombies. Use the Monitor tool. - Don't add a SessionEnd teardown hook — the Monitor self-terminates, and
/clearnever fires SessionEnd. - Don't raise more than one monitor. The hook guarantees a clean slate before you raise.
- Don't handle message content here — that's
inter-session-peer-discipline. - Don't rely on this in headless — use the pull model (Stop-hook + Notify). Active headless polling, if ever needed, is a separate cron Routine, not this skill.