Skip to content

feat: key per-session state by parent Claude Code PID, not TERM_SESSION_ID (v1.2.0)#9

Merged
JonyanDunh merged 1 commit into
mainfrom
feat/claude-pid-key-v1.2.0
Apr 11, 2026
Merged

feat: key per-session state by parent Claude Code PID, not TERM_SESSION_ID (v1.2.0)#9
JonyanDunh merged 1 commit into
mainfrom
feat/claude-pid-key-v1.2.0

Conversation

@JonyanDunh
Copy link
Copy Markdown
Owner

The bug this fixes

v1.1.0 was unusable in most terminals because it keyed its per-session state file by `TERM_SESSION_ID`, which only iTerm2, WezTerm, and JetBrains IDE terminals export. Every other terminal — Windows Terminal, macOS Terminal.app, GNOME Terminal, Alacritty, tmux, screen, plain Linux ttys — leaves it unset, so `/watchdog:start` would fail at setup with a clear error but no path forward short of `export TERM_SESSION_ID=$(uuidgen)`.

The fix

Walk the process ancestry. On startup both `setup-watchdog.js` and `stop-hook.js` walk upward from their own `process.ppid` until they find a process whose name is `claude`, and use that PID as the state file key. Every Claude Code session has a distinct PID, so concurrent watchdogs in the same project directory never collide — tested up to 100 concurrent sessions by design.

State files are now named `.claude/watchdog.claudepid..local.json`.

Side benefit: no more recursion guard

The v1.1.0 plugin had a `owner_session_id` recursion guard to stop the headless Haiku classifier's own Stop hook from clobbering the main session's state. That's entirely removed in v1.2.0 because the natural isolation falls out of the PID-based keying: the Haiku subprocess is a new Claude Code process with its own distinct PID, so its recursive Stop hook walks ancestry to find THAT PID (not the main session's), and the lookup naturally misses.

New module: `lib/claude-pid.js` (173 lines)

Three OS paths:

Platform Source
Linux `/proc//comm` + `/proc//status` (zero subprocess spawn)
macOS / BSD `ps -o comm=,ppid= -p ` via `execFileSync`
Windows PowerShell `Get-CimInstance Win32_Process` via `execFileSync`

Plus a `WATCHDOG_CLAUDE_PID` env var override so unit tests can inject a synthetic PID without needing a real Claude Code ancestry.

Test suite: 59 → 75

  • New `test/claude-pid.test.js` — `isClaudeProcessName` heuristic, env override, host-platform smoke tests
  • Expanded `test/state.test.js` with `listAll` coverage
  • Rewrote `test/setup.test.js` / `test/stop-watchdog.test.js` / `test/stop-hook.test.js` / `test/stop-hook-haiku.test.js` to key state files by `claudePid` and include concurrent-session isolation scenarios (e.g. 3 concurrent sessions, fire hook for session B, verify A and C untouched)
  • Two setup/stop-watchdog tests are now skipped when the test runner itself happens to be inside a Claude Code session (`CLAUDECODE=1`), since the ancestry walk then correctly finds a real `claude` PID and those tests were asserting failure

Docs cleanup

All 7 READMEs (English + zh/ja/ko/es/vi/pt) propagated via 6 parallel subagent translations. Additionally cleaned up marketing/changelog language throughout:

  • Dropped `(Node.js rewrite)` / `cross-platform` qualifiers from marketplace name, description, plugin description, keywords, and READMEs. Name stays `claude-code-watchdog`.
  • Dropped "no WSL2 / Git Bash required" / "runs directly on native Windows" contrast language and the redundant `WSL2 on Windows` row from the Platform support table.
  • Dropped the entire `## Testing` section from READMEs (that info lives in `CONTRIBUTING.md` where contributors look).
  • Dropped the `(no TERM_SESSION_ID required)` Requirements table row and `(new in 1.2.0)` markers from the Plugin Layout tree.
  • Rewrote the Inspired By "State scoping" row to highlight ralph-loop's hard limit of ONE concurrent loop per project.

Also updated `NOTICE` (v1.2.0 modification entry + `lib/claude-pid.js` file list), `CONTRIBUTING.md` (new test file + concurrent-session scenarios), `SECURITY.md` (attack surface updated), `commands/help.md` (Requirements table + Per-session isolation paragraph), and added `.claude/` to `.gitignore`.

Breaking change

State file naming changed. Previous v1.1.0 files at `.claude/watchdog.<TERM_SESSION_ID>.local.json` will be orphaned after upgrade. Users who had a working v1.1.0 should run `rm -f .claude/watchdog.*.local.json` before using `/watchdog:start` again. Users who never got v1.1.0 working (most of them) see no migration at all.

