feat(session-inbox-monitor): SessionStart hook + fill SKILL body v0.2.0
Core content task of the session-inbox-monitor line. Two deliverables:
1. SessionStart hook `skills/session-inbox-monitor/hooks/inbox-monitor.ps1`
(versioned for multi-machine rollout; deployed to ~/.claude/hooks/ and
registered in ~/.claude/settings.json SessionStart):
- sweep: Get-CimInstance | Stop-Process orphaned monitors of THIS inbox,
matched by sentinel CLAUDE_INBOX_MONITOR + inbox path (a /clear leaves
the poll process alive -> re-raise without sweep stacks duplicates);
- inject: hookSpecificOutput.additionalContext with the exact persistent
Monitor command (Monitor tool, not background Bash);
- opt-in gate: fires only on .claude-inbox/ dir or CLAUDE.md trigger line.
ASCII-only (em-dash -> mojibake under WinPS 5.1 without BOM, fixed).
2. SKILL.md body filled (When to use / Inputs / Steps / Deployment /
Failure modes / Side effects / What NOT to do); bump 0.1.0 -> 0.2.0 MINOR.
Headless: no hook-level signal exists (verified via claude-code-guide) ->
agent-side best-effort skip, default errs toward raising (false-skip in
interactive loses the feature; false-raise in headless is a harmless no-op).
Live-verified: inject -> valid JSON; sweep -> killed a planted orphan (PASS);
real Monitor tool spawns a bash process carrying the sentinel (sweep will
find real orphans); settings.json stays valid. Sweep over-match edge and
multi-session-per-project limit documented honestly in Failure modes.
Closes [session-inbox-monitor-sessionstart-hook].
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: session-inbox-monitor
|
||||
version: 0.1.0
|
||||
version: 0.2.0
|
||||
description: >
|
||||
Raises a persistent Monitor (Monitor tool, NOT background Bash) on the
|
||||
project's `.claude-inbox/` at the start of an interactive session, so
|
||||
@@ -26,24 +26,102 @@ for free on session end. Headless sessions skip it and rely on the pull-model
|
||||
|
||||
## 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.:
|
||||
```json
|
||||
{ "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.
|
||||
|
||||
## 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.
|
||||
|
||||
86
skills/session-inbox-monitor/hooks/inbox-monitor.ps1
Normal file
86
skills/session-inbox-monitor/hooks/inbox-monitor.ps1
Normal file
@@ -0,0 +1,86 @@
|
||||
# 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 <project>/.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
|
||||
Reference in New Issue
Block a user