Claude Session Lifecycle Tracking
Issue: #2426
Status: implemented (PR to dev)
Track every local Claude Code session (dev-laptop, and any workstation) as a row in the
claude_sessions Supabase table via the meta-dispatcher CF Worker /sessions endpoint. A
PreToolUse hook registers + heartbeats the session; a Stop hook closes it and appends a JSONL
audit record to ~/.claude/claude-sessions.log (7-day retention). Zombie sessions (no heartbeat
5 min) are marked
orphanedand reaped after 7 days.
Scope: local interactive sessions only. vps-i1 / bms-4 autonomous worker sessions are tracked separately via
agent_sessions/dev_r_agent_sessions— do not point these hooks at those hosts.
Architecture
PreToolUse hook (debounced 60s) Stop hook
-> session-heartbeat.ps1 -> session-stop.ps1
no lock -> POST /sessions DELETE /sessions/:id
stale -> PATCH /sessions/:id append ~/.claude/claude-sessions.log (JSONL)
fresh -> no-op delete lock file
Nightly Task Scheduler (register-cleanup-task.ps1)
-> cleanup-sessions.ps1 -> drop log entries where delete_at < now()
on error: Discord embed + gh issue (radieu/p24-infra, label=bug)
Storage: claude_sessions (Supabase). No D1/KV — the worker has no D1 binding.
| Component | Path (repo source of truth) | Deploy target |
|---|---|---|
| Table migration | supabase/migrations/20260701162903_claude_sessions.sql | Supabase |
| CF Worker endpoint | infra-src/meta-dispatcher/src/sessions.ts + index.ts route | wrangler deploy |
| PreToolUse hook | .claude/hooks/session-heartbeat.ps1 | ~/.claude/hooks/ |
| Stop hook | .claude/hooks/session-stop.ps1 | ~/.claude/hooks/ |
| Nightly cleanup | scripts/cleanup-sessions.ps1 | ~/.claude/scripts/ |
| Task registration | scripts/register-cleanup-task.ps1 | run once |
| Key setup | scripts/setup-session-key.ps1 | run once |
| Zombie migration | scripts/migrate-zombie-sessions.ps1 | run once |
Deployment order (MANDATORY)
The migration must be live before the CF Worker is deployed, or insertSession 404s with
PostgREST “relation does not exist”.
- Apply the migration —
supabase/migrations/20260701162903_claude_sessions.sql(via CI,supabase db push, or the Supabase MCPapply_migration). - Deploy the CF Worker —
cd infra-src/meta-dispatcher && npm run deploy(wrangler deploy). - Populate the key —
scripts/setup-session-key.ps1writesQUEUE_API_URL+QUEUE_API_KEYfromsecrets/monitoring.env.sopsinto~/.claude/.env. - Install the hooks — copy
.claude/hooks/session-heartbeat.ps1andsession-stop.ps1into~/.claude/hooks/, then add the hook wiring to~/.claude/settings.json(see below). - Register the cleanup task — copy
scripts/cleanup-sessions.ps1to~/.claude/scripts/, then runscripts/register-cleanup-task.ps1. - Clear the backlog — run
scripts/migrate-zombie-sessions.ps1once (use-DryRunfirst).
settings.json wiring (MERGE — never overwrite)
~/.claude/settings.json already has live PostToolUse (redact-credentials.py — security
critical) and SessionStart hooks. Add the two keys below into the existing hooks object;
do not replace the file. Losing redact-credentials.py is a security regression.
{
"hooks": {
// ... keep existing PostToolUse + SessionStart entries ...
"PreToolUse": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "pwsh -NoProfile -File \"$HOME/.claude/hooks/session-heartbeat.ps1\"" }
]
}
],
"Stop": [
{
"hooks": [
{ "type": "command", "command": "pwsh -NoProfile -File \"$HOME/.claude/hooks/session-stop.ps1\"" }
]
}
]
}
}The PreToolUse hook is non-blocking (debounced 60s, ~<200ms, fire-and-forget) — a down API or
missing key is swallowed and logged to ~/.claude/session-hook.log; tool calls are never delayed.
Data formats
claude_sessions row
session_id (PK, UUID from the lock file) · machine_id · branch · started_at ·
last_heartbeat · ended_at · status (active | orphaned | completed) · delete_at
(= started_at + 7d).
~/.claude/claude-sessions.log (JSONL, one object per line)
{"session_id":"uuid","machine_id":"dev-laptop","branch":"fix/...","started_at":"...","ended_at":"...","status":"completed","delete_at":"2026-07-08T..."}status = completed (clean Stop hook) or orphaned (reaped by zombie migration).
Session identity (lock files)
Claude Code does not expose a stable per-session UUID env var in PreToolUse hooks. If the hook
stdin JSON carries session_id, the lock is keyed on it; otherwise the key is derived from the
working directory and a fresh UUID is generated and persisted in the lock file:
~/.claude/sessions/active/<key>.lock # {session_id, machine_id, branch, started_at, last_heartbeat}
Lock-file presence = session active. Two VSCode windows on the same worktree produce two locks (two real sessions) — benign; the zombie migration handles both.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Session never appears in claude_sessions | QUEUE_API_KEY not in ~/.claude/.env | run setup-session-key.ps1; check ~/.claude/session-hook.log |
Session stuck active after closing VSCode | Stop hook didn’t fire (crash/kill) | zombie migration marks it orphaned after 5 min; run migrate-zombie-sessions.ps1 |
insertSession 404 relation does not exist | migration not applied before deploy | apply migration (step 1), redeploy worker |
| Hooks fail silently | QUEUE_API_KEY rotated | re-run setup-session-key.ps1 |
| Cleanup task never runs | Task Scheduler entry missing | re-run register-cleanup-task.ps1; verify Get-ScheduledTask -TaskName P24-ClaudeSessionCleanup |
Manually close a stuck session
# via the endpoint (preferred)
Invoke-RestMethod -Method Delete -Uri "$env:QUEUE_API_URL/sessions/<session_id>" `
-Headers @{ Authorization = "Bearer $env:QUEUE_API_KEY" }
# or directly in Supabase (service role): PATCH status='orphaned' / DELETE the rowRollback
Session hooks are non-blocking — even a fully broken /sessions endpoint cannot affect existing
queue operations (/queue-issue, /trigger) or delay tool calls. To roll back the endpoint:
- Remove the
/sessionsroute block +handleSessionRequestimport fromindex.ts. - Delete
sessions.ts. - Revert the
insertSession/updateSession/deleteSessionadditions insupabase.tsand theSessionRowtype intypes.ts. - Redeploy the CF Worker (
wrangler deploy). - The
claude_sessionstable may remain (harmless). Local hooks fail silently until re-installed.
To disable client-side only: remove the PreToolUse + Stop entries from ~/.claude/settings.json.
Error notification
cleanup-sessions.ps1 follows the CLAUDE.md Error Notification Standard — on any failure it sends
a Discord embed via P24_DISCORD_INFRA_SCRIPTS_ERRORS_WEBHOOK_URL and creates a GitHub issue in
radieu/p24-infra (label bug).