Files
claude-skills/skills/session-inbox-monitor/SKILL.md
vitya 29d5e9ffa8 fix(session-inbox-monitor): force UTF-8 on Stop-hook delivery + writer guide v0.2.1
Closes session-inbox-monitor-stophook-utf8-fix. Cyrillic message bodies arrived
as mojibake when force-delivered via the Stop-hook block reason.

Root cause: ~/.claude/hooks/stop-dispatcher.ps1 reads bodies with -Encoding UTF8
(fine) but did NOT set [Console]::OutputEncoding, so ConvertTo-Json to stdout
under a harness-spawned redirected pipe (WinPS 5.1) emitted in OEM cp866.

Fix applied to the machine-local hook (not in git): set
[Console]::OutputEncoding/$OutputEncoding = UTF8 at the top. In-situ RED->GREEN
verified through the real Stop-hook path: a Cyrillic pangram that previously came
back as mojibake now delivers clean; no-loop holds.

Repo changes: SKILL.md Failure modes documents the encoding contract for inbox
writers (no-BOM UTF-8 LF; WriteAllText, not Set-Content -Encoding utf8 which BOMs
under 5.1); bump 0.2.0 -> 0.2.1 PATCH. stop-dispatcher.ps1 multi-machine
propagation is the workshop setup's concern (notified).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:06:00 +03:00

7.5 KiB

name, version, description
name version description
session-inbox-monitor 0.2.1 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 *.md files 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

  1. Mode check. If this is a headless / non-interactive run (claude -p), STOP — do not raise a monitor. The Stop-hook inbox pickup plus Notify:/ 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.
  2. 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.
  3. Do not sweep yourself. Killing orphans is the hook's job (it runs before you, at SessionStart, when no other session activity is live).
  4. On an event (New inter-session message in inbox: <name>), read .claude-inbox/ and handle the message per inter-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.
  5. Teardown is automatic. The Monitor dies at session end. Do not add a SessionEnd teardown — and note /clear does 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.json under hooks.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 line inbox 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_MONITOR and 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]::UTF8 or 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)), NOT Set-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.

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.json registration 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 /clear and accumulates zombies. Use the Monitor tool.
  • Don't add a SessionEnd teardown hook — the Monitor self-terminates, and /clear never 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.