Skip to content

Releases: ron2k1/claude-code-structured-concurrency

v1.2.0 — macOS MEDIUM tier + cmd.exe shim

Choose a tag to compare

@ron2k1 ron2k1 released this 18 May 03:15

v1.2.0 — macOS MEDIUM tier + cmd.exe shim

Second cross-platform release, and it closes the roadmap item v1.1.0 promised: macOS support. macOS Claude Code users now get the same subprocess-hygiene story Windows and Linux users have — with an honest MEDIUM ceiling that is pinned by a test so it can never silently regress. cmd.exe users on Windows get the full STRONG guarantee for the first time.

This release also makes the CI matrix honest about hosted Intel macOS, which GitHub is retiring.

What's new

  • tools/macos/find-claude.sh — macOS probe resolver mirroring the Linux/Windows versions, including the fnm dual-directory layout. Resolves every supported Claude Code install path on Darwin (PATH → npm prefix → /opt/homebrew/usr/local → nvm → fnm → asdf → volta → yarn global).
  • tools/macos/claude-jobbed.sh — the macOS reaper: setpgid + bash trap + a disowned, out-of-process watchdog. macOS has none of the kernel primitives the other tiers lean on (no Win32 Job Object, no cgroup cgroup.kill, no prctl(PR_SET_PDEATHSIG)), so the watchdog is what carries the guarantee.
  • tools/claude-jobbed.cmd — cmd.exe shim. It re-execs the PowerShell wrapper and inherits the same STRONG Win32 Job Object. cmd.exe users now get the identical kernel-enforced kill-on-close guarantee PowerShell users have had since v1.0.0 — doskey claude=C:\path\to\tools\claude-jobbed.cmd $* and you are covered.
  • install.sh — real macOS Darwin branch with a portable BSD/macOS mktemp template, ~/.bash_profile handling, and an honest-ceiling banner printed at install time so the user is told the MEDIUM limitation up front, not buried in docs.
  • tests/macos/test-honesty.bats — a NEGATIVE test that pins the honest MEDIUM ceiling: it asserts that a simultaneous SIGKILL of wrapper and watchdog leaks by design. If someone ever "fixes" this into a false STRONG claim, this test fails. Honesty is enforced, not promised.
  • CI matrix — bats now runs on macos-14 (Apple Silicon) alongside ubuntu-latest, with PowerShell on windows-latest.
  • Hosted Intel macos-13 dropped, tracked as an open gap (#3). GitHub is retiring hosted Intel macOS; the macos-13 leg never received a runner and ran to GitHub's hard 24h "awaiting a runner" ceiling (timeout-minutes bounds execution after a runner is assigned, never queue time). The scripts are architecture-neutral and bash-3.2-safe by construction, and the static-parse step still proves the 3.2 syntax, so only x86_64 execution is uncovered — not syntax. Documented as an explicit open gap rather than silently removed, consistent with this project's honest-ceiling ethos.

Guarantee matrix

Platform Mechanism SIGKILL-of-wrapper-alone survival Simultaneous SIGKILL of wrapper + watchdog
Windows 10+ (PowerShell and cmd.exe) Win32 Job Object + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE ✅ Kernel-enforced ✅ Kernel-enforced (no watchdog needed)
Linux ≥ 5.14 + systemd --user systemd-run --scope + cgroup.kill (watchdog-supervised) ✅ Kernel-enforced via cgroup.kill ✅ Kernel-enforced via cgroup.kill
Linux < 5.14 / no systemd / WSL1 setpgid + bash trap ❌ (trap doesn't fire on SIGKILL)
macOS (v1.2.0, MEDIUM) setpgid + trap + disowned out-of-process watchdog ✅ Watchdog outlives the wrapper and reaps the tree By design — pinned honest by test-honesty.bats

The v1.1.0 matrix row macOS — deferred to v1.2.0 is now retired: macOS is shipped at the MEDIUM tier above.

Quick start

macOS

curl -fsSL https://raw.githubusercontent.com/ron2k1/claude-code-structured-concurrency/main/install.sh | bash -s -- --yes

The installer prints the honest MEDIUM ceiling banner. Then in a new shell:

claude --version  # routes through tools/macos/claude-jobbed.sh transparently

Windows cmd.exe (new in v1.2.0)

doskey claude=C:\path\to\claude-code-structured-concurrency\tools\claude-jobbed.cmd $*

claude from cmd.exe now runs inside the same STRONG Job Object as the PowerShell path.

Windows PowerShell / Linux

Unchanged — see the v1.1.0 release notes.

Why macOS is MEDIUM, not STRONG

Windows has JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; Linux ≥ 5.14 has cgroup.kill. Both are kernel-enforced tree-kill primitives — the OS itself guarantees no descendant outlives the job/scope. macOS has no equivalent: no Job Object, no kill-able cgroup, no PR_SET_PDEATHSIG. The closest honest construction is setpgid + a trap to reap the process group, plus a disowned watchdog (a backgrounded subshell that polls our identity and tears the group down when the wrapper vanishes).

That construction survives a Force-Quit / SIGKILL of the wrapper alone — the watchdog is a separate process, so it outlives the wrapper and reaps the tree. What it cannot survive is a simultaneous SIGKILL of wrapper and watchdog: bash traps don't fire on -9, and there is no kernel backstop on macOS to catch the gap. Rather than paper over that with an optimistic claim, tests/macos/test-honesty.bats asserts the leak happens in that exact scenario — so the ceiling is enforced by CI, not just documented.

Intel (x86_64) macOS coverage — the open gap

CI executes on Apple Silicon (macos-14) only. Hosted Intel (macos-13) execution is intentionally not a CI leg in v1.2.0 (GitHub hosted-Intel retirement; the leg ran to the 24h await-runner ceiling). The scripts are architecture-neutral and bash-3.2-safe by construction, and the static-parse step still proves bash-3.2 syntax — so only x86_64 execution coverage is missing, not syntax correctness. Restoring an Intel execution leg (self-hosted or paid runner) is tracked in #3.

What's next

  • v1.3.0+: optional macOS Swift kqueue helper to close the simultaneous-SIGKILL gap (would lift macOS toward STRONG); restored Intel macOS execution coverage (#3); Homebrew formula; apt PPA.

Stats

  • 36 PowerShell unit assertions + 1 functional test (Windows) + 41 bats tests (19 Linux + 22 macOS) — all passing in CI on ubuntu-latest, macos-14, windows-latest.
  • 9 ms reap latency verified on Windows 11 build 26200; macOS/Linux validated in CI (not micro-benched — no synthetic latency numbers claimed for the POSIX tiers).
  • macOS honest ceiling pinned by a dedicated negative test (tests/macos/test-honesty.bats).

v1.1.0 — Linux support

Choose a tag to compare

@ron2k1 ron2k1 released this 11 May 05:59
2b28322

v1.1.0 — Linux support

First cross-platform release. Linux Claude Code users can now get the same subprocess hygiene story Windows users have had since v1.0.0, with a kernel-enforced strong path on modern systems and an honest fallback on older or container-only systems.

What's new

  • tools/linux/find-claude.sh — 9-probe ordered resolution mirroring the Windows version (PATH → npm prefix → /opt/homebrew/usr/local → nvm → fnm → asdf → volta → yarn global). Handles every supported Claude Code install path on Linux + macOS.
  • tools/linux/claude-jobbed.sh — wrapper with two reaper paths picked at runtime:
    • STRONG: systemd-run --user --scope (kernel ≥ 5.14, default on Ubuntu 22.04 / Fedora 36 / Debian 12 +). Closest POSIX equivalent of Win32 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE. Survives SIGKILL of the wrapper itself via an out-of-process watchdog that triggers cgroup.kill on every descendant.
    • FALLBACK: setpgid + bash trap on EXIT/INT/TERM/HUP. Works on no-systemd containers, WSL1, minimal Alpine. Honest caveat: does NOT survive SIGKILL of the wrapper (bash traps don't fire on -9).
  • install.sh — top-level OS-detect router. --yes, --force, --uninstall flags. Idempotent shell-function injection in ~/.bashrc / ~/.zshrc / ~/.config/fish/config.fish. Refuses to duplicate without --force.
  • bats test suite — 19 tests pinning runtime + behavior on ubuntu-latest. Includes the load-bearing cgroup.kill strong-path test that SIGKILLs the wrapper and asserts the grandchild is reaped within 5s.
  • CI matrix.github/workflows/test.yml now runs PowerShell tests on windows-latest and bats on ubuntu-latest (with loginctl enable-linger so systemctl --user is active for the cgroup test).

Guarantee matrix

Platform Strong path SIGKILL-of-wrapper survival Fallback
Windows 10+ Win32 Job Object + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE ✅ Kernel-enforced n/a
Linux ≥ 5.14 + systemd --user systemd-run --scope + cgroup.kill (watchdog-supervised) ✅ Kernel-enforced via cgroup.kill setpgid + trap
Linux < 5.14 / no systemd / WSL1 n/a ❌ (trap doesn't fire on SIGKILL) setpgid + trap
macOS n/a ❌ (deferred to v1.2.0) n/a

Quick start

Linux

curl -fsSL https://raw.githubusercontent.com/ron2k1/claude-code-structured-concurrency/main/install.sh | bash -s -- --yes

Then in a new shell:

claude --version  # routes through claude-jobbed.sh transparently

Windows

Unchanged — see v1.0.3 release notes.

Why the watchdog matters

systemd-run --user --scope is a CONTROLLER, not a parent. Per systemd.scope(5): "When the last process leaves the scope, systemd cleans the scope up." Scope lifetime tracks DESCENDANT lifetime, not controller lifetime. So a SIGKILL of the wrapper just kills systemd-run; the scope stays alive with claude + grandchildren intact. Bash traps don't fire on SIGKILL either, so we cannot tear the scope down from in-process.

The watchdog is a backgrounded subshell that polls our parent identity via stat -c %Y /proc/$$ (the procfs entry mtime equals process start time, which is robust to PID reuse). When it observes the parent vanish, it runs systemctl --user stop $unit, which triggers cgroup.kill on every descendant — delivering the SIGKILL-survival guarantee the README documents.

What's next

  • v1.2.0: macOS support (setpgid + trap; honest documentation that Force-Quit case is not covered).
  • v1.3.0+: Optional macOS Swift kqueue helper for Force-Quit coverage; Homebrew formula; apt PPA.

Stats

  • 36 PowerShell tests + 19 bats tests pass on CI
  • 3-9 ms reap latency (Windows orphan cleanup)
  • ~5 s upper bound for cgroup.kill scope teardown after SIGKILL of wrapper

v1.0.3 -- fix -ShadowClaude wrapper crash on .cmd/.ps1 shims

Choose a tag to compare

@ron2k1 ron2k1 released this 11 May 05:10

Bugfix: -ShadowClaude wrapper crash on npm-installed Claude Code

If you ran the v1.0.2 installer with -ShadowClaude and Claude Code was installed via npm, every claude invocation through the wrapper crashed with %1 is not a valid Win32 application. This release fixes that.

Root cause

Start-Process -NoNewWindow requires a true PE binary. npm ships claude as claude.cmd (and claude.ps1) shims, not claude.exe. The wrapper passed the shim path directly to Start-Process, which the OS loader rejects with ERROR_BAD_EXE_FORMAT.

Fix

  • New tools/lib/SpawnPlan.ps1 helper -- pure function that maps the resolved claude path to the correct host process:
    • .exe -> direct spawn
    • .cmd / .bat -> cmd.exe /c <path>
    • .ps1 -> powershell.exe -NoProfile -ExecutionPolicy Bypass -File <path>
  • Find-ClaudeExe now prefers extensions .exe > .cmd > .bat > .ps1 when PATH returns multiple shims (the .ps1 shim hangs on redirected stdin due to interactive-mode autodetect).
  • 14 new unit tests in tests/test-spawn-plan.ps1 cover all four extensions plus uppercase / unknown / no-extension fallthrough.

Guarantee unchanged

KILL_ON_JOB_CLOSE still applies. The Job Object is assigned to the host process (cmd.exe / powershell.exe); on Windows 8+ children inherit job membership, so the real node.exe child the shim launches is still kernel-reaped on wrapper exit. Existing tests/test-job-object.ps1 still passes (8ms reap).

Upgrade

cd $env:USERPROFILE\.claude\skills\structured-concurrency
git pull
.\tests\run-all.ps1   # 22+1 tests should pass

No re-install needed -- the wrapper script is the file that changed; the -ShadowClaude PROFILE function continues to call it.

v1.0.2 — installer -ShadowClaude flag, README hardening, demo video

Choose a tag to compare

@ron2k1 ron2k1 released this 08 May 04:12

First GitHub release. v1.0.0 and v1.0.1 were documentation milestones in the commit log; v1.0.2 is the first cut anyone can git clone --branch v1.0.2 against a stable tag.

What this is

Kernel-enforced cleanup of orphaned Claude Code subprocesses on Windows. Wraps claude.exe in a Win32 Job Object so the OS reaps every child on exit, including crashes. Same kernel mechanism browser sandboxes use to bound renderer and tab lifetime — but no Node.js runtime wires it up for child processes spawned from Claude Code, so this skill does.

Three layers, each composable, each independently tested:

Tool Layer What it does
tools/cc-procs.ps1 Visibility Read-only inventory: PID, parent, age, memory, classification, orphan flag. No kill capability.
tools/cleanup-orphans.ps1 Cleanup Terminates strict-orphan subtrees per ~/.reap/config.json. Dry-run by default.
tools/claude-jobbed.ps1 Prevention Win32 Job Object wrapper. Kernel terminates the entire CC tree on wrapper exit.

Trust and scope

About 642 lines of PowerShell across tools/ and hooks/, plus 345 lines of tests. No network calls, no telemetry, no registry access, no Windows services. The default install is a guaranteed no-op until a ~/.reap/config.json opts in. Uninstall is three commands.

Full per-surface access scope (network, file reads, file writes, registry, process kills, shell profile, claude.exe, background services) with greppable verify commands lives in the README's Auditable section. Threat model, capability boundaries, and vulnerability reporting in SECURITY.md.

What's in v1.0.2

  • install-reap.ps1 -ShadowClaude flag — opt-in switch that redefines plain claude as a PowerShell function delegating to claude-jobbed.ps1. Function form (not Set-Alias) because PowerShell resolves Functions before PATH at parse time, so the function wins over claude.exe. Without the flag, you have to type claude-jobbed every time you want protection.
  • README rewritten for senior-dev posture — PowerShell-only callout (cmd.exe has no $PROFILE so the wrapper is bypassed), three-layer component table, explicit safety invariants, prose tightened to match the seriousness of the kernel mechanism it documents.
  • 58-second demo video at docs/demo.mp4. Shows the orphan MCP count dropping to zero on claude.exe exit, with cleanup driven by the kernel's Job Object close — not by application code.
  • 22 unit assertions plus 1 functional test passing on Windows 11 build 26200. 9 ms measured reap latency for the functional Job Object test.

Why this exists

Claude Code spawns 40-60 child processes per session (MCP servers, plugins, LSPs, hooks). On Windows, those children frequently outlive the parent. The leak class is well-documented in the upstream issue tracker — issue states are noted in brackets so the dossier is self-verifying:

  • #42169 [OPEN] — "claude.exe accumulates 13+ GB virtual memory, triggers Windows Resource Exhaustion"
  • #53134 [OPEN] — "[BUG] Windows: MCP servers spawned twice at startup (directMcpHost + LocalMcpServerManager)"
  • #28126 [CLOSED · NOT_PLANNED] — "[BUG] Task tool subagents spawn duplicate MCP servers and leak ~/.claude/tasks/ directories on Windows"
  • #40667 [CLOSED · DUPLICATE] — "MCP server processes leak on host after subagent/session termination"
  • #32304 [CLOSED · DUPLICATE] — "[Bug] Memory leak: claude.exe grows to 21GB+ on Windows during normal workflow with sub-agents"

Windows ships the OS-level primitive. Per Microsoft Learn:

A job object allows groups of processes to be managed as a unit. [...] Operations performed on a job object affect all processes associated with the job object.

JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE extends that: when the last handle to the job is closed, the kernel terminates every member process. There is no application code path that can leak. Linux has the equivalent in prctl(PR_SET_PDEATHSIG) plus cgroups; macOS has the equivalent semantics. Windows ships the primitive too — Node.js just doesn't wire it up.

This is structured concurrency as Nathaniel J. Smith framed it in 2018: child task lifetimes bounded by their parent, enforced by the runtime, not by application discipline. Application discipline is what produced the leaks in the first place.

Prior art

  • TheStack-ai/zcleannpx zclean, cross-platform reactive cleanup for AI coding tools (Claude Code, Codex, Cursor). Same problem class, different layer: zclean kills zombies after they form. claude-jobbed.ps1 (Layer 3 here) prevents zombies from forming in the first place by binding the process tree to a Win32 Job Object so the kernel reaps on parent exit. The two are complementary — zclean cleans up the past, the Job Object wrapper bounds the future.
  • Browser sandboxes (Chrome, Edge) use JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE to bound renderer-process lifetime. The mechanism is well-trodden — what's new here is wiring it up for Node-spawned MCP and plugin chains, which no Node.js runtime does on its own.

Install

git clone --branch v1.0.2 https://github.com/ron2k1/claude-code-structured-concurrency `
    "$env:USERPROFILE\.claude\skills\structured-concurrency"

& "$env:USERPROFILE\.claude\skills\structured-concurrency\tools\install-reap.ps1" -ShadowClaude

Open a fresh PowerShell window and confirm:

Get-Command claude
# CommandType=Function (Definition: claude-jobbed @args)  ->  wrapped
# CommandType=Application                                  ->  NOT wrapped

Verify

.\tests\test-job-object.ps1      # functional: spawns child, closes job, asserts reaped < 2s
.\tests\test-orphan-detect.ps1   # synthetic: orphan detection + PID-reuse guard via StartTime
.\tests\test-config-loader.ps1   # config: spare-wins-over-kill safety invariant

All three suites must pass before relying on the wrapper. Job Object behavior varies by Windows build; test-job-object.ps1 in particular catches kernel-level edge cases on older builds.

Safety guarantees

  • cc-procs.ps1 never kills. Run it any time.
  • cleanup-orphans.ps1 defaults to dry-run. Live kills require both -Force and a config that opts in. With no ~/.reap/config.json, the engine is a guaranteed no-op even with -Force.
  • The decision flow always runs spare_classifications before kill_names. claude.exe is classified as claude and claude is in the default spare_classifications, so it cannot be killed even if a user adds node.exe to kill_names. Tested explicitly in tests/test-config-loader.ps1.
  • claude-jobbed.ps1 is opt-in. Plain claude.exe still works without the wrapper, just unprotected.

Changelog

Version What changed
v1.0.0 (commit-log only) Initial three-layer skill (cc-procs / cleanup-orphans / claude-jobbed) + tests + four config profiles.
v1.0.1 (commit-log only) README adoption-gap warning, 15-question FAQ, dangerous-by-omission framing for the cleanup engine.
v1.0.2 (this release) -ShadowClaude installer flag, README polish + safety-invariant prose, 58-second demo video, SVG architecture diagram, test-config-loader hardening.

License

MIT. See LICENSE.