You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: key per-session state by parent Claude Code PID, not TERM_SESSION_ID (v1.2.0) (#9)
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>
"description": "Watchdog (Node.js rewrite) — cross-platform self-referential Claude Code loop with zero bash/jq dependencies. Runs natively on Linux, macOS, and Windows. Apache 2.0, derived from ralph-loop.",
3
+
"name": "claude-code-watchdog",
4
+
"description": "Watchdog — a self-referential Claude Code loop that re-feeds the same prompt until files actually stop changing. Apache 2.0, derived from ralph-loop.",
5
5
"owner": {
6
6
"name": "Jonyan Dunh",
7
7
"email": "jonyandunh@outlook.com"
8
8
},
9
9
"plugins": [
10
10
{
11
11
"name": "watchdog",
12
-
"description": "Self-referential loop for Claude Code that re-feeds the user's prompt until the task truly stops producing file edits. Cross-platform Node.js implementation with no bash / jq dependencies. Uses a headless Haiku classifier to judge convergence, requires the agent to actually call tools before exit (no pure-text 'done' claims), and is hidden from the agent so it cannot cheat. Per-session state keyed by TERM_SESSION_ID. Apache 2.0, derived from ralph-loop.",
13
-
"version": "1.1.0",
12
+
"description": "Self-referential loop for Claude Code that re-feeds the user's prompt until the task truly stops producing file edits. Uses a headless Haiku classifier to judge convergence, requires the agent to actually call tools before exit (no pure-text 'done' claims), and is hidden from the agent so it cannot cheat. Apache 2.0, derived from ralph-loop.",
Copy file name to clipboardExpand all lines: .claude-plugin/plugin.json
+3-5Lines changed: 3 additions & 5 deletions
Original file line number
Diff line number
Diff line change
@@ -1,7 +1,7 @@
1
1
{
2
2
"name": "watchdog",
3
-
"version": "1.1.0",
4
-
"description": "Self-referential loop for Claude Code. Re-feeds the same prompt after every turn until files actually stop changing. Cross-platform Node.js implementation — no bash / jq dependencies, runs natively on Linux, macOS, and Windows.",
3
+
"version": "1.2.0",
4
+
"description": "Self-referential loop for Claude Code. Re-feeds the same prompt after every turn until files actually stop changing.",
Copy file name to clipboardExpand all lines: CONTRIBUTING.md
+7-6Lines changed: 7 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -81,7 +81,7 @@ Common types:
81
81
82
82
## Testing your change
83
83
84
-
Watchdog ships with 59 automated tests using Node's built-in `node:test` runner — no external test dependencies. Run them from the repo root:
84
+
Watchdog ships with 75 automated tests (73 active + 2 skipped-when-inside-Claude-Code) using Node's built-in `node:test` runner — no external test dependencies. Run them from the repo root:
-**`test/setup.test.js`** — E2E subprocess tests for `scripts/setup-watchdog.js`
106
-
-**`test/stop-watchdog.test.js`** — E2E subprocess tests for `scripts/stop-watchdog.js`
107
-
-**`test/stop-hook.test.js`** — E2E subprocess tests for `hooks/stop-hook.js` covering the recursion guard, ownership claim, max iterations cap, missing transcript, and pure-text turn branches (Haiku subprocess not invoked)
108
-
-**`test/stop-hook-haiku.test.js`** — E2E integration tests that exercise the **real**`spawnSync('claude', ...)` subprocess path by placing a cross-platform mock Claude CLI (POSIX shell script + Windows `.cmd` wrapper) on the hook's `PATH`. Tests all verdict branches: FILE_CHANGES / NO_FILE_CHANGES / ambiguous (both markers) / ambiguous (neither marker) / CLI failure
105
+
-**`test/claude-pid.test.js`** — process ancestry walk (lib/claude-pid.js): isClaudeProcessName heuristic, WATCHDOG_CLAUDE_PID env override, readProcComm / readProcPpid
106
+
-**`test/setup.test.js`** — E2E subprocess tests for `scripts/setup-watchdog.js` including concurrent-session independence
107
+
-**`test/stop-watchdog.test.js`** — E2E subprocess tests for `scripts/stop-watchdog.js`, including the "only cancels THIS session, leaves concurrent sessions alone" assertion
108
+
-**`test/stop-hook.test.js`** — E2E subprocess tests for `hooks/stop-hook.js` covering the natural recursion isolation (different claudePid = different state file), max iterations cap, missing transcript, pure-text turn, and 3-concurrent-sessions scenario (Haiku subprocess not invoked — see next file)
109
+
-**`test/stop-hook-haiku.test.js`** — E2E integration tests that exercise the **real**`spawnSync('claude', ...)` subprocess path by placing a mock Claude CLI (POSIX shell script + Windows `.cmd` wrapper) on the hook's `PATH`. Tests all verdict branches: FILE_CHANGES / NO_FILE_CHANGES / ambiguous (both markers) / ambiguous (neither marker) / CLI failure
109
110
110
111
In addition to the unit/integration suite, you should **also** manually verify your change in a live Claude Code session:
[](https://github.com/anthropics/claude-plugins-official/tree/main/plugins/ralph-loop)
10
10
@@ -61,7 +61,7 @@ Todo lo demás es automático. El agent nunca se entera de que hay un bucle corr
61
61
-**Cero trampas del agent** — Al agent nunca se le dice que está dentro de un bucle. Sin `systemMessage`, sin contador de iteraciones, sin banner de arranque. No puede atajar emitiendo una señal de completado falsa.
62
62
-**Verificación con herramientas obligatoria** — Un turno de puro texto ("Lo he revisado, todo bien") jamás termina el bucle. El agent **tiene que** invocar una herramienta de verdad antes de que siquiera se plantee la salida.
63
63
-**Detección de cambios de archivos juzgada por un LLM y consciente del proyecto** — Una llamada headless a `claude -p --model haiku` es el **único** juez de "¿este turno modificó algún archivo del proyecto?". Ve la entrada completa de cada invocación de herramienta y decide semánticamente.
64
-
-**Aislamiento por sesión** — El archivo de estado se indexa por `TERM_SESSION_ID`, así que lanzar varios watchdogs en distintas pestañas de terminal nunca choca.
64
+
-**Aislamiento por sesión** — El archivo de estado se indexa por el ID del proceso padre de Claude Code, descubierto recorriendo la ascendencia de procesos. 100 watchdogs concurrentes en el mismo directorio de proyecto nunca chocan.
65
65
-**Oculto por diseño** — Toda la salida de diagnóstico va a stderr. El transcript JSONL nunca filtra metadatos del bucle al contexto del agent.
66
66
-**Apache 2.0** — Derivado de forma limpia del propio plugin `ralph-loop` de Anthropic, con la atribución completa en [NOTICE](./NOTICE).
67
67
@@ -121,38 +121,35 @@ Si alguna de las dos falla, el bucle continúa. Rutas de salida adicionales:
121
121
122
122
## Archivo de estado
123
123
124
-
El estado por sesión vive en `.claude/watchdog.<TERM_SESSION_ID>.local.json`:
124
+
El estado por sesión vive en `.claude/watchdog.claudepid.<PID>.local.json`, donde `<PID>` es el ID del proceso padre de Claude Code descubierto recorriendo la ascendencia de procesos. Ejemplo:
Cada sesión tiene su propio archivo, indexado por `TERM_SESSION_ID`. Lanzar varios watchdogs en distintas pestañas de terminal funciona sin conflictos.
137
+
Cada sesión de Claude Code tiene un PID distinto, así que **100 watchdogs concurrentes en el mismo directorio de proyecto nunca chocan** — cada uno tiene su propio archivo de estado, y `/watchdog:stop` en cualquiera de ellos solo cancela el bucle de esa sesión concreta.
138
138
139
139
**Monitorizar los watchdogs activos:**
140
140
141
141
```bash
142
142
# Lista todos los archivos de estado por sesión activos en este proyecto
**Matar manualmente todo lo que haya en este proyecto:**
153
150
154
151
```bash
155
-
rm -f .claude/watchdog.*.local.json
152
+
rm -f .claude/watchdog.claudepid.*.local.json
156
153
```
157
154
158
155
---
@@ -292,14 +289,11 @@ El clasificador Haiku no es infalible. Un agent atascado que no para de hacer ed
292
289
293
290
## Requisitos
294
291
295
-
Watchdog 1.1.0 es una **reescritura en Node.js**. Nada de bash, nada de jq, nada de POSIX coreutils — solo necesitas `node` y el `claude` CLI. Corre de forma nativa en Linux, macOS y Windows.
296
-
297
292
| Requisito | Por qué |
298
293
| --- | --- |
299
294
|**Claude Code 2.1+**| Usa el sistema de `Stop hook` y el formato de plugin del marketplace |
300
-
|**`node`** 18+ en el `PATH`|Toda la lógica de los hooks y del setup está escrita en JavaScript. `node:test` (que usa la suite de tests) requiere Node 18+|
295
+
|**`node`** 18+ en el `PATH`|Runtime de los hooks y scripts de setup del plugin|
301
296
|**`claude` CLI** en el `PATH`| Se usa para la llamada headless de clasificación con Haiku. Tiene que estar autenticado (OAuth o `ANTHROPIC_API_KEY`) |
302
-
| Variable de entorno **`TERM_SESSION_ID`**| Indexa el archivo de estado por sesión. La definen la mayoría de emuladores de terminal (iTerm2, WezTerm, terminales modernas de Linux). Workaround si no está definida: `export TERM_SESSION_ID=$(node -e "console.log(require('crypto').randomUUID())")` antes de lanzar `claude`. |
303
297
304
298
### Instalar dependencias
305
299
@@ -347,16 +341,13 @@ scoop install nodejs-lts
347
341
# o bájate el instalador desde https://nodejs.org
348
342
```
349
343
350
-
No hace falta WSL2 ni Git Bash — Watchdog 1.1.0 corre directo en Windows nativo.
351
-
352
344
### Soporte de plataformas
353
345
354
346
| Plataforma | Estado |
355
347
| --- | --- |
356
348
| Linux (Node 18 / 20 / 22) | ✅ Probado en CI |
357
349
| macOS (Node 18 / 20 / 22) | ✅ Probado en CI |
358
-
| Windows (Node 18 / 20 / 22) | ✅ Probado en CI (PowerShell / cmd nativo, sin WSL2) |
359
-
| WSL2 en Windows | ✅ Funciona (es Linux) |
350
+
| Windows (Node 18 / 20 / 22) | ✅ Probado en CI |
360
351
361
352
---
362
353
@@ -382,18 +373,21 @@ claude-code-watchdog/
382
373
├── lib/ # módulos compartidos (reutilizados por todos los entry points)
383
374
│ ├── constants.js # patrón del path del estado, tokens marcadores, plantillas de prompt
384
375
│ ├── log.js # diagnósticos por stderr
385
-
│ ├── stdin.js # lector síncrono de stdin cross-platform
376
+
│ ├── stdin.js # lector síncrono de stdin
386
377
│ ├── state.js # ciclo de vida atómico del archivo de estado
387
378
│ ├── transcript.js # parser JSONL + extracción de herramientas del turno actual
388
-
│ └── judge.js # subproceso headless de Haiku + parser del veredicto
379
+
│ ├── judge.js # subproceso headless de Haiku + parser del veredicto
380
+
│ └── claude-pid.js # recorrido de la ascendencia de procesos
389
381
├── test/ # tests unitarios + de integración con node:test
390
382
│ ├── fixtures/ # fixtures JSONL de transcripts
391
383
│ ├── transcript.test.js
392
384
│ ├── state.test.js
393
385
│ ├── judge.test.js
386
+
│ ├── claude-pid.test.js
394
387
│ ├── setup.test.js
395
388
│ ├── stop-watchdog.test.js
396
-
│ └── stop-hook.test.js
389
+
│ ├── stop-hook.test.js
390
+
│ └── stop-hook-haiku.test.js
397
391
├── .github/ # workflow de CI (matriz node --test, jsonlint, markdownlint) + plantillas de issue/PR
398
392
├── .gitattributes # fuerza los finales de línea LF
399
393
├── LICENSE # Apache License 2.0
@@ -402,32 +396,6 @@ claude-code-watchdog/
402
396
└── README.{zh,ja,ko,es,vi,pt}.md # traducciones
403
397
```
404
398
405
-
## Tests
406
-
407
-
Watchdog 1.1.0 viene con 59 tests automatizados usando el runner `node:test` integrado de Node — sin dependencias externas. Lánzalos desde la raíz del repo.
408
-
409
-
**Node 22+:**
410
-
411
-
```bash
412
-
node --test 'test/*.test.js'
413
-
```
414
-
415
-
**Node 18 / 20** (el soporte de globs se añadió en Node 21, así que o dejas que tu shell expanda el patrón, o listas los archivos a mano):
416
-
417
-
```bash
418
-
node --test test/*.test.js
419
-
```
420
-
421
-
Ejecuta solo un archivo:
422
-
423
-
```bash
424
-
node --test test/transcript.test.js
425
-
```
426
-
427
-
El CI corre la suite completa en `ubuntu-latest`, `macos-latest` y `windows-latest` con Node 18 / 20 / 22 en cada push y pull request.
@@ -439,9 +407,9 @@ Watchdog mantiene el mecanismo central — un `Stop hook` que vuelve a inyectar
439
407
|**Disparador de salida**| El clasificador headless con Haiku es el **único** juez. Lee la entrada completa de cada invocación de herramienta y decide semánticamente si se modificó directamente algún archivo del proyecto. | El agent tiene que emitir una etiqueta XML `<promise>…</promise>` en su texto final. La frase dentro de las etiquetas es configurable vía `--completion-promise "…"` (por ejemplo `COMPLETE`, `DONE`). Un grep en el `Stop hook` busca la cadena exacta. |
440
408
|**Precondición de salida**| Hay que haber llamado a herramientas **Y** que Haiku diga `NO_FILE_CHANGES`| Basta con que coincida el texto del `<promise>`. El agent puede hacer trampa emitiendo la etiqueta antes de tiempo; la única defensa de ralph-loop es un prompt que le pide al agent que no mienta. |
441
409
|**Visibilidad para el agent**| Completamente oculto (sin systemMessage, sin banner, diagnósticos solo por stderr) | Al agent se le informa del bucle y del protocolo del promise |
442
-
|**Ámbito del estado**|Archivo por sesión, indexado por `TERM_SESSION_ID`| Archivo único de estado a nivel de proyecto |
410
+
|**Ámbito del estado**|Un archivo de estado por cada sesión de Claude Code — sin límite de watchdogs concurrentes en el mismo proyecto | Un solo archivo de estado por proyecto — solo UN ralph-loop puede correr por proyecto a la vez|
443
411
|**Formato del archivo de estado**| JSON (parseado con `JSON.parse` nativo) | Markdown con frontmatter YAML (parseado con sed/awk/grep) |
444
-
|**Runtime**| Node.js 18+ — cross-platform (Linux, macOS, Windows nativo) | Bash + jq + POSIX coreutils — solo Unix|
0 commit comments