Skip to content

claude-session-watchdog

Standalone Windows Scheduled Task that detects dead Claude Code sessions during an overnight mission window and pages Andrew through the PavlokPager CLI.

Why this exists: on 2026-07-08 Claude was down ~6 hours (~1:50 AM–7:58 AM CDT) during an overnight mission and nothing paged, because the paging standard lived inside Claude. This watchdog runs entirely outside the Claude ecosystem — plain PowerShell + Task Scheduler + the pavlok CLI — so a total Claude outage is exactly the event it can still report.

How it works

Task Scheduler (daily 00:00, repeat 15 min x 8 h)
        │
        ▼
src/watchdog.ps1 ──► Invoke-WatchdogCheck (src/Watchdog.psm1)
        │
        ├─ 1. Inside 00:00–08:00 local window?          no → exit
        ├─ 2. Mission marker armed + unexpired?         no → clear incident, exit
        ├─ 3. Newest *.jsonl write under ~\.claude\projects\
        ├─ 4. Silence < 120 min?                        yes → healthy, clear incident
        └─ 5. Dead → open/continue incident
               ├─ first detection → pavlok page --level urgent
               ├─ still dead ≥30 min after last page → re-page (max 3/incident)
               └─ pavlok exit 3 (3-per-8h CLI budget) → stand by

Key design decisions:

  • Transcript writes are the liveness signal, not process presence. A hung claude.exe during an API outage looks alive by process list — the 07-08 outage shape. Process count is included in the page reason for diagnosis but never suppresses a page.
  • Armed only when a mission marker exists. No marker (or expired marker) means no pages — quiet nights stay quiet. Corrupt marker fails armed, not disarmed.
  • Acknowledgement = transcript activity resumes or the marker is deleted. Either clears the incident.
  • Paging budget is enforced twice: max 3 pages per incident locally, and the pavlok CLI's own hard 3-per-8h budget (exit code 3) as backstop.
  • This tool never restarts, kills, or reboots anything. Read-only except for its own state/log files and the pavlok call.

Install

# 1. Register the scheduled task (current user, runs while logged on)
pwsh -File install\install-task.ps1

# 2. Verify pavlok CLI is set up (one-time; see PavlokPager repo)
pavlok test

# 3. Daytime smoke test (bypasses the window check; pages only if a marker
#    is armed AND sessions are actually silent 2 h+)
pwsh -File src\watchdog.ps1 -Force

Uninstall: pwsh -File install\uninstall-task.ps1

Arming / disarming

At overnight mission start (Claude session or human):

pwsh -File tools\Start-OvernightMission.ps1 -Mission "WW clear-the-board" -Hours 9

At mission end, or on waking to a page:

pwsh -File tools\Stop-OvernightMission.ps1

Marker file: ~\.claude\overnight-mission.marker.json{ mission, startedUtc, expiresUtc }. Markers self-expire (default 10 h, hard cap 16 h without expiresUtc) so a forgotten marker cannot page tomorrow night.

Configuration

Defaults live in Get-WatchdogDefaultConfig. Override any subset in ~\.claude-watchdog\config.json:

Key Default Meaning
projectsPath ~\.claude\projects Transcript tree to watch
markerPath ~\.claude\overnight-mission.marker.json Arming marker
inactivityThresholdMinutes 120 Silence before "dead"
repageMinutes 30 Gap between re-pages while still dead
maxPagesPerIncident 3 Local page cap per incident
windowStartHour / windowEndHour 0 / 8 Local-time watch window (may cross midnight)
pavlokLevel urgent Pavlok ladder (urgent = vibe 80 → beep 100 → zap 100 ×5, the deep-sleeper wake protocol)
pavlokExe pavlok CLI executable
markerMaxAgeHours 16 Staleness cap for markers without expiresUtc

State & logs

  • ~\.claude-watchdog\state.json — current incident (start, pages sent, exit codes)
  • ~\.claude-watchdog\watchdog.log.jsonl — one JSON line per pass, every pass
  • ~\.pavlok\pages.jsonl — the pavlok CLI's own stimulus log

Exit codes of watchdog.ps1: 0 = ran fine (any non-error outcome), 1 = pavlok CLI error (send failed / CLI missing).

Tests

Invoke-Pester -Path tests   # 37 unit tests, Pester 5+, fully mocked/sandboxed

Pure PowerShell tool → per tools-org testing policy this ships with mocked unit coverage of every decision branch (window, marker, activity, incident state machine, budget, pavlok exit codes) in place of mutation testing.

Ops notes

  • Task runs as the logged-on user (Interactive), StartWhenAvailable catches missed ticks, 10-min execution cap, new instances skipped while one runs.
  • The pavlok page --level urgent ladder blocks ~2 min — well inside the cap.
  • If pavlok is missing the pass logs pavlok-error and exits 1; it never throws the task into a retry storm.