Test plan

  • `node --test ...` locally — 75 tests, 73 pass, 2 skipped inside Claude Code, 0 failures
  • `node --check` clean on every `.js` file
  • Manifest versions bumped to 1.2.0 consistently (plugin.json + marketplace.json + plugin entry)
  • All 7 READMEs structurally parallel (14 h2 sections each)
  • CI will confirm across 9 matrix jobs (ubuntu/macos/windows × Node 18/20/22)

Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com

…ON_ID (v1.2.0)

The v1.1.0 watchdog was unusable in most terminals because it keyed its
per-session state file by TERM_SESSION_ID, which only iTerm2, WezTerm,
and JetBrains IDE terminals export. Windows Terminal, macOS Terminal.app,
GNOME Terminal, Alacritty, tmux, screen, plain Linux ttys, and most other
terminals leave it unset, so /watchdog:start would fail at setup.

v1.2.0 replaces TERM_SESSION_ID entirely with a process ancestry walk:
on startup (both setup-watchdog.js and stop-hook.js), we walk upward
from our own process.ppid until we find a process whose name is
`claude` and use that PID as the state file key. Every Claude Code
session has a distinct PID, so concurrent watchdogs in the same
project directory never collide — tested up to 100 concurrent
sessions by design. State files are now named
`.claude/watchdog.claudepid.<PID>.local.json`.

This also ELIMINATES the previous owner_session_id recursion guard.
The headless Haiku classifier we spawn is a new Claude Code process
with its own distinct PID; its recursive Stop hook walks ancestry to
find THAT PID (not the main session's), so the state file lookup
naturally misses and the recursive hook exits silently. No explicit
ownership bookkeeping needed.

## Architecture

New module: lib/claude-pid.js (173 lines). Three OS paths:

  - Linux: /proc/<pid>/comm + /proc/<pid>/status (fast, no subprocess)
  - macOS / BSD: ps -o comm=,ppid= -p <pid> via execFileSync
  - Windows: PowerShell Get-CimInstance Win32_Process via execFileSync

Also handles a WATCHDOG_CLAUDE_PID env var override so unit tests can
inject a synthetic PID without needing a real Claude Code ancestry.

## Test suite: 59 → 75

Added test/claude-pid.test.js covering isClaudeProcessName heuristic,
the env override, and host-platform smoke tests for readProcComm /
readProcPpid. Also expanded test/state.test.js with a listAll() test
and rewrote test/setup.test.js, test/stop-watchdog.test.js,
test/stop-hook.test.js, and test/stop-hook-haiku.test.js to key state
files by claudePid instead of termSessionId and include concurrent-
session isolation scenarios. Two setup/stop-watchdog tests are now
skipped when the test runner itself happens to be inside a Claude Code
session (CLAUDECODE=1), since the ancestry walk then correctly finds a
real claude PID and those tests were asserting failure.

## Docs

All 7 READMEs updated in the same PR. State File section, Requirements
table, Plugin Layout tree, and Inspired By comparison table all
reflect the new architecture. Cleaned up marketing / changelog-style
language throughout:

  - Dropped "Node.js rewrite" / "cross-platform" qualifiers from the
    marketplace name, description, plugin description, keywords, and
    READMEs. The name stays claude-code-watchdog.
  - Dropped "no WSL2 / Git Bash required" / "runs directly on native
    Windows" contrast language and the redundant WSL2 row from the
    Platform support table.
  - Dropped the entire ## Testing section from READMEs. Test info
    lives in CONTRIBUTING.md where contributors look.
  - Dropped the "(no TERM_SESSION_ID required)" Requirements table
    row and "(new in 1.2.0)" markers from the Plugin Layout tree.
  - Rewrote the Inspired By "State scoping" row to highlight
    ralph-loop's hard limit of ONE concurrent loop per project.

NOTICE updated with the 1.2.0 modification entry and lib/claude-pid.js
in the file list. CONTRIBUTING.md updated with the new test file and
concurrent-session scenarios. SECURITY.md attack-surface section
updated to drop TERM_SESSION_ID path traversal and owner_session_id
recursion guard references. commands/help.md Requirements table and
Per-session isolation paragraph rewritten. .gitignore adds .claude/
to prevent state file and user settings leaks.

## Breaking change

State file naming changed: previous v1.1.0 files at
`.claude/watchdog.<TERM_SESSION_ID>.local.json` will be orphaned after
upgrade. Users who had a working v1.1.0 (only iTerm2 / WezTerm /
JetBrains) should clean up with
`rm -f .claude/watchdog.*.local.json` before running /watchdog:start
again. Users who never got v1.1.0 working (most of them) see no
migration at all.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@JonyanDunh JonyanDunh merged commit 0a51a00 into main Apr 11, 2026
11 checks passed
@JonyanDunh JonyanDunh deleted the feat/claude-pid-key-v1.2.0 branch April 11, 2026 00:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant