Summary
continuous-learning-v2's background observer (agents/start-observer.sh → agents/observer-loop.sh) is lazy-started via nohup ... & from inside a hook process (hooks/observe.sh, invoked per-tool-call through scripts/hooks/observe-runner.js → scripts/hooks/plugin-hook-bootstrap.js). On native Windows (Git Bash / MSYS2, not WSL), the backgrounded process is reaped almost immediately after the spawning hook process exits, so the observer never survives long enough to analyze observations.jsonl or produce instincts — even though observer.enabled: true is set correctly and the lazy-start logic runs without error.
Environment
- OS: Windows 11
- Shell for hook execution: Git Bash (MSYS2), confirmed via
uname -a → MINGW64_NT-...-Msys
- ECC installed via
npx ecc-install --profile developer --with capability:security (manual install, not the Claude Code plugin path)
- Node 24, Python 3.14 (not the WindowsApps redirector stub)
Repro
- Set
observer.enabled: true in continuous-learning-v2/config.json.
- Wire the plugin's
PreToolUse/PostToolUse observe hooks into settings.json (manual-install path — same effect as the plugin path enabling them).
- Trigger any tool call.
hooks/observe.sh's lazy-start logic fires correctly:
.observer.pid is created with a real PID.
observer-start.log logs Observer started (PID: <pid>).
- Immediately check the PID (
Get-Process -Id <pid> / tasklist /FI "PID eq <pid>"): process is already gone.
observer.log contains only the startup line — no periodic analysis entries, confirming the loop never got a chance to run its first cycle.
Root cause (best guess)
agents/start-observer.sh backgrounds the loop with:
nohup env ... "$OBSERVER_LOOP_SCRIPT" >> "$LOG_FILE" 2>&1 &
This is a POSIX pattern that assumes the child process detaches from the parent's process group and survives the parent's exit. On Windows, the parent chain here (Node spawnSync → bash.exe (MSYS2) → observe.sh → nohup bash &) is a chain of native Windows processes. Many Windows process-spawning frameworks (including Node's child_process in some configurations) create child processes under a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, which kills all descendants — including nohup-backgrounded ones — when the top-level spawning process (the hook, which is transient and exits right after the tool call) terminates. nohup/& under Git Bash/MSYS2 does not give the same process-group detachment guarantee as real Unix, since it's still CreateProcess under the hood, not fork().
This means the entire v2 observer daemon is silently non-functional on any Windows install that isn't running Claude Code itself inside WSL2.
Suggested fixes (any of)
- Detect Windows and use a Windows-native backgrounding mechanism instead of
nohup ... &, e.g. powershell -NoProfile -Command "Start-Process ... -WindowStyle Hidden" or scheduling via Task Scheduler, so the child process isn't tied to the parent's Job Object.
- Document the limitation explicitly in
docs/wsl-setup.md / the continuous-learning-v2 README: the background observer requires WSL2 (or macOS/Linux) and is a no-op on native Windows, so users aren't left with observer.enabled: true silently doing nothing.
- Add a startup self-check: after the lazy-start,
observe.sh (or start-observer.sh) could re-check the PID after a short delay and log a clear [Hook] observer failed to persist (native Windows backgrounding limitation) warning instead of only logging "Observer started" and going silent.
Related
The manual install path (npx ecc-install) places skills under skills/ecc/<name>/ instead of skills/<name>/, which several slash commands' path resolvers assume (referenced as issue #2037 in commands/instinct-status.md's own comments). That's a separate bug from this one, but worth linking since both surfaced together while setting up continuous-learning-v2 on a manual Windows install.
Summary
continuous-learning-v2's background observer (agents/start-observer.sh→agents/observer-loop.sh) is lazy-started vianohup ... &from inside a hook process (hooks/observe.sh, invoked per-tool-call throughscripts/hooks/observe-runner.js→scripts/hooks/plugin-hook-bootstrap.js). On native Windows (Git Bash / MSYS2, not WSL), the backgrounded process is reaped almost immediately after the spawning hook process exits, so the observer never survives long enough to analyzeobservations.jsonlor produce instincts — even thoughobserver.enabled: trueis set correctly and the lazy-start logic runs without error.Environment
uname -a→MINGW64_NT-...-Msysnpx ecc-install --profile developer --with capability:security(manual install, not the Claude Code plugin path)Repro
observer.enabled: trueincontinuous-learning-v2/config.json.PreToolUse/PostToolUseobservehooks intosettings.json(manual-install path — same effect as the plugin path enabling them).hooks/observe.sh's lazy-start logic fires correctly:.observer.pidis created with a real PID.observer-start.loglogsObserver started (PID: <pid>).Get-Process -Id <pid>/tasklist /FI "PID eq <pid>"): process is already gone.observer.logcontains only the startup line — no periodic analysis entries, confirming the loop never got a chance to run its first cycle.Root cause (best guess)
agents/start-observer.shbackgrounds the loop with:This is a POSIX pattern that assumes the child process detaches from the parent's process group and survives the parent's exit. On Windows, the parent chain here (
Node spawnSync→bash.exe(MSYS2) →observe.sh→nohup bash &) is a chain of native Windows processes. Many Windows process-spawning frameworks (including Node'schild_processin some configurations) create child processes under a Job Object withJOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, which kills all descendants — including nohup-backgrounded ones — when the top-level spawning process (the hook, which is transient and exits right after the tool call) terminates.nohup/&under Git Bash/MSYS2 does not give the same process-group detachment guarantee as real Unix, since it's stillCreateProcessunder the hood, notfork().This means the entire v2 observer daemon is silently non-functional on any Windows install that isn't running Claude Code itself inside WSL2.
Suggested fixes (any of)
nohup ... &, e.g.powershell -NoProfile -Command "Start-Process ... -WindowStyle Hidden"or scheduling via Task Scheduler, so the child process isn't tied to the parent's Job Object.docs/wsl-setup.md/ the continuous-learning-v2 README: the background observer requires WSL2 (or macOS/Linux) and is a no-op on native Windows, so users aren't left withobserver.enabled: truesilently doing nothing.observe.sh(orstart-observer.sh) could re-check the PID after a short delay and log a clear[Hook] observer failed to persist (native Windows backgrounding limitation)warning instead of only logging "Observer started" and going silent.Related
The manual install path (
npx ecc-install) places skills underskills/ecc/<name>/instead ofskills/<name>/, which several slash commands' path resolvers assume (referenced as issue #2037 incommands/instinct-status.md's own comments). That's a separate bug from this one, but worth linking since both surfaced together while setting up continuous-learning-v2 on a manual Windows install.