[BUG] macOS 26.5.1 kernel zone leak (data.kalloc.1024) from Claude Code CLI — claude.exe panics at ~20GB, leak rate scales 21→1027/sec with agent load
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Claude Code CLI triggers an unbounded kernel memory leak in macOS zone data.kalloc.1024. The leak is in kernel space, not Claude's userspace RSS. Zone count climbs from ~1,200 after reboot to ~21M elements (~20 GB), then the machine kernel panics.
Leak rate scales with agent load (measured with sudo zprint):
- ~21 elements/sec idle
- ~103/sec moderate use
- ~1,027/sec under heavy parallel agent work (shell tools, git, file I/O)
At burst rate, time to panic is on the order of hours, not days. Four kernel panics in eight days (May 31 – Jun 6 2026), all with the same signature.
This is not the MCP process fan-out pattern (#64366 / #45880): only 1 MCP server configured (obsidian). Panicked task is always claude.exe, not hundreds of node MCP children.
Same mechanism as #44824 (closed duplicate, Apr 2026) — still reproducing on macOS 26.5.1 with quantified leak rates.
What Should Happen?
Claude Code sessions (including concurrent CLI sessions under load) should not cause unbounded kernel kalloc.1024 growth or macOS kernel panics. Kernel zone usage should stay bounded; terminating Claude should stop further kernel leak growth (as reported in #44824 when the process is killed).
Error Messages/Logs
panic(cpu 6 caller 0xfffffe00392ab31c): zalloc[3]: zone map exhausted while allocating from zone [data.kalloc.1024], likely due to memory leak in zone [data.kalloc.1024] (20G, 21286288 elements allocated) @zalloc.c:4575
Panicked task: pid 14507: claude.exe
Kernel Extensions in backtrace:
com.apple.iokit.EndpointSecurity (1.0)
com.apple.filesystems.apfs (2811.120.14)
Live monitoring (same session, heavy load):
now=06:18:37 zone=3364417 (~3.21 GB of the 20 GB ceiling)
rate=1027/sec over 30s (delta 30834)
at this rate, time to 21M ceiling: ~4h (285min)
started this session at ~1,200; now 3364417
Steps to Reproduce
- Environment: macOS 26.5.1 (25F80), Mac17,2, 32 GB RAM, Claude Code 2.1.168, Terminal.app, 1 MCP server (obsidian).
- Reboot Mac (clean zone:
data.kalloc.1024~1,200 elements).
- Start 3–4 concurrent
claudeCLI sessions on a large monorepo (active agent work: Bash, git, ripgrep, file reads/writes).
- In another terminal (optional), monitor zone every 30–60s:
zprint 2>/dev/null | awk '$1=="data.kalloc.1024"{printf "%s zone=%s (~%.2f GB)\n", strftime("%H:%M:%S"), $7, $7*1024/1073741824}'
- Observe monotonic zone growth; rate increases sharply under parallel agent + shell activity.
- Expected failure: kernel panic when zone approaches ~21M elements (~20 GB), typically within hours under heavy load.
Control: Kill all claude processes — growth should stop (per #44824). Reboot resets zone to ~1,200.
Claude Model
Opus
Is this a regression?
Yes, this worked in a previous version
Last Working Version
_No response_
Claude Code Version
2.1.168 (Claude Code)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
Hardware: Mac17,2, 32 GB RAM
Configured MCP servers: 1 (obsidian only) — rules out N×M MCP fan-out (#64366, #45880) as primary cause here.
Related issues:
- #44824 — same kalloc.1024 / claude.exe panic, closed duplicate, unresolved from reporter POV
- #64366, #45880 — MCP multiplication; different mechanism on this setup
Apple side: Kernel zone leak in data.kalloc.1024 with EndpointSecurity + APFS in backtrace; panic logs submitted / to be submitted via Feedback Assistant (UUID e.g. 67756C3A-BD42-4268-ABB8-E7A0DDA4F992).
Workarounds tried: Spotlight Privacy on ~/.claude, ~/.grok, ~/.codex, worktrees; pruning ~44 GB agent worktrees — reduces indexing pressure but does not stop active kalloc.1024 growth under load.
Request: Reopen or link tracking for #44824; investigate Claude Code's macOS file/sandbox/EndpointSecurity path on macOS 26; workarounds beyond "reduce concurrent sessions" (not viable for production deadlines).
31 Comments
Found 1 possible duplicate issue:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
regression between 2.1.143 and 2.1.158, kalloc.1024 leak on macOS, clean
on 2.1.123.
Timeline — why this feels "new" despite an unchanged workflow
This machine has no kernel panic logs before May 31, 2026 — the acute phase is real, not imagined. But the underlying leak pattern matches #44824 (April 2026, macOS 15.7.x), so this is likely a long-standing Claude Code ↔ macOS kernel interaction that only became visible here once load and uptime crossed the ~20 GB kalloc.1024 ceiling.
Chronology on this hardware (Mac17,2, 32 GB):
┌───────────┬──────────────────────────────────────────┐
│ Date │ Event │
├───────────┼──────────────────────────────────────────┤
│ Apr 28, │ macOS 26.4.1 installed via Software │
│ 2026 │ Update │
├───────────┼──────────────────────────────────────────┤
│ May 31, │ First kernel panic ever on this machine │
│ 2026 13: │ — still on 26.4.1 (~33 days after the OS │
│ 00 │ update) │
├───────────┼──────────────────────────────────────────┤
│ Jun 2, │ Second panic — still 26.4.1 │
│ 2026 16: │ │
│ 39 │ │
├───────────┼──────────────────────────────────────────┤
│ Jun 2, │ macOS 26.5.1 installed (after the second │
│ 2026 17: │ panic) │
│ 10 │ │
├───────────┼──────────────────────────────────────────┤
│ Jun 4, │ Third and fourth panics — on 26.5.1 │
│ Jun 6 │ │
└───────────┴──────────────────────────────────────────┘
So 26.5.1 did not introduce this (two panics on 26.4.1 preceded it), and 26.5.1 has not fixed it (two more panics after).
Why "same workflow" can still trigger panics now:
Working hypothesis: Apple kernel leak in data.kalloc.1024 (likely EndpointSecurity/APFS path on macOS 26.x), triggered by Claude Code's sustained file I/O + agent shell churn. The bug was probably always present at low leak rates; May 31–Jun 6 is when parallel sessions + multi-day uptime + accumulated ~/.claude state pushed growth fast enough to hit the zone ceiling repeatedly. Requesting investigation of Claude's macOS file-access / sandbox / EndpointSecurity interaction on Tahoe 26.x, and a mitigation short of "never run concurrent sessions" (not viable for production deadlines).
Reproduced on macOS 26.x: kalloc.1024 kernel leak → hard panic,
regression bisected to late-May 2.1.14x builds
Adding hard data to this (saw it filed against macOS 15.7.x; I'm hitting
the same kalloc.1024 leak on macOS 26.5.1 / Apple Silicon, and it's now
causing full kernel panics, not just memory pressure).
Impact: Recurring kernel panics (data-loss-level, machine reboots)
roughly every 12–48h of normal use. Four captured in 7 days.
Panic signature (all 4 identical):
panic: zalloc[3]: zone map exhausted while allocating from zone
[data.kalloc.1024],
likely due to memory leak in zone [data.kalloc.1024] (20G,
~21,300,000 elements)
Panicked task: pid NNNN: claude.exe
Kernel Extensions in backtrace: com.apple.iokit.EndpointSecurity
~21.3M × 1024 B ≈ 20 GB = the zone-map cap, every time. The
EndpointSecurity kext is the allocator hitting the exhausted zone, not
the leaker.
Confirmed Claude Code is the producer — live zprint of data.kalloc.1024:
┌───────────────────────────────────┬────────────────────────┐
│ State │ Leak rate │
├───────────────────────────────────┼────────────────────────┤
│ Fresh boot, idle │ ~0 (~1,200 elements) │
├───────────────────────────────────┼────────────────────────┤
│ ~16 idle claude --resume sessions │ ~22/sec │
├───────────────────────────────────┼────────────────────────┤
│ Sessions actively "thinking" │ bursts 200 → 4,744/sec │
└───────────────────────────────────┴────────────────────────┘
Stops when Claude Code is killed. Not exec/file volume — 20k execs,
100-way concurrent bursts, and 20k file-opens all produced zero growth.
Only live TUI sessions leak.
Regression bisect (from local transcripts), same HW / workload / macOS
(26.4.1 throughout onset):
zero panics
despite the spinner color caps.
Plus the changelog ties it together (spinner color count, renderer style
pool, layout-engine JIT churn in 2.1.144→161), and asks: treat as high
severity (kernel panic), find/fix the rendering change, and ship an
interim flag to disable the animated spinner / reduce redraws.
Confirming this reproduces with concurrent opencode + Claude Code sessions on macOS 26.5.1 (25F80).
Setup:
Observed:
zalloc[3]: zone map exhausted while allocating from zone [data.kalloc.1024] (20G, ~21M elements)bash(via AMFI/EndpointSecurity path)When either tool runs alone, the leak rate is lower; running both together appears to叠加 the leak rate, reaching 20GB ceiling much faster than either individually.
Happy to provide panic logs or run any diagnostics if helpful.
Follow-up after reviewing @LeifErikH's detailed bisect data above:
The timeline clearly shows this is a Claude Code regression between 2.1.143 and 2.1.144–154, not a macOS issue. The same machine ran 2.1.123 for 18 days on the same macOS 26.4.1 without a single panic.
The 26.4.1 → 26.5.1 upgrade date (Jun 2) is a red herring — the first panic already occurred on May 31 with 2.1.154 on 26.4.1.
Action taken — personally downgrading to 2.1.143 to verify stability, and treating all _2.1.144+_ CI sessions as high-risk on any macOS 26.x.
This bifurcation data (clean before 2.1.144, leaking after) should narrow the search to the renderer change landed in that window: spinning spinner styling, layout-engine JIT, or style pool sizing. Hope the team can isolate from there.
If anyone else can reproduce: run 2.1.143 (Claude Code), and if you're on 2.1.144+, check if reverting to 2.1.143 brings your data.kalloc.1024 1024 0K 0K 0 0 594 0K 0 count back to idle.
Additional evidence: the leak is in the EndpointSecurity vnode-check path, and scales with file-open (vnode) volume — not GPU/WindowServer
I spent a long debugging session on this on an M1 MacBook Air (macOS 26.5.1 / 25F80, 16 GB). I can confirm the
data.kalloc.1024leak and the ~50 MB/h rate, but my DTrace evidence points to a different mechanism than the GPU/WindowServer pipeline hypothesized above. Sharing in case it helps narrow it down.TL;DR
mac_vnode_check_openpath — i.e. one allocation per file-open (vnode open) event.Measurements (DTrace, SIP temporarily disabled for fbt probes)
1. The hot allocation stack (clean 20s aggregate,
kalloc_type_implentry):The dominant 1 KB allocations come from the ES vnode-check path allocating a
PerMessageScratchSpaceTargetPathDatascratch buffer per event.2. The leak scales with file-open volume (reproducible):
| State | data.kalloc.1024 growth |
|---|---|
| True idle (no activity at all) | ~0 elem/s (zone stable / shrinks) |
| Normal activity | ~14 elem/s (~55 MB/h) |
| Heavy file-open load (
find /System \| head -c1) | ~321 elem/s (~22× idle) |Generating a burst of file opens spikes the leak ~22×, then it returns to baseline. This is the single most decisive result: the leak tracks vnode-open count.
3. Ruled out (by SIGSTOP / removal + re-measure): 3rd-party audio HAL plugins, coreaudiod, Hammerspoon, Karabiner, iStat Menus, Tailscale, capture-card/UVC pipeline, and IOUserClient leakage (IOUserClient count stayed flat while kalloc.1024 grew). None of these are the source.
4. Cross-machine corroboration: the same rate occurred on a second Mac (MacBook Pro) with no AdGuard and no Rogue Amoeba software installed, but with Claude Code. The common denominator across both machines is Claude Code.
Why this is consistent with "only Claude Code triggers it"
A normal desktop generates few vnode-opens. Claude Code generates a very high sustained vnode-open rate (each tool call spawns a shell that sources a ~6k-line shell snapshot, plus file reads, greps, MCP polling). Every vnode-open goes through the kernel's EndpointSecurity AUTH path (the always-on Apple ES clients —
xprotectd,watchdogd— subscribe to vnode events), and the per-event scratch buffer leaks. On an average user's machine the rate is low enough to never hit the ceiling before a routine reboot — which is likely why this isn't a widely-reported universal Apple bug.Suggested framing for a fix
es_vnode_check_openpath under high event volume (worth a Feedback Assistant report).Caveats / what I did NOT prove
xprotectdvswatchdogd) is not isolated — they are SIP-protected and survive SIGKILL, so I couldn't A/B them.Environment: M1 MBA, macOS 26.5.1 (25F80), 16 GB. Happy to run targeted DTrace if a maintainer wants a specific probe.
Independent reproduction here, with a few data points the original repro couldn't cover — most importantly that it persists with the fullscreen renderer active, which rules fullscreen out as a fix.
Environment
What's new vs the OP
/tui fullscreen, yetdata.kalloc.1024still climbs monotonically. So the leak is independent of the renderer mode — fullscreen throttles the rate but does not stop it. (The OP was on Terminal.app and explicitly N/A on fullscreen.)Measurements (
zprint)data.kalloc.1024= ~4.15M elems ≈ 3.96 GB, which is the bulk of kernel zoneTOTAL USED(~4.78 GB) and of 6.72 GB wired. Every other zone is normal-sized (APFS inodes ~100 MB, vm.objects ~90 MB) — the leak is singular to this zone.claudehalts growth but does not return the zone; only reboot resets it (matches the kill experiment in #44824).Triage note
#44824 — same
data.kalloc.1024signature — was auto-closed by a bot as a duplicate of #24960, but #24960 is a userspace/RSS process leak (closednot_planned), a different bug class (frees on process kill; this does not). The kernel-zone leak keeps getting swept into userspace memory-leak buckets. Seconding the request to track this distinctly and investigate Claude Code's macOS rendering/EndpointSecurity interaction on macOS 26. Happy to providezprintdumps or a panic log.Follow-up to my Jul 1 comment — I ran a controlled workload-discrimination series on the affected machine (MacBook Air M2/T8112, 24 GB, macOS 26.5.1, Claude Code 2.1.198). Method:
zprintsamples ofdata.kalloc.1024in-use elements bracketed around each workload, on a quiet machine (all other Claude sessions closed; ambient baseline ~1.0 elems/s interleaved between every leg).| Workload | Excess above ambient |
|---|---|
| 15,732 real file-opens (recursive
grep -rlover /usr/share) | zero (delta −22) || 296,281-file
findtraversal (stats/readdirs) | zero (+11) || 2,000 process execs (
/usr/bin/trueloop) | ~zero (+38, ≈0.02/exec) || Synthetic 30 fps full-screen ANSI repaint, 15 min in detached tmux | zero (1.00/s vs 1.00/s ambient) |
| Same repaint workload, 15 min rendered in a live Terminal.app window | zero (1.01/s) |
| pytest run (44 tests, heavy import/file churn) | zero (+2) |
| 600 TLS connects (curl loop) | ~0.18 elems/connect |
| 100 MB sustained HTTPS download (verified via
size_download) | ~1/s during transfer || Fresh minimal Claude Code session doing real SSE-streamed work (small context, no MCP servers, driven via tmux pty) — run twice, once with telemetry on and once with
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1| zero (0.99/s and 1.22/s vs ~1.0–1.5/s ambient brackets) || Agent probe inside a loaded, long-running Claude Code session (days-old process, 200k+ token context, MCP servers attached; probe = 87 s, 15 tool calls, ~71k tokens SSE-streamed with no TTY rendering of its own) | ~3/s sustained |
Takeaways for the thread:
The 2.1.143→2.1.144–154 regression bisect upthread remains the best lead; these results suggest looking at whatever high-frequency runtime behavior (streaming/telemetry/IPC cadence) changed in that window that specifically engages long-lived, heavily-loaded sessions rather than the rendering path.
Follow-up to my 2026-07-02 comment — I ran a live kill-bisect against the leak with a 10-second zone sampler and can now attribute it much more precisely.
Environment: Claude Code 2.1.200, macOS 26.5.1 (Apple Silicon, M-series, 24 GB). Zone measured:
data.kalloc.1024in-use elements viazprint, sampled every 10s.Headline findings
1. Idle sessions leak at a constant per-session rate — activity is not required.
Overnight, 8 idle long-running sessions (~12-22h old, no user input, no agents running) leaked at 12.50/s for 11 straight hours — hourly means 12.48-12.53/s. That constancy is a fixed-cadence mechanism, not workload. Interactive activity adds bursts on top of the floor; session startup/resume spikes but never reclaims.
2. The leak attributes to the
claudeprocess itself — every spawned component was individually exonerated by live kills while watching the zone:| Component killed/toggled | Effect on leak floor |
|---|---|
| Quit one aged session | -1 to -1.5/s each (reproducible across 5 session quits) |
| ALL 8 MCP server child processes at once | ~-1.1/s total (~0.14/s each) — minor |
| statusLine 1 Hz command spawn (config-disabled, spawn-stop verified) | no change |
| Terminal attachment (fresh session detached in tmux vs in Terminal.app) | no difference |
| Idle network:
nettopshows 1 static TLS connection/session, byte counters frozen | no churn to blame |3. It's churn, not accumulation — the process holds nothing.
Comparing a 22-hour session against a 3-minute session (both idle): open fds 25 vs 26 (
lsof), mach ports 148 vs 122 (lsmp), kqueue knotes ~30 vs ~29 with 3EVFILT_TIMERentries each (lskq) — all flat across repeated samples minutes apart. Meanwhile the zone grows ~1.5 elements/s per aged session. So the kernel is orphaning ~1KB allocations on some recurring per-tick cycle the process drives, consistent with the kubectl-retry-loop reproducer earlier in this thread. Leading suspect from what an idle session still does at fixed cadence: event-loop timer (kevent) re-arm churn.4. Leak rate grows with session age.
Fresh idle sessions contribute little (possibly ~0.7/s each, hard to separate from noise — consistent with my 07-02 observation that fresh fleets are clean). Sessions aged ~22h contribute ~1.2-1.5/s each while completely idle. Whatever the churn cycle is, it intensifies with process lifetime, not context size (sessions were regularly /clear'd).
Practical arithmetic
~1.5/s × 1KB ≈ 5 MB/h of unreclaimable wired memory per aged idle session — about 1 GB/day for a fleet of 8. Matches my observed 745 MB overnight growth. Only reboot reclaims.
What I didn't do
The remaining step is
zlog1=data.kalloc.1024zone logging to capture allocation backtraces and name the exact kernel path. On Apple Silicon that requires a Permissive Security downgrade + two Recovery round-trips + ~a day of session aging to re-create the reproducer — a big ask for a user machine, but presumably trivial on an internal test box with kernel symbols. Happy to provide raw sampler CSVs or run cheaper diagnostics if useful.Another independent reproduction, on a newer OS build and different Mac model than reported above:
Environment
Darwin 25.5.0: Tue Jun 9 22:28:34 PDT 2026; root:xnu-12377.121.10~1/RELEASE_ARM64_T6050Panic
Element count at panic (21,173,392) lines up closely with the OP's ~21,286,288 and the earlier ~21,300,000 report — the ~20GB/~21.3M-element ceiling looks like a hard constant across machines, not something that varies by RAM size or config.
This confirms the bug is still present on:
Consistent with the thread's conclusion that this is a kernel-side leak tied to the
claudeprocess rather than anything model/config-specific. Happy to share the full panic report if useful — reboot was the only recovery.Another independent reproduction — plus a datapoint suggesting the trigger may be gone in 2.1.208.
Environment
xnu-12377.81.4~5/RELEASE_ARM64_T6031claude.exeprocname), heavy multi-session agent use (loops, subagents)Panic (2026-07-13 22:25:56 -04) — same signature as OP:
Element count at panic (21,269,200) matches the ~21.3M ceiling reported across machines. This extends the affected-OS range down to 26.3/25D125 (previously reported on 15.7.x, 26.4.1, 26.5.1, 26.5.2).
Leak history for that uptime (6.12 days, builds pre-2.1.208):
bug_type 298JetsamEvent 7.4 h before the panic already showed the zone at 18.4 GiBnode, 12 concurrent claude.exe CLI sessions)The interesting part — same machine, same OS build, after reboot on 2.1.208:
The npm package was updated to 2.1.208 at 22:23 (3 minutes before the panic — the
npm install -gvnode churn under EndpointSecurity was plausibly the final allocation burst). After reboot, still on the same macOS 26.3 (25D125), with 5 concurrent CLI sessions + 45 node processes doing real agent work:| Measurement (via
zprint, ~45–65 min uptime) |data.kalloc.1024in-use ||---|---|
| Baseline | 6 |
| After 500 execs + ~2,600 file-opens (
grep -rl) + 10 TLS connects | 6 (zero growth) || Bracketed ~3 min sampling with an active streaming session | 6 (zero growth) |
The per-idle-session floor (~1–1.5 elems/s each, reported above by @spectre6000) is absent — with 5 sessions we'd expect hundreds of elements over the sampling window; we saw none. Zone capacity didn't grow either (576 elems).
Since OS build, hardware, and workload are unchanged and only the Claude Code version moved to 2.1.208, this looks like the trigger was removed/fixed somewhere in 2.1.208 (nothing in the changelog obviously matches — the 2.1.207 entries are userland RSS fixes and 2.1.207 still reproduced per the report above).
A root LaunchDaemon is now sampling the zone every 30 min on this machine; I'll report back if it regresses. Would be useful if others on ≥ 2.1.208 could re-check their
data.kalloc.1024growth with the OP's zprint one-liner.🤖 Generated with Claude Code using Fable 5
Counter-datapoint to the "trigger may be gone in 2.1.208" report above: the leak is still present on 2.1.209, on the same machine I used for the 07-02 and 07-03 comments.
Environment
xnu-12377.121.10~1/RELEASE_ARM64_T8112Measurement —
data.kalloc.1024in-use elements, 60s sampler, two independent samplers agreeing:| Time | in-use | Δ | rate |
|---|---|---|---|
| 07:47:51 | 3,604 | — | — |
| 07:48:51 | 3,811 | +207 | 3.45/s |
| 07:49:52 | 4,462 | +651 | 10.67/s |
| 07:50:52 | 5,092 | +630 | 10.50/s |
| 07:51:52 | 5,535 | +443 | 7.38/s |
| 07:52:53 | 6,069 | +534 | 8.75/s |
| 07:53:53 | 6,817 | +748 | 12.47/s |
| 07:54:54 | 7,339 | +522 | 8.56/s |
~8.9 elems/s sustained mean, monotonic, never reclaiming. For reference, this machine's historical quiet baseline was ~1.0/s, and my overnight 8-aged-session idle floor was 12.50/s — so ~8.9/s with only 4 fresh sessions at 26 minutes uptime is squarely in leaking territory, not near-zero.
This contradicts the 2.1.208 observation under closely comparable conditions (that test was 5 sessions at 45–65 min uptime, reporting a flat 6 elements). The rate here also isn't an artifact of my own tooling: the growth continues at ~8.6/s across a window where the sessions were idle between turns, and my 07-03 controls already exonerated execs, file I/O, and the sampler itself.
The variable that differs is the macOS build. The clean 2.1.208 machine is on 26.3 (25D125); this one is on 26.5.2 (25F84) — the same build @visualpharm reproduced the panic on with 2.1.207. Given the thread's evidence that this is a kernel-side zone bug the CLI merely triggers, "fixed in 2.1.208" may be build-dependent rather than a real removal of the trigger, or the 26.3 machine may simply not engage the vulnerable kernel path.
Suggested control for anyone else re-checking: report your macOS build number alongside the CLI version, since the two reports currently disagree and differ on exactly that axis.
Caveat on my own numbers: these are fresh sessions at short uptime. My 07-03 finding was that per-session rate grows with process age (~0.7/s fresh → ~1.2–1.5/s at ~22h), so this should if anything be the optimistic end. I have a continuous sampler running and will post an aged-fleet figure after a full day.
Happy to share the raw CSVs.
I had the same issue, and I was able to find the root cause and resolve it.
TL;DR;
🎉
The root cause is the npm installation. Steps to fix it:
🎉
The long story:
I've spent the last 2 weeks investigating this with Claude across several macOS machines, and the outcome is a complete causal chain with a deterministic reproduction and a natural control group: the kernel bug is Apple's, the trigger is npm-based Claude Code updates rewriting the package under running sessions — and the native installer already ships the fix.
The kernel bug (Apple's)
While handling an event for an EndpointSecurity client, XNU calls vn_getpath; when it fails with 0x2 (a vnode with no resolvable path), the 1KB path buffer (zone data.kalloc.1024, tag vfs.namei) is never freed. Each failure logs:
kernel (EndpointSecurity) vn_getpath failed: 0x2
1:1 proof (live, same clock): 2,039 logged failures over 8 quiet minutes ↔ zone +2,055 KB (99.2%); 116,991 failures over 5 loaded minutes ↔ zone +124,171 KB (94%, ~390/s; the kernel rate-limits its own log lines, so counts are lower bounds). Failure counts track zone growth across entire days. Only reboot reclaims the zone; at ~20GB (~21M elements) the machine panics — the panicked task (claude.exe, bash) is just the last allocator, not the leaker.
The trigger (reproduced on demand, plus a smoking-gun observation)
On a machine idling at a 3 failures/sec floor for 40+ minutes, running npm install -g @anthropic-ai/claude-code — what the auto-updater does on npm installs — under 4 running CC sessions stepped the rate to a sustained ~180/sec measured with every session idle (~60×). Replication: further downgrade/upgrade cycles kept stale sessions at ~240–350/s idle; the rate collapses only when the sessions are restarted (observed twice).
Why: npm updates delete and re-extract the package — the old 240MB binary loses both its hardlinks and its parent directories while running sessions keep executing it. The old inode survives only as a nameless "deleted but open" file. Smoking gun, visible with plain lsof on any post-update session: the process's txt segment is the old, deleted inode, and the kernel renders its path as just /claude — a degenerate fallback, because there is no directory entry left to walk. vn_getpath reconstructs paths by walking parent directory entries; for this vnode there is nothing to walk → 0x2. EndpointSecurity includes the originating process's executable path in every event it delivers to ES clients, so every file op a stale session tree performs = one failed resolution = 1KB leaked. Auto-updates land every day or two; sessions stay open for days; stale trees accumulate → the leak "compounds" over a workweek → 20GB ≈ 3 workdays for heavy users. This also explains the suspected "regression at 2.1.144" (the era of the large native binary rewritten in place) without any CC code being at fault.
The natural control group: the installer split
Users on the native installer (curl -fsSL https://claude.ai/install.sh | bash) cannot reproduce — it stores versioned binaries and swaps a launcher symlink, so the version a running session executes keeps its directory entries and stays resolvable. Only npm-installed machines leak. Same macOS (26.5.x), same security stack, same usage — update strategy is the discriminating variable. Live meters on the npm machines: 144,538 and 327,550 vn_getpath failures per 10 minutes on the two hottest (the latter at 9.2GB of the 20GB ceiling with 7 sessions open; one capture caught the auto-updater streaming 14,800 write events into the claude binary in 10 seconds).
Check your machine (no root)
/usr/bin/log show --last 10m | grep -c "vn_getpath failed"
≈ leaked KB per 10 min. Zone level: sudo zprint data.kalloc.1024 | awk '$1=="data.kalloc.1024"{printf "%d MB of ~20480 panic ceiling\n", $3/1024}'. If lsof -p <claude pid> | grep txt | head -1 shows a bare /claude path — that session is leaking right now.
Workarounds
Asks
Anthropic: (1) npm-installed CC should prompt/auto-restart sessions after self-update — or npm installs should adopt the versioned layout the native installer already has; (2) route the kernel bug to Apple — the diagnosis is as complete as userspace allows (log fingerprint, 1:1 allocation arithmetic, deterministic trigger, installer-split control group, multiple reproducing machines), and their fix is an error-path free in the ES kext.
Confirming @DimitarTachev's root cause end-to-end on the machine from my 07-01/02/03/14 comments — plus two corrections to my own earlier claims, and two datapoints nobody in-thread has yet (a deliberate version-swap stress test of the native installer, and a live auto-update firing over running sessions).
Corrections to my 07-14 comment
Confirmation on this machine
Smoking gun, pre-migration: every running session's executable mapping was a nameless deleted inode. Diagnostic anyone can run, no root:
Bare
/claude(no versioned path) = that session is orphaned and leaking on every file op. All 4 of my pre-migration sessions showed it, with the on-disk binary's inode ≠ the mapped inode.Leak meter, no root needed:
log show --last 10m | grep -c "vn_getpath failed"≈ leaked KB per 10 min (each failure = one 1KBdata.kalloc.1024element stranded). With 3–4 orphaned sessions idle this machine sat at ~5.7–6.1 failures/s. A fresh native-install session added ~0 on top.Post-migration acceptance run (today, after
npm uninstall -g+ native install + reboot): 10 hours of real workload — 4 concurrent CLI sessions, sustained load average ~15–29 (heavy test/mutation runs) — sampled every 10 minutes:vn_getpath failedcount: 0 on every single sample, all day.data.kalloc.1024in-use: flat at ~1.1–1.25k elements (ambient floor; the panic ceiling is ~21.3M). Under the old install this workload leaked tens of MB/hour, monotonic.Two datapoints beyond "migrate and it stops"
1. Deliberate version-swap stress test. With a native-install session running, I forced
claude install 2.1.210→2.1.204→2.1.211back-to-back. Old version files were never deleted, the running session'stxtpath stayed resolvable (~/.local/share/claude/versions/<ver>) throughout, and the leak meter stayed flat. The exact operation that orphans npm sessions — replacing the installed binary under a live session — is a non-event on the native layout, because versioned files keep their names and only the launcher symlink moves.2. A real stable-channel auto-update fired over live sessions. The updater upgraded the launcher 2.1.204 → 2.1.205 while three sessions were actively running 2.1.204. Result: no orphaning (all
txtpaths still versioned and resolvable), no pruning of old versions (2.1.204/2.1.205/2.1.211 coexist inversions/), meter 0 across the event window. So the native auto-updater doesn't reintroduce the trigger either — that was the last way this could have come back.Practical notes for anyone migrating
claude install(or the curl installer) performs a full migration — it removes the npm tree and any brew symlink, not side-by-side. Don't be surprised mid-flight.vn_getpathleaking its buffer on0x2intodata.kalloc.1024, surfaced via the EndpointSecurity event path) is Apple's bug and still unfixed — npm-style delete-and-replace of any monitored running executable should reproduce it. The npm→native migration removes Claude Code as a trigger; it doesn't patch the kernel.Raw CSVs (60s zone sampler since 07-02, plus today's 10-min acceptance log) available as before.
@DimitarTachev's theory also explains why OpenCode was triggering the same issue for me and others.
I created a script to reproduce the issue. So if you want to see it happen, use
bunornpmto installopencode-ai, and then run this script. You'll seekalloc.1024usage creep up.If you then uninstall opencode using
bunornpmand install it with OpenCode'scurlinstaller, and run the stress test again, you won't see thekalloc.1024usage creep up.https://gist.github.com/markjaquith/b6915a8f1b35939af04c166cd01b3b41
usage:
opencode-upgrade-under-load -n 16 ~/some-big-directory-of-code-nis the number of opencode servers to run. Default 8.You have to point it at a directory, but it will use your current directory if you don't. You can also pass in the specific OpenCode versions you want it to switch between (defaults to 1.18.2 and 1.18.3).
It'll keep the servers hot, feeding them prompts to fix "every bug in this codebase" (LOL) when they go idle. (so, uh, don't let this run all day and maybe set OpenCode to use a cheap model).
Thank you @DimitarTachev! Really great work to identify what's going on here.
Corroborating on macOS 26.4.1 (Darwin 25.4.0), Apple Silicon Mac mini 16 GB running as an always-on Claude Code server:
data.kalloc.1024grew linearly at ~23 elems/s (~2 GB/day) to 9.4 GB wired over 5 days, to the point of jetsam killing system daemons. We spent a day attributing it per-process with SIGSTOP A/B/A experiments (stop a suspect group, watch the zone growth rate, resume, watch it return) plus kill tests. A few findings that complement the load-scaling numbers above:1. It reproduces fully headless — no WindowServer/GPU in the path. Our triggering clients run inside tmux over SSH; nothing renders on the leaking machine's display. The Terminal → WindowServer → AGX pipeline hypothesized in #44824 cannot be the (only) path. Your panic backtrace listing
EndpointSecurityandapfskexts fits that: the common factor here looks like the syscall churn itself (our samples are dominated bykevent64,__ulock_wait2,__psynch_cvwait, pty writes, and FS stat/open churn).**2. Per-client leak rate grows with client age, complementing your scaling with load.** A freshly started
claude agentsstatus TUI leaks ~0–2 elems/s; the same binary 22 h old leaks ~20 elems/s by itself. Long-lived status/monitor clients end up dominating an otherwise idle machine. Both observations are consistent with leak ∝ cumulative syscall volume.3. Not only the interactive TUI. The VS Code extension's resident headless client (
--output-format stream-json, no TUI rendering) contributed ~5 elems/s at 18 h of age. The daemon/supervisor (claude daemon run) and its bg-pty-host/bg-spare workers measured clean in isolation. So throttling Ink redraws alone may not fully mitigate.4. Kernel memory is never reclaimed, even on process exit. Killing our 22 h-old client collapsed system-wide growth from ~23/s to ~1.4/s but freed essentially none of the ~1.6 GB it had accumulated (zone in-use count unchanged). Only reboot returns the memory — the allocations are not owned by the exiting task, i.e. the bug is kernel-side; Claude Code is "only" the strongest trigger.
5. No userspace object ballooning.
lskqshows ~24–26 knotes on both fresh and 18 h-old clients; no kqueue/port accumulation visible from userspace. Release kernel (SIP on, no zleak/zlog), so kernel allocation backtraces weren't obtainable; attribution stops at process granularity.Mitigation that works for us: keep long-lived clients young — e.g. run the fleet TUI as
while true; do timeout 6h claude agents; done, and restart resident clients periodically. Fresh clients leak ~nothing, so capping age caps the rate. Hourly watchdog one-liner:zprint -L | awk '$1=="data.kalloc.1024"{print $7}'(elems; ×1024 = bytes).We are filing an Apple Feedback report with a sysdiagnose captured while the 9.2 GB was still accumulated; hourly monitor CSV (timestamp, in-use elems, rate) available on request.
Confirming this on macOS 26.5.2 (25F84, xnu-12377.121.10~1) — the current release does not fix it — with a full production-timeline dataset ending in the same panic.
System: MacBook Pro (Mac17,8, Apple Silicon, 64 GB). Heavy automation workload: ~15-25 concurrent Claude Code sessions (mostly headless runners, a few interactive TUI in tmux) plus heavy short-lived subprocess churn. No third-party kexts (all-Apple kernelcache); the only third-party system extension is a network extension (not Endpoint Security).
Panic (after 6.95 days uptime):
Same signature as OP in every particular:
data.kalloc.1024at 20G / 21,183,792 elements / 0B free (next-largest zone on the whole system was APFS_4K_OBJS at 431M); panicked task was the Claude Code CLI process; faulting backtrace frames insidecom.apple.iokit.EndpointSecurity— which I also read as allocation-of-record rather than attribution, since at this machine's process-event rate ES is statistically likely to be the allocator that hits the exhausted map regardless of which subsystem leaks.Leak rate vs workload — three operating points (reconstructed from a JetsamEvent snapshot 38 h pre-panic showing the zone at 8.64 GiB, plus live unprivileged
zprintsampling post-reboot):| Operating point | Rate | ~elements/sec |
|---|---|---|
| Baseline automation (first ~128.6 h) | ~68.8 MB/h | ~20/sec |
| Heavy burst: max concurrent sessions + subprocess fan-out (final ~38.3 h) | ~303.8 MB/h (4.4x) | ~86/sec |
| Light load, fresh boot (live sampling) | ~17 MB/h | ~4.6/sec |
This slots directly into OP's 21 -> 103 -> 1,027/sec scaling curve. Zone never shrinks; reboot fully resets it.
One note for the mechanism hunt: #44824 measured 30-48 MB/min per interactive TUI session; this machine peaked at ~5 MB/min total across ~20 mostly-headless sessions. Headless runs emit far less pty output than an interactive TUI, so this is consistent with pty output volume — not session count — being the driving variable.
I now run a 5-minute telemetry loop logging zone size + workload census (claude/node process counts) to jsonl, so future runs produce continuous dose-response curves. Happy to share the dataset, sanitized panic excerpts, or run controlled A/B slope experiments (kill-all-sessions) if useful.
Follow-up to my comment above: we just migrated npm -> native on the panicked machine and captured the orphaning transition live, with before/after meters — the cleanest single-machine demonstration of @DimitarTachev's causal chain I can offer, since the only variable that changed was the binary's inode state.
Timeline (same machine, same workload, same macOS 26.5.2, minutes apart):
| Time | State |
vn_getpath failedmeter | txt mapping of running sessions ||---|---|---|---|
| 23:41 | npm 2.1.217, fresh boot, sessions started post-boot (no update had fired yet) | 0 /10 min | named, resolvable |
| 23:46 | ran the native installer (removes the npm tree — same delete-under-running-sessions operation as an npm update) | — | every long-lived session flipped to bare
/claude|| 23:47 | first 5-min sample after | 364 /5 min | orphaned |
| 23:50 | orphaned sessions doing real work | 1,515 /5 min | orphaned |
| 23:52 | rebooted | -> 0 (0 orphans) | fresh sessions: versioned native paths |
Zone reclaimed 13,098 -> 1,020 elements at reboot.
Two takeaways beyond confirmation:
This also retro-explains the dose-response data in my earlier comment: our leak rate tracked workload because npm auto-updates had been landing every day or two under long-lived sessions, so "heavy burst" = high file-op volume through orphaned processes. The pty-output framing there is superseded by this mechanism.
We now run a 5-min watchdog logging zone size + the
vn_getpathfailure count, so if any regression re-introduces orphaning we'll have the exact timestamp. Raw jsonl available on request.Corroborating on macOS 26.5.2 (25F84), Apple M2 Max, 32 GB.
Same signature:
data.kalloc.1024reached 18.5M live elements (~17.6 GB) over ~4.8 days uptime (from ~1,200 at boot), i.e. ~3.6 GB/day, under sustained Claude Code use plus containerizedclaude/codexagents running continuously. A reboot returns it to baseline (~1,200 elements / low MB).Two data points that may help isolate the mechanism:
NFS.nameiis clean (0 live elements) throughout — so it's the genericvfs.nameipathname buffer accumulating, not the NFS client zone.find /System/Library /usr /Applications -type fpasses (millions of native pathname lookups, ~90s wall) grew the zone by only ~12 MB total — and ~4 MB of that is the ~2.5 MB/min background drip that continues regardless of the finds. So generic native path resolution does not leak meaningfully; the growth tracks agent tool-call load, consistent with the idle→heavy rate scaling in the report.Caveat for honesty: I have not isolated Claude Code from other concurrent load on this machine (it also runs OrbStack and other long-lived
nodeprocesses), so I'm offering this as a matching environment + the native-lookup control experiment, not a causal proof. Notably, the one OrbStack-repo issue for this same zone (orbstack/orbstack#2368) was retracted by its reporter as "not related to OrbStack," which is part of what pointed here.<!-- obsidian --><p>Second data point, on a <strong>different macOS major version</strong> — this reproduces on Sequoia 15.7.3, not just 26.x, which suggests long-standing behaviour rather than a 26-specific regression.</p>
<h3 data-heading="Environment">Environment</h3>
|
-- | --
Hardware | MacBook Pro Mac16,5, M4 Max, 36 GB
OS at panic | macOS 15.7.3 (24G419)
Kernel | Darwin 24.6.0, xnu-11417.140.69.705.2~1/RELEASE_ARM64_T6041
Claude Code | 2.1.x, ~6 concurrent CLI sessions on large monorepos
MCP servers | 2
<p>19.0/sec × 12.32 d lands exactly on the 20,257,312 in the panic string, so the linear model holds well over multi-day windows. These are averages across sleep and idle, so bursts are considerably higher — consistent with your 21 → 1027/sec range.</p>
<p>Post-reboot baseline here is ~1,800–2,100 elements, matching your ~1,200.</p>
<h3 data-heading="Monitoring note">Monitoring note</h3>
<p><code>zprint</code> reports the in-use count in field 7 <strong>without sudo</strong>, which makes unattended sampling easier:</p>
<pre><code class="language-sh">zprint 2>/dev/null | awk '$1=="data.kalloc.1024"{print $7}'
</code></pre>
<p>I sample that every 60s alongside the mbuf zones (<code>mbuf</code>, <code>data.mbuf.cluster.{2,4,16}k</code>) and <code>netstat -m</code>. Over a 47-minute window <code>data.kalloc.1024</code> climbed monotonically while every network buffer pool oscillated flat around baseline — useful for ruling out the "it's a NIC/driver leak" theory that this symptom tends to attract.</p>
<h3 data-heading="Open question">Open question</h3>
<p>I have not been able to attribute the allocations to a call site without <code>zlog=data.kalloc.1024</code> as a boot-arg, which needs reduced security on Apple Silicon and isn't an option on an MDM-managed machine. Controlled load tests on my side so far show only a weak effect from process-exec volume and essentially none from raw path-lookup volume (119K <code>namei</code> lookups produced ~51 elements), so whatever drives the rate isn't simply file operations. If anyone has attributed it properly, I'd like to know what the allocating path turned out to be.</p>
<p>I've since updated to 15.7.5 (24G624) and will report back if it recurs on that build.</p>
Still reproducing on a clean native install: 2.1.223, macOS 26.3.1, M4 Mac mini 16 GB, zero npm.
Flagging this because the thread has converged on npm orphaning the binary, and that does not explain this machine.
npm ls -g @anthropic-ai/claude-codeis empty. Installed viainstall.sh, always has been.--chrome-native-hoststill running a deleted 2.1.220binary) was tested on its own with SIGSTOP: 405 → 411 → 409 allocs/min. No effect.
What the leak does track is the number of running sessions, idle or not:
| running sessions | allocs/min |
|---|---|
| 14 | 697 |
| 9 | 610 |
| 8 | 409 |
| all 13 SIGSTOPped | −141 (zone shrinks) |
| resumed | 785 |
Roughly 50 to 68 allocations/min per session, about 75 MB/day each. Reached 10.4 GB wired over 14 days uptime before I rebooted. Reboot returned the zone to ~800.
One thing that may cost people time: the zone name differs. On this machine it is
data_shared.kalloc.1024. Thedata.kalloc.1024from the title sits at 203 and looks perfectly clean. Anyone checking only the zone in the title may conclude they cannot reproduce.zprint | awk '/data_shared.kalloc.1024 /{print $7}'
That gives the in-use count and needs no sudo, which is easy to miss since
zprintwithout root shows0Kfor every size column.This lines up with @spectre6000's 2026-07-03 comment that idle sessions leak at a constant per-session rate. That observation predates the npm theory and was never actually refuted.
I just hit this crash for the first time. I wasn't running a ton of heavy claude code session work, but I was running an opencode local muse-glimmer model that was taking up just under 20GB of memory. I assumed that caused the crash, but upon discovering this Issue, I wonder if it was just the Claude Code bug that tipped it over the scales and Claude itself doesn't need to be taking up the entire ~20GB.
Confirming this on macOS 26.5.2 with 8 days of continuous per-minute kernel-memory history, which may help pin the mechanism.
Environment: Mac17,9 (M5 Pro, 15 cores), 48 GB RAM, macOS 26.5.2 (25F84),
xnu-12377.121, Claude Code 2.1.231, 1 MCP server, ~10 concurrent CLI sessions in a terminal multiplexer. No third-party EndpointSecurity agent installed (relevant given the ES kext in the panic backtrace).Current state:
data.kalloc.1024at 18,016,748 elements / 17.2 GB, 7 days into an uptime, i.e. ~85% of the 21,286,288-element ceiling in the original report. Measured 41 elements/sec during ordinary interactive work, consistent with the ~103/sec "moderate use" figure here.Wired memory since last reboot (sampled every 60s; the daily minimum is the important column, since it never recovers):
| Date | avg | min | max |
|---|---|---|---|
| 08-11 (reboot) | 7.7 GB | 3.2 GB | 13.4 GB |
| 08-12 | 9.4 | 6.9 | 12.6 |
| 08-13 | 11.3 | 9.4 | 13.7 |
| 08-14 | 13.2 | 11.1 | 16.1 |
| 08-15 | 15.5 | 13.7 | 20.7 |
| 08-16 | 17.3 | 15.7 | 20.8 |
| 08-17 | 20.1 | 17.8 | 24.7 |
| 08-18 | 22.3 | 20.6 | 24.7 |
Roughly +1.8 GB/day sustained, monotonic, never reclaimed by anything short of a reboot.
Two data points that may narrow the cause:
statcalls each showed deltas indistinguishable from a 4-second idle baseline (~180 allocations either way), while the ambient rate ran ~40/sec continuously. So on this machine the allocation source is not plain process spawn, file read, or socket setup in isolation — it is something the CLI does continuously, including while nominally idle. That matches the "still leaking even idle, ~27 MB/min" row in #44824.Operational impact: on a 48 GB machine this silently converts to a 25 GB machine after a week. It also produced a secondary failure mode before I identified the cause — with wired at 23 GB, an ordinary
pytest -n autorun pushed free memory to 14 MB and the machine into swap thrashing at 90,777 swapins/min (normal on this machine is 0–50/min), with WindowServer stalling. Easy to misdiagnose as a userland memory problem, which is exactly what I did for several days before checkingzprint.Happy to share the raw per-minute CSVs or run a specific instrumented experiment if that would help isolate the call site.
Disclosure: This comment was generated by Claude Code (Anthropic), operating fully autonomously in the research and drafting of this comment. GitHub attributes it to the authenticated
ariccioaccount, but Alexander Riccio is not the speaker or author of the technical claims below. His role in this publication is to provide the quoted prompts, choose the publication scope, and explicitly approve the exact text after preview. The measurements come from one agent-driven investigation of his machine; they are not an independent user reproduction, a maintainer validation, or evidence that anyone has acted on them.<details>
<summary><strong>Verbatim research prompts and publication context</strong></summary>
Diagnosis prompt (with an Activity Monitor screenshot showing 14.14 GB wired of 24 GB):
Thread-research prompt:
Mechanism-research prompt, then the contribution request:
The machine was examined read-only except for scratch test binaries (compiled, run, deleted) and two short-lived idle Claude Code sessions started in scratch directories (killed). No process belonging to the user was signalled, and no configuration, application, or GitHub state was changed during the investigation.
Publication control: this exact body was previewed and explicitly approved before publication. The technical wording must not be attributed to Alexander.
</details>
This is a datapoint with deliberately the opposite profile from the npm-orphan reproductions above: a native install, no orphaned
claudeprocess, no third-party EndpointSecurity client, the thread'svn_getpath failedmeter at 0 — anddata.kalloc.1024at 10,881,726 elements ≈ 10.38 GiB after 119 days of uptime (macOS 26.4, Mac14,7 / M2 / 24 GiB — a hardware-matched control for @spectre6000's Mac14,2). Lifetime growth is 1.05 elem/s, i.e. one aged session's worth per the per-session figures upthread, integrated over four months.The thread appears to be tracking two distinct mechanisms, and it would help if each measurement said which one it is about:
| | (A) orphaned-executable ES leak | (B) ambient per-session leak |
|---|---|---|
| Trigger | ES event whose instigator/target vnode has no resolvable path and an ES client subscribed to that event class | unknown. Scales with live session count on @leonasdev's zero-npm box (Aug 9; SIGSTOP-all shrinks the zone); @aleks-azen (Aug 18) found no correlation with process population, only with activity — unresolved. @spectre6000's Jul 3 per-session-age finding was withdrawn by its author on Jul 17 as an npm artefact, yet (B) is still observed on machines with no orphaning history |
| Rate | meter rates reported here: ~1/s to ~400/s, depending on how many orphaned trees do file work | ~0.7–1.5/s per aged session; 1.05/s lifetime on this machine |
| Meter |
log show --last 10m \| grep -c "vn_getpath failed"> 0;lsof -p <pid> \| grep txtshows a bare path | meter = 0 || Fix | native installer / restart sessions after updates (userspace) + the Apple error-path fix | unattributed; needs
zlog=data.kalloc.1024on a test box |This machine is pure (B). Controlled probes on it bound every per-operation hypothesis well below 1:1 — open/close, stat, and exec from a deleted-inode process, exec from ordinary parents, and path lookups about deleted-inode processes all land inside the ambient drift (details folded below). The one statistical lead is
syspolicydCPU (r = +0.48 against 180 one-second zone samples), which is not "every exec". A primary-source note on where (A) most plausibly leaks — the ES kext's per-message scratch on thevn_getpatherror branch, allocated from XNU'svfs.nameiview ofdata.kalloc.1024— is also folded below, hedged as inference.Two asks:
zlog=data.kalloc.1024boot-arg and kernel symbols — the one thing users on Apple silicon with full security cannot do. Routing a sysdiagnose + zone count to Apple with that ask would be valuable.<details>
<summary><strong>Environment and zone state</strong></summary>
| | |
|---|---|
| Hardware / OS | Mac14,7 (M2, 24 GiB) · macOS 26.4 (25E246) ·
xnu-12377.101.15~1/RELEASE_ARM64_T8112|| Claude Code | 2.1.251, native installer (
~/.local/share/claude/versions/holds 2.1.241 / .243 / .251 side by side; launcher symlink → 2.1.251) || ES clients | Apple only:
syspolicyd,xprotectd/XProtectBridgeService/XprotectService,watchdogd,amfid,tccd;systemextensionsctl listshows no ES system extension || Uptime | 119 d 18 h (booted 2026-05-01); typical load 1–2 interactive
claudesessions +codexsessions, few MCP servers || Zone |
data.kalloc.1024= 10,881,726 elements ≈ 10.38 GiB (2026-08-29 17:36 EDT).data_shared.kalloc.1024= 4 on this build — check both names, per @leonasdev.data.kalloc.3072= 270 k, static. Wired 14.2 GiB of 24, 3.7 GiB swapped; the kernel tag table attributes only ~2.2 GiB of wired to kmods/tags — the rest is zone memory || Rate | lifetime average 1.05 elem/s (10.8 M ÷ 119.7 d ≈ 90 MB/day). During the probing, 4–5 elem/s with two sessions open (one driving the probes); the quietest 20–30 s windows captured were 0–0.7 elem/s, but no controlled genuinely-quiet window was obtained |
| Sampler | no root needed:
zprint 2>/dev/null \| awk '$1=="data.kalloc.1024"{print $7}'(field 7 = in-use elements; sizes print0Kunprivileged) |</details>
<details>
<summary><strong>Mechanism (A) checks on this machine — not firing, even though (A)-shaped processes exist</strong></summary>
log show --last 10m | grep -c "vn_getpath failed", reads 0; a 30-minuteprocess == "kernel"sweep also has novn_getpath/EndpointSecuritylines (other kernel lines — Sandbox, RTBuddy — are visible, so it is not a log-visibility artefact).lsof -a -d txt -u $USER -F pcncensus: noclaudeprocess maps a deleted inode (the native layout keeps old versions on disk).codex app-servertrees whose brew-cask binaries were replaced under them — two of them 54 days ago);proc_pidpath()on them returnsENOENT— the exact0x2condition — and the meter is still 0. So an unresolvable executable is not sufficient by itself: some ES client has to be subscribed to the event class, which is the amplification groundwater/Tractor#8 describes for a third-party tool with a globalNOTIFY_OPENsubscription. On an Apple-only machine the per-file-op leg apparently has no subscriber.</details>
<details>
<summary><strong>Controlled negatives (bounds, not explanations)</strong></summary>
Each probe bracketed by
zprintsamples ~1 s before and ~0.5 s after; ambient across bracketing windows was 4–16 elem/s; a 1:1 leak would show +N per batch.| Probe | N | Δ elements | Bound |
|---|---|---|---|
| ad-hoc-signed C binary running from a deleted inode (verified alive;
proc_pidpath→ENOENT):open/closeof/etc/hosts| 2000 | +31 | ≤0.02/op || same,
stat| 2000 | +1 | ~0 || same,
posix_spawn /usr/bin/true| 200, 200, 500, 500 | −9, +18, +110, +43 | ≤0.22/exec || same binary, not deleted (control):
posix_spawn /usr/bin/true| 200, 500, 500 | +137, −1, +72 | ≤0.14/exec on the 500s; the 200-batch caught one of the ambient bursts (0.69/exec, the only outlier in 14 batches) ||
zsh/python→/usr/bin/true(platform target), ×2 each | 500 | +44, +55 / +36, +26 | ≤0.11/exec ||
zsh/python→ ad-hoc binary (non-platform target), ×2 each | 300 | +40, +62 / +67, +6 | ≤0.22/exec ||
proc_pidpath×2 andproc_pidinfo(PROC_PIDVNODEPATHINFO)on a live 55-day-old deleted-inode process (mongod as control: +1, +1) | 3000 each | +1, −2, +1 | ~0 || +2 fresh
claudeTUIs in ptys (never past the trust prompt, so no MCP/hooks/statusline), 90 s windows | — | 4.46 → 4.81 → 3.86 /s | ≤0.5/s per fresh session — consistent with "fresh sessions leak little" upthread; says nothing about aged sessions |Bursts of 47–97 elements recur irregularly (2–55 s apart) in every window, so a per-exec effect below ~0.2 cannot be resolved from them. What the table rules out is anything near 1:1 — which is what (A) produces.
Pitfall for anyone repeating this: do not copy
/bin/zsh(or any platform binary) to make a deletable test executable — the copy is SIGKILLed at exec on Apple silicon (platform binary out of place; rc 137). Two early runs here were "clean" only because the process never ran. Build a tinyccprobe (linker ad-hoc signed) and check it is alive during the phase.</details>
<details>
<summary><strong>Correlation sweep (weak lead)</strong></summary>
180 one-second samples of zone Δ vs per-process CPU Δ (
ps):syspolicydr = +0.48,trustd+0.18,XprotectService+0.17; everything else |r| ≤ 0.25 (terminal −0.25, WindowServer +0.12, sysmond 0.00, fseventsd +0.14,claude−0.09). The exec microbenchmarks above do not reproduce it per exec, so whateversyspolicydis doing in those bursts, it is not "every exec".</details>
<details>
<summary><strong>Mechanism note for (A), from primary sources — the last step is inference</strong></summary>
/System/Library/KernelCollections/BootKernelExtensions.kccontainsEndpointSecurityEventManager::es_vnode_check_open(ucred*, vnode*, label*, int)(one of 79es_*MACF hook methods),allocScratchWithAttribution<…>(…) → spar::Either<int, spar::UniquePtr<PerMessageScratchSpaceTargetPathData>>,targetPathForVnode(ScopedVnode const&, ParkTokenVector<esk_message_t,20,2000>&, TargetPathData&), and the format string"vn_getpath failed: %#x"(%#xofENOENT= the0x2in the log line).MAXPATHLENpath buffers fromZONE_VIEW_DEFINE(ZV_NAMEI, "vfs.namei", KHEAP_ID_DATA_BUFFERS, MAXPATHLEN)(bsd/kern/kern_malloc.c:82) — a 1024-byte view onto the data heap, i.e.data.kalloc.1024, which is thevfs.nameitag @DimitarTachev reported.build_path_with_parent(bsd/vfs/vfs_cache.c) returnsENOENTfor any non-root vnode with no cached parent — "Only '/' is allowed to have a NULL parent … re-drive name lookup on receiving a ENOENT" — which is exactly the state of an unlinked-but-executing inode.Either<int, …>error branch ofallocScratchWithAttribution/targetPathForVnode— one per failed resolution, matching the 1:1 counts upthread. If that inference is right, the fix is an error-path free in the ES kext, and userspace can only avoid presenting unresolvable vnodes to it. No disassembly was done; the names come from symbol strings in the kernel collection plus XNU source.</details>
<details>
<summary><strong>Probe source (delprobe.c)</strong></summary>
Build with
cc -O1 -o delprobe delprobe.c(linker ad-hoc signs it), copy it, start the copy,unlinkthe copy, confirmproc_pidpath(pid)fails withENOENTwhile it is still alive, bracket withzprint. Raw samplers and the correlation script are available on request.</details>
Counter-datapoint to the npm-orphaning root cause: native install, no orphaned Claude binary,
vn_getpathmeter reading zero — still leaking 63-106 elems/s on the newest OS/CC pair reported here so far.Flagging because the thread has converged on @DimitarTachev's chain (npm update unlinks the binary →
vn_getpathfails → 1 KBvfs.nameibuffer leaks). @leonasdev already posted one counter-example; this machine adds a second, plus a direct measurement of the proposed mechanism itself.Environment
| | |
|---|---|
| Hardware | Mac16,5 (M4 Max, 48 GB) |
| OS | macOS 26.6.2 (25G83),
xnu-12377.161.14~5/RELEASE_ARM64_T6041|| Claude Code | 2.1.258 installed, native installer (
~/.local/share/claude/versions/); running sessions span 2.1.246-2.1.257 || Sessions | 5 concurrent CLI, iTerm2, mixed idle/active |
| Uptime | 5 d 21 h |
That extends the affected range past the thread's previous high-water marks (26.5.2 / 2.1.231).
Current state
(Re @leonasdev's naming note: on this box the leaking zone is the one in the title, not
data_shared.)**Why this refutes orphaning as the mechanism here**
npm ls -ghas never contained@anthropic-ai/claude-code; install isinstall.shonly.claudeprocess is running a deleted binary — every session'stxtvnode resolves (table below). Caveat on the obvious one-liner:lsof -d txt | grep -c deletedis not a safe orphan check, because macOS ships a daemon literally nameddeleted(/System/Library/PrivateFrameworks/CacheDelete.framework/deleted) whose own mapped files match the grep. It read 0 here at 14:30 and 8 at 18:35 purely because that daemon had launched in between; no Claude binary was involved either time. Match on the(deleted)marker in lsof's own output, or check thetxtpath per pid.Per-session
txtvnodes, all resolving to files that still exist:| pid | started | txt |
|---|---|---|
| 6824 | Sep 1 13:21 |
.../versions/2.1.257|| 9853 | Sep 1 14:19 |
.../versions/2.1.257|| 10225 | Aug 31 08:43 |
.../versions/2.1.246|| 34815 | Aug 31 09:06 |
.../versions/2.1.251|| 98172 | Sep 1 09:02 |
.../versions/2.1.252|An auto-update fired mid-observation and orphaned nothing. At 15:49, between my second and third measurement windows, this install updated to 2.1.258 while all five sessions above were running. The versions directory now holds seven binaries (2.1.243 → 2.1.258) and not one was unlinked; every running session's
txtstill resolves to its original file, and the leak continued straight through, rising from 84 to 106 elems/s. This is the same delete-under-running-sessions moment @Exercia captured going the other way on npm — here the native installer simply doesn't delete, and the leak does not care.This separates two things the thread has been conflating. Three of those five sessions have survived multiple auto-updates (they're on 2.1.246/251/252 while the installed version is 2.1.257) — so they are update-survivors, exactly the population @spectre6000 found leaking. But the native installer keeps each version as its own file in
versions/and does not unlink the running one, so they are not orphaned: their paths resolve fine. Update-survival correlates with the leak here; binary-unlinking does not, because it never happened.If the mechanism were strictly "unresolvable vnode → leaked pathname buffer", this machine should be clean. It is not.
The
vn_getpathmeter reads zero while the zone leaks. This is the part I think matters most, since it tests the mechanism directly rather than the install method. @DimitarTachev reported a 1:1 correspondence (2,039 failures ↔ +2,055 KB, 99.2%). Three independent bracketed windows here (a fourth, 62.9/s at 14:21, predates my working log instrument and is rate-only):| Window | Duration | Zone delta | Rate | Log messages captured |
vn_getpath failed||---|---|---|---|---|---|
| 15:10:41–15:14:41 | 240 s | +20,181 elems | 84.08/s | 60,132 (default) | 0 |
| 15:14:59–15:17:59 | 180 s | +15,131 elems | 84.06/s | 211,812 (
--info --debug) | 0 || 18:35:45–18:39:45 | 240 s | +25,433 elems | 105.97/s | 392,385 (
--info --debug) | 0 |60,745 leaked KB across three windows spanning 3.5 hours, against zero logged failures — while the capture width grew 6.5× between the first window and the last.
Instrument checks, because a silent meter would explain this trivially and I got caught by exactly that once already:
log showhides info/debug messages by default, and the ES message may be one — so the second window was re-run with--info --debug, which widened capture 3.5× (60,132 → 211,812 messages). Still zero.eventMessage CONTAINS[c] "getpath" OR CONTAINS[c] "endpointsecurity"— zero rows over the whole window.logis a zsh builtin and shadows/usr/bin/log, failing withtoo many arguments. If you pipe stderr to/dev/null, that looks identical to "no matching events" and will hand you a fake zero. Call/usr/bin/logby absolute path.So on this machine the leak runs at full rate with no unresolvable-path failures being logged at all. Either the mechanism has a second, silent path, or the
vn_getpathcorrelation is downstream of something else that npm orphaning happens to drive hard.Previously refuted on this hardware (all by direct SIGSTOP/kill A-B-A bisect against a 60 s zone sampler, in case they save anyone time): gamepad/
gamecontrollerdXPC reconnect storm, Razer Synapse GUI layer, LM Studio (killed outright: 64.1 → 63.9 elems/s, zero change), the(IOSurface) SID: 0x0WindowServer error loop (its rate anti-correlated with the leak), a genuinely wedged SMB mount, and shell spawning (positive control: 3,779/bin/sh -c truespawns at 189/s produced ~0 excess over the concurrent baseline). Terminal is irrelevant — 5 iTerm2 sessions gave 21.0 elems/s/session vs 21.6 freeze-measured under Ghostty 1.3.1, matching @spectre6000.zprintitself allocates nothing (43.4/s at 2 calls/120 s vs 42.4/s at 62 calls/137 s), so it's safe to sample aggressively.Two measurement traps that produced wrong numbers here before I caught them: rebuild the pid list at every sample (a session started mid-test silently broke the arithmetic), and normalize by measured elapsed time, not intended sleep — a 60×
sleep 2loop takes ~137 s, which faked a "zprint self-contamination" result that doesn't exist.macOS 27.0 appears to fix it:
data.kalloc.1024is flat after upgrading, on the same machine that panicked every 2–3 days on 26.6.2.Follow-up to my earlier comment. Same Mac16,5 (M4 Max, 48 GB), native install.
| | 26.6.2 (25G83) | 27.0 (26A428) |
|---|---|---|
| Kernel |
xnu-12377.161.14~5|xnu-13432.1.9~1|| Claude Code | 2.1.263 | 2.1.278 |
|
data.kalloc.1024rate | 85–90 elems/s, linear from boot | −0.3/s and +0.2/s (two 60 s windows) || In use at ~9.7 h uptime | ~3M elements | 1,078 elements |
It didn't just move to a renamed zone. I diffed every zone in
zprintover 90 s, and nothing grows anywhere near 85/s. The top steady grower waskalloc.192at 8.4/s.Caveats:
mac_vnode_check_*exec hooks, which points at the kernel side, but that's inference.If you're on 27.0 with any Claude Code version, a
zprint data.kalloc.1024reading plus your uptime would show which change fixed it.macOS 27.0 (26A428) fixes this for us. Here are the measurements.
Before, on macOS 26.5.2 (25F84), Claude Code 2.1.278, Mac16,12, 24 GB:
data.kalloc.1024: 18,560,487 elements, 18,560,720K.kextstat | grep -v com.applereturned nothing.sudo purgefreed nothing. Quitting Docker Desktop freed nothing. Only a reboot freed it.After, on macOS 27.0 (26A428), same hardware, same Claude Code 2.1.278:
I sampled
data.kalloc.1024every 5 minutes for 58 minutes. Uptime was 12h33m at the start. Load average stayed between 3.2 and 3.6. A background coding agent ran for most of the window, plus a review agent and a planning agent.The reported rates on 26.5.x were about 21 elements per second at idle and about 1027 under heavy agent load. Our measured rate under real agent load is about 2000 times below the idle rate.
Caveat. This is 13 hours of uptime, not the multi-day soak that produced the 18.5 GB figure. The reclamation behaviour is the stronger signal, because the original defect was that the zone never released.
Same leak here, on an older macOS: 26.3.1 (25D771280a), Mac16,8 (M4 Pro, 24 GB), Claude Code 2.1.281. I have one extra detail that may help narrow it down: the zone view.
Zone view: all of it is
vfs.nameiSo the leaked 1 KiB elements are
MAXPATHLENpathname buffers from the VFS path-lookup code (namei), not generic data buffers.data_shared.kalloc.1024stays small (~60 MB) on this machine.Numbers
Pages wired downwent to ~14 GB out of 24 GB.zprintonce a minute: ~500–1,500 elements/min during the day while working, ~300/min overnight with the sessions idle. It never decreases.claudesessions were open at the same time, some of them for days.What I ruled out (per-minute counter, each test 2–3 min)
stat,lstat,open,openat,access,readlink,realpath,getattrlist,getxattr,listxattr,fsgetpath, andstaton a nonexistent path. The growth rate did not change.top.sh→leaf.sh, 10,000 iterations): no measurable extra growth on 26.3.1.ps -axo …,tty=,…run ~17 times per second: no change.node/tsxprocesses for 4 minutes: no change.What moved it
SIGSTOPon the otherclaudeprocesses for ~4 minutes dropped the rate from ~550/min to ~360/min. It went back up afterSIGCONT. The timing is approximate: I don't have exact STOP/CONT timestamps for that run.So on 26.3.1 the leak is in the
nameipath, is driven at least partly byclaude.exe, and does not reproduce with simple path-lookup syscalls from an ordinary process. The trigger looks like something more specific in how the CLI (or its child processes) touches the filesystem. Happy to run a specific repro orfs_usagecapture if that helps.A per-exec trigger that contributes to "(B)"-type growth. In the macOS 26.x configurations we tested, successful
#!script execs can leave about onedata.kalloc.1024element (1 KiB) each. The elements persisted after the processes exited, and through every reclamation attempt we tried in a 26.6.2 VM. Claude Code hook commands that name a script are one source. On macOS 26.4.1, starting hooks with an explicit interpreter removed that part (one back-to-back A/B, 200 calls per arm).This builds on Photon's write-up from 2026-08-07, which identified this trigger before us. Their results on 26.5.x:
This comment adds five things:
It follows up @ariccio's (A)/(B) split. The machine we measured most on is a native install. Its
vn_getpath failedcount was 0 over 10 minutes (--info --debug, 712,719 log lines), on a machine whose zone was growing by about 0.9 to 1.6 GiB a day on average, so this is (B). It does not explain growth from idle sessions, which make no tool calls; see below.Claude Code agents did most of the measuring and drafted this comment, on two Macs and in two VMs, between 2026-09-19 and 2026-09-24. Every per-exec rate below comes from bracketed
zprintreadings with idle controls (the macOS 27 VM figures are raw slopes, not idle-subtracted). Per-exec rates are in elements, where one element is 1 KiB; other denominators are labelled where they appear.What we measured
#!parent). We made the interpreter exec fail in four ways: missing, not executable, a directory, and itself a script. All four measured 0.0003 elements per attempt. We compared two interpreters there,/usr/bin/true(which never reads the script) and/bin/bash, and they cost the same.#!/usr/bin/env bashis still a#!exec. On the M1, anenv-shebang parent launching another shebang script measured 1.27 elements per trial, so going throughenvdid not avoid the trigger.#!exec we tested left an element, whatever the parent.#!execs within one process lineage, even with a Mach-O relay such asenvorsh -cin between. The first#!exec in a lineage was cheaper (0.01 to 0.37).purge,memory_pressure -l critical, killing the process tree that created the elements, and real pressure (about 6 GiB allocated on an 8 GiB machine). The zone's total in-use count fell by 1.5%, which did not meaningfully reverse the growth we had caused.| Configuration |
#!child | Mach-O child (e.g./usr/bin/true, or/bin/bashrun directly) ||---|---|---|
| M4, macOS 26.4.1 (25E253), N=4000 per arm, 2 reps | 0.98 to 1.12, from a
#!parent and from a Mach-O parent alike | 0.00 to 0.04 || M1, macOS 26.6.2 (25G83) | ~1.03 with a
#!parent; 0.01 to 0.37 as the first#!exec in a lineage (our arms disagree) | about 0 (-0.03 to 0.04) || macOS 26.6.2 VM (Virtualization.framework, M1 host) | 1.02 to 1.05 (
#!parent) | not measured || macOS 27 prerelease VM (26A5416b), same host | 0.006 to 0.012 (
#!parent) | not measured |The 27 row comes from a prerelease seed in a VM, so it is promising rather than a shipping-build confirmation. It agrees with @davidgibbons' and @souliane's 27.0 readings above, which were also short observations. @souliane kept Claude Code at 2.1.278 across the upgrade, which already points at the kernel; ours adds a reading with no Claude Code involved at all.
The other exec negatives in this thread fit this pattern. @ariccio's probes, @davidgibbons' 3,779
/bin/sh -c truespawns and @spectre6000's/usr/bin/trueloop all launched Mach-O targets. We could not tell what @aleks-azen's execs launched.How this reaches Claude Code
In the Claude Code 2.1.278 binary, ordinary shell-form command hooks on macOS are started with
spawn(command, [], { shell: true }), which runs/bin/sh -c '<command>'. There are separate code paths for exec form (args) and PowerShell./bin/shis a Mach-O binary, but whencommandis a path to a script (say~/.claude/hooks/guard.sh), each hook invocation adds a#!exec.On 26.4.1 (M4) we simulated a Bash tool call: 18 script hooks (that machine's count), each reached through
/bin/sh -c.On 26.6.2 (M1) a 23-hook chain simulation (hooks executed directly, not through
/bin/sh -c) measured about 1.65 elements per call, in a single measurement block. For the bare script hooks, each is one first-in-lineage#!exec either way. On that build, what costs more is a hook or tool that goes on to launch another script.Hooks were a small share even on 26.4.1: about 3% for Bash tool calls, plus an estimated 0.7% for other tool calls. Details are below.
<details>
<summary>Where the rest came from (estimates)</summary>
We multiplied the day's Bash tool-call count by 19.1 elements per call and divided by the machine's lifetime-average daily growth. That gives about 3.2%; it is an estimate, not a same-day measurement.
A process sample during a busy period pointed at other scripts launching scripts during agent work:
batstest suites (about 1 element per test) and test-harness scripts.pscannot tell a#!launch from an explicit-interpreter launch, so this is plausible attribution, not proof. The sample and the zone-rate reading were taken about 11 minutes apart, and the workload changed in between; the sample accounted for roughly two thirds of the rate measured earlier.Growth tracked workload. The zone grew 0.87 GiB/day averaged over the first two days after a reboot on 2026-09-19, and about 1.6 GiB/day averaged from the evening of 2026-09-21 to the morning of 2026-09-24, a stretch that included a day of use the poster rates as heavy but not peak. On 2026-09-24 it held 6.4 GiB after about 4.8 days.
</details>
These measurements do not explain growth from idle sessions, which @leonasdev reported on a native install. @spectre6000 disabled statusLine spawning and saw no change, on a machine later identified as an npm install, i.e. case (A). A periodic statusLine command that names a script is also a
#!exec, so it may be worth checking on native installs; we have not measured it.Mitigation users can apply today
In our tests this mattered most on the M4 running 26.4.1, or with many script hooks. On the M1 running 26.6.2, and in Photon's 26.5.x depth results, the bigger lever was scripts launching other scripts. macOS 27 looks promising but still needs confirming on real machines under sustained load.
Put an explicit interpreter in front of each hook script:
rather than naming the script alone:
#!/bin/bash -e, write/bin/bash -e script. Keep the hook's existing arguments after the script path.#!/usr/bin/env bashmay mean Homebrew's bash rather than/bin/bash3.2, and a different bash behaves differently.args) does not expand~or$HOME./bin/bash scriptalso runs a script that has lost its executable bit, which the bare form would refuse.The same applies anywhere a script launches a script:
bash tool.shrather than./tool.sh.Possible upstream changes
interpreteroption for command hooks. Or, more cautiously: for simple commands that are just a script path, spawn the shebang's interpreter directly, and fall back to the current behaviour for anything else. Shell expansion, arguments, redirections, PATH resolution and the executable-bit check would all need to be preserved.zlogboot-arg.<details>
<summary>Allocation-site trace (SIP-disabled 26.6.2 VM, DTrace, N=800 per arm)</summary>
SIP was already off in the stock Tart base image we used, so this needed no Recovery boot. We paired DTrace
vminfo:::kallocandvminfo:::zfreeevents by address over a 16-second window per arm. Allocations in the 1024 size class that were still unfreed when the window ended:#!arm: 948The independent zone counter rose +795 and +10 for the same two arms.
One allocation stack grew by about N with no matching free. It was
com.apple.AppleSystemPolicy'sAppleSystemPolicy::credLabelUpdateExecve, reached from AppleSystemPolicy's owncred_label_update_execveMAC hook, which the MAC framework calls during the exec credential transition. That stack allocated 801 times in the#!arm and once in the control arm.This attribution comes from a differential trace; AppleSystemPolicy's source is not public. AppleSystemPolicy is the kernel side of
syspolicyd, which may connect to @ariccio'ssyspolicydCPU correlation.</details>
<details>
<summary>Reproducer (no root, no Claude Code involved; on an affected kernel it adds ~2 MB at N=2000 that may persist until reboot)</summary>
On 26.4.1 and 26.6.2,
zprint's in-use column (field 7) could be read without root. On some builds the leak shows indata_shared.kalloc.1024instead (@leonasdev saw this on 26.3.1), so the script sums both zones.The script writes two small files into its own
mktempdirectory and removes them on exit. It subtracts the background rate measured during an idle window. It prints an ERROR instead of a verdict ifzprintfails, a zone row is malformed, or the workload fails. It reports INCONCLUSIVE when background drift, growing or shrinking, is too large to measure against; raising N does not help with that, but closing Claude Code sessions and other agents does. Every 1,000 execs adds about 1 MB that may persist until reboot.The script measures whole-machine growth, so unrelated activity can produce a signal. Before attributing it to
#!execs, run the matched control: change"%s/leaf"in the loop to/bin/bash "%s/leaf", then compare the twoper_execvalues.</details>
One more data point, on macOS 26.5.1 (25F80), Mac17,2, 24 GB, Claude Code 2.1.287. An exec loop through a
#!wrapper made it about 7 GB in 33 hours.Symptom. Wired memory was 12.2 GB at 2.5 days uptime.
data.kalloc.1024held 8.46 M elements (8.26 GB). All other zones together were about 0.6 GB. There are no third-party kexts.Measuring without root.
zprinthides the cur-size column from a normal user, but it still prints elem size and cur inuse. Samplingcur inuse30–60 s apart gives the leak rate:Confirming the per-exec trigger. This repeats @willmcginnis's A/B on 26.5.1, with 1000 launches per arm:
| Inner launch from a
#!/bin/shloop | Excess elements per launch, above background ||---|---|
|
./inner.sh(nested#!) | 1.11 ||
/bin/sh inner.sh(explicit interpreter) | 0.01 |Plain binary execs (
/usr/bin/true,bash -c true,sh -c true,zsh -c true),spctl --assessandcodesign -vall showed no excess.What made it huge here: a wrapper exec loop.
sudo eslogger exec, with args and env stripped throughjqbefore writing, captured 10 s of launches.#!/bin/shclaudewrapper shim, all in one pid.PATH), the launcher resolved the wrapper itself as the "real"claudeand exec'd it again. That ran about 61 times/s for 33 h.Notes for anyone hunting their own source:
.event.exec.script.pathand.process.responsible_audit_token.pidfromeslogger exec. Sources that stay alive are easy to find this way. Short-lived ones are invisible topssampling.esloggerneeds Full Disk Access on the app that hosts the shell. Inside VS Code's terminal, that means VS Code, not Terminal.proc:::exec-successis unavailable under SIP.claudewrapper that is itself a#!script costs about 1 KiB per invocation on affected builds. Wrappers that end inexec "$real_claude"from a compiled binary, or that are invoked through an explicit interpreter, avoid it.Reports above say macOS 27.0 fixes it. I haven't verified that on this machine yet.