Skip to content

[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 #66020

Open

Description

@LeifErikH

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • 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

  1. Environment: macOS 26.5.1 (25F80), Mac17,2, 32 GB RAM, Claude Code 2.1.168, Terminal.app, 1 MCP server (obsidian).

  2. Reboot Mac (clean zone: data.kalloc.1024 ~1,200 elements).

  3. Start 3–4 concurrent claude CLI sessions on a large monorepo (active agent work: Bash, git, ripgrep, file reads/writes).

  4. 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}'

  5. Observe monotonic zone growth; rate increases sharply under parallel agent + shell activity.

  6. 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:

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).

Activity

  1. github-actions commented on Jun 7, 2026

    @github-actions

    Found 1 possible duplicate issue:

    1. Claude Code CLI triggers macOS kernel memory leak (kalloc.1024) → kernel panic #44824

    This issue will be automatically closed as a duplicate in 3 days.

    • If your issue is a duplicate, please close it and 👍 the existing issue instead
    • To prevent auto-closure, add a comment or 👎 this comment

    🤖 Generated with Claude Code

  2. LeifErikH commented on Jun 7, 2026

    @LeifErikH
    Author

    regression between 2.1.143 and 2.1.158, kalloc.1024 leak on macOS, clean
    on 2.1.123.

  3. LeifErikH commented on Jun 7, 2026

    @LeifErikH
    Author

    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:

    1. Cumulative leak, hard ceiling. Zone count climbs from ~1,200 after reboot toward ~21M elements (~20 GB), then panics. At idle the measured rate is ~21 allocs/sec; under heavy parallel agent work it hits ~1,027/sec — time-to-panic drops from days/weeks to ~4 hours. Same habits, higher duty cycle or longer uptime between reboots, suddenly looks like "it started two weeks ago."

    2. More concurrent claude.exe than intended. The May 31 panic snapshot lists 13+ claude.exe processes (multi-GB RSS each, tens of millions of page faults) — not just the 3–4 sessions I deliberately run. Stale terminals, worktree agents, or forgotten sessions may compound kernel allocations even when my intended workflow is unchanged.

    3. Accumulated on-disk state. ~/.claude is ~25 GB on this machine (worktrees, session history, project caches). More files under agent-managed paths → more file/security events → more EndpointSecurity + APFS kernel traffic per agent turn. Spotlight Privacy on agent dirs reduced indexing pressure but did not stop active kalloc.1024 growth under load.

    4. Not Grok/MCP fan-out. Early suspicion was Grok session shims or MCP multiplication ([BUG] Unbounded MCP server fan-out across Cowork/agent sessions exhausts RAM and kernel-panics macOS (4 panics + forced power-off on M2 Max / 32 GB) #64366). This setup has 1 MCP server (obsidian) and panic logs consistently name claude.exe as the panicked task, with EndpointSecurity + APFS in the kernel backtrace — pointing at file/sandbox monitoring, not hundreds of node MCP children.

    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).

  4. LeifErikH commented on Jun 7, 2026

    @LeifErikH
    Author

    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):

    • 2.1.123–2.1.143 (May 1–18, incl. an 18-day continuous 2.1.123 session):
      zero panics
    • Converged to 2.1.144 on May 19; first panic May 31 on 2.1.154–158
    • → Regression between 2.1.143 and ~2.1.154. Current 2.1.168 still leaks
      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.

  5. huashiyiqike commented on Jun 12, 2026

    @huashiyiqike

    Confirming this reproduces with concurrent opencode + Claude Code sessions on macOS 26.5.1 (25F80).

    Setup:

    • Claude Code 2.1.153 + oh-my-claudecode (autopilot mode)
    • opencode 1.16.0 + oh-my-openagent (10 agents configured)
    • Both running inside zellij sessions

    Observed:

    • 8 kernel panics in 5 days (Jun 8-12), all identical signature:
      • zalloc[3]: zone map exhausted while allocating from zone [data.kalloc.1024] (20G, ~21M elements)
      • Panicked process: always bash (via AMFI/EndpointSecurity path)
    • Crash intervals: 9-23 hours, suggesting heavy parallel agent load accelerates the leak as noted in the report (21->1027/sec scaling).

    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.

  6. huashiyiqike commented on Jun 12, 2026

    @huashiyiqike

    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.

  7. sarimarton commented on Jun 15, 2026

    @sarimarton

    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.1024 leak 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

    • The leaked 1 KB objects are EndpointSecurity per-message scratch buffers allocated on the mac_vnode_check_open path — i.e. one allocation per file-open (vnode open) event.
    • The leak rate is directly proportional to vnode-open volume, not to GPU/TUI redraw.
    • Claude Code is the trigger because it drives a very high sustained vnode-open rate (tool calls spawning shells, file reads, MCP polling), which the kernel ES subsystem processes — and the scratch buffer is not reclaimed.

    Measurements (DTrace, SIP temporarily disabled for fbt probes)

    1. The hot allocation stack (clean 20s aggregate, kalloc_type_impl entry):

    EndpointSecurity`...PerMessageScratchSpaceTargetPathData::new_()
    EndpointSecurity`EndpointSecurityEventManager::es_vnode_check_open(ucred*, vnode*, label*, int)
    kernel`mac_vnode_check_open
    ...
    

    The dominant 1 KB allocations come from the ES vnode-check path allocating a PerMessageScratchSpaceTargetPathData scratch 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

    • Claude Code side: reducing sustained vnode-open volume would directly slow the leak — fewer shell spawns per tool call, reusing a long-lived shell, avoiding re-sourcing the full snapshot each call, batching file reads.
    • Apple side: this looks like an ES scratch-buffer not being reclaimed on the es_vnode_check_open path under high event volume (worth a Feedback Assistant report).

    Caveats / what I did NOT prove

    • I did not get a clean per-tool-call vnode-open attribution (the interactive vs non-interactive shell distinction matters and I ran out of clean measurement windows).
    • The exact ES client whose un-acknowledged message leaks the buffer (xprotectd vs watchdogd) is not isolated — they are SIP-protected and survive SIGKILL, so I couldn't A/B them.
    • "Idle Claude Code" alone (open but not working) did not drive the leak in my tests — it's the active file-I/O volume that matters.

    Environment: M1 MBA, macOS 26.5.1 (25F80), 16 GB. Happy to run targeted DTrace if a maintainer wants a specific probe.

  8. spectre6000 commented on Jul 1, 2026

    @spectre6000

    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

    • macOS 26.5.1 (25F80), Mac14,2 (Apple M2 MacBook Air), 24 GB RAM
    • Claude Code 2.1.197 (29 releases past the 2.1.168 in the OP — still present)
    • Terminal: Ghostty 1.3.1 (contains Mitchell Hashimoto's PageList fix, so this is not the Ghostty userspace leak)
    • 8 concurrent CLI sessions on a mid-size repo (git/file I/O/E2E), 0 MCP servers

    What's new vs the OP

    1. Reproduces in fullscreen / alt-screen mode. All 8 sessions confirmed on /tui fullscreen, yet data.kalloc.1024 still 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.)
    2. Terminal-independent. OP on Terminal.app, this on Ghostty — same zone, same behavior. Not terminal-specific.
    3. Still present on 2.1.197.

    Measurements (zprint)

    • data.kalloc.1024 = ~4.15M elems ≈ 3.96 GB, which is the bulk of kernel zone TOTAL 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.
    • Leak rate ~13 elem/sec (~800/min) with 8 sessions mostly parked in fullscreen; consistent with the OP's "~21/sec idle → ~1,027/sec heavy" load-scaling. Monotonic, never plateaus (a balanced alloc/free would plateau).
    • One controlled webkit E2E pass (spawns/kills many browser procs) did not grow wired (−0.13 GB) — confirming the leak is rendering/background-driven, not subprocess-spawn-driven.
    • Reboot-only reclaim confirmed: killing claude halts growth but does not return the zone; only reboot resets it (matches the kill experiment in Claude Code CLI triggers macOS kernel memory leak (kalloc.1024) → kernel panic #44824).

    Triage note
    #44824 — same data.kalloc.1024 signature — was auto-closed by a bot as a duplicate of #24960, but #24960 is a userspace/RSS process leak (closed not_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 provide zprint dumps or a panic log.

  9. spectre6000 commented on Jul 2, 2026

    @spectre6000

    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: zprint samples of data.kalloc.1024 in-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 -rl over /usr/share) zero (delta −22)
    296,281-file find traversal (stats/readdirs) zero (+11)
    2,000 process execs (/usr/bin/true loop) ~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:

    1. The per-vnode-open EndpointSecurity theory does not reproduce on this machine — 15k real opens leaked nothing. Whatever the DTrace stacks show on other machines, per-open leaking is not universal among affected hosts (may be config-dependent — different ES clients installed — or a second trigger).
    2. The terminal/rendering path is exonerated here — an aggressive synthetic TUI leaked nothing whether detached (no rendering) or fully rendered through WindowServer. "Only live TUI sessions leak" may be conflating TUI presence with the session actively working.
    3. "Claude Code doing real work" alone is not sufficient either — two fresh minimal sessions doing genuine streaming work were completely clean (which also made the telemetry on/off comparison moot: neither leg leaked). The only workload that reproduced the leak was activity in a loaded, long-running session (large context, MCP servers, days-old process). I can't attribute the ~3/s between the host session and the agent it spawned, but the loaded/long-lived configuration — and/or concurrency on top of it — appears to be required. That's consistent with the OP's superlinear scaling under parallel agent load (21/s idle → 1,027–4,744/s heavy).
    4. Generic file I/O, execs, and bulk network are all clean. The profile most consistent with the known reproducers (this app's SSE token streaming; kubernetes/kubectl#1798's auth-retry loop; orbstack/orbstack#2368's small-write workloads) is high-frequency small network I/O, but I haven't isolated the syscall — and per takeaway 3, raw streaming volume alone doesn't do it on a fresh session.
    5. Per-unit rates look small but accumulate hard: this machine reached 4.47 GB in 6.5 days of mixed use (average 8/s around the clock), dominated by multi-instance overnight runs; quiet days add ~84 MB/day and nothing is ever returned without a reboot.

    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.

  10. spectre6000 commented on Jul 3, 2026

    @spectre6000

    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.1024 in-use elements via zprint, 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 claude process 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: nettop shows 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 3 EVFILT_TIMER entries 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.1024 zone 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.

  11. visualpharm commented on Jul 11, 2026

    @visualpharm

    Another independent reproduction, on a newer OS build and different Mac model than reported above:

    Environment

    • macOS 26.5.2 (25F84), Mac17,8, Claude Code 2.1.207 (currently installed; matches the version at time of panic)
    • Kernel: Darwin 25.5.0: Tue Jun 9 22:28:34 PDT 2026; root:xnu-12377.121.10~1/RELEASE_ARM64_T6050

    Panic

    panicString: zalloc[3]: zone map exhausted while allocating from zone [data.kalloc.1024],
    likely due to memory leak in zone [data.kalloc.1024] (20G, 21173392 elements allocated) @zalloc.c:4575
    timestamp: 2026-07-11 16:20:09 -0300
    

    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:

    1. A newer macOS point release (26.5.2 build 25F84) than previously reported (26.5.1/25F80)
    2. A newer Claude Code version (2.1.207) than the last update in this thread (2.1.200)
    3. A different Mac model (Mac17,8) than the two reporters above (Mac17,2, Mac14,2)

    Consistent with the thread's conclusion that this is a kernel-side leak tied to the claude process rather than anything model/config-specific. Happy to share the full panic report if useful — reboot was the only recovery.

  12. 9 remaining items

  13. michaelhayman commented on Aug 7, 2026

    @michaelhayman

    Second data point, on a different macOS major version — this reproduces on Sequoia 15.7.3, not just 26.x, which suggests long-standing behaviour rather than a 26-specific regression.

    Environment

       
    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

    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.

    Post-reboot baseline here is ~1,800–2,100 elements, matching your ~1,200.

    Monitoring note

    zprint reports the in-use count in field 7 without sudo, which makes unattended sampling easier:

    zprint 2>/dev/null | awk '$1=="data.kalloc.1024"{print $7}'
    

    I sample that every 60s alongside the mbuf zones (mbuf, data.mbuf.cluster.{2,4,16}k) and netstat -m. Over a 47-minute window data.kalloc.1024 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.

    Open question

    I have not been able to attribute the allocations to a call site without zlog=data.kalloc.1024 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 namei 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.

    I've since updated to 15.7.5 (24G624) and will report back if it recurs on that build.

  14. leonasdev commented on Aug 9, 2026

    @leonasdev

    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-code is empty. Installed via install.sh, always has been.
    • The only orphaned process on the box (a --chrome-native-host still running a deleted 2.1.220
      binary) 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. The data.kalloc.1024 from 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 zprint without root shows 0K for 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.

  15. thomasdalbo-at-m42 commented on Aug 13, 2026

    @thomasdalbo-at-m42

    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.

  16. aleks-azen commented on Aug 18, 2026

    @aleks-azen

    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.1024 at 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:

    1. It correlates with nothing in userspace. I ran Pearson correlations of hourly wired growth against every resident metric I sample (process count, open file count, per-family process counts and RSS for the CLI sessions, browser, node, Electron, multiplexer, load average, compression and pagein rates). Every coefficient came back between -0.07 and +0.09 — i.e. no relationship to what is resident. Growth tracks activity intensity, not process population, which fits the "scales with agent load" observation here rather than a per-process or per-session allocation.

    2. A controlled workload test could not reproduce it on demand. Timing 300 execs, 100 shell spawns, 300 file reads, 100 TCP connections and 3,000 stat calls 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 Claude Code CLI triggers macOS kernel memory leak (kalloc.1024) → kernel panic #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 auto run 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 checking zprint.

    Happy to share the raw per-minute CSVs or run a specific instrumented experiment if that would help isolate the call site.

  17. ariccio commented on Aug 29, 2026

    @ariccio

    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 ariccio account, 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.

    Verbatim research prompts and publication context

    Diagnosis prompt (with an Activity Monitor screenshot showing 14.14 GB wired of 24 GB):

    Figure out why memory usage is SO high: [Activity Monitor screenshot]
    Ultrathink

    Thread-research prompt:

    Interesting. Can you tell me more about #66020?
    Ultrathink.

    Mechanism-research prompt, then the contribution request:

    Tell me more about EndpointSecurity … PerMessageScratchSpaceTargetPathData::new_() under es_vnode_check_open ← mac_vnode_check_open and vn_getpath. Do more web research.

    When that agent finishes, then use EnterPlanMode to draft a contribution to the relevant github threads/issues. Ultrathink.

    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.

    This is a datapoint with deliberately the opposite profile from the npm-orphan reproductions above: a native install, no orphaned claude process, no third-party EndpointSecurity client, the thread's vn_getpath failed meter at 0 — and data.kalloc.1024 at 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 txt shows a bare path meter = 0
    Fix native installer / restart sessions after updates (userspace) + the Apple error-path fix unattributed; needs zlog=data.kalloc.1024 on 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 syspolicyd CPU (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 the vn_getpath error branch, allocated from XNU's vfs.namei view of data.kalloc.1024 — is also folded below, hedged as inference.

    Two asks:

    1. Triage hygiene: Claude Code CLI triggers macOS kernel memory leak (kalloc.1024) → kernel panic #44824 was auto-closed as a duplicate of Kernel panic: 3 Claude processes hit 17.3 GB resident on 18 GB machine, MacBook powered off (v2.1.37) #24960, a userspace Ghostty scrollback leak (frees on process exit — a different bug class), and [Bug] macOS kernel panic: data.kalloc.1024 zone exhausted from leaked file descriptors/kqueues in long-running sessions #82941 has the same panic signature and is going stale with 0 comments. One tracking issue for the kernel-zone class would help.
    2. Naming the (B) call site needs a machine with the zlog=data.kalloc.1024 boot-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.
    Environment and zone state
    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 list shows no ES system extension
    Uptime 119 d 18 h (booted 2026-05-01); typical load 1–2 interactive claude sessions + codex sessions, 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 print 0K unprivileged)
    Mechanism (A) checks on this machine — not firing, even though (A)-shaped processes exist
    • The thread's meter, log show --last 10m | grep -c "vn_getpath failed", reads 0; a 30-minute process == "kernel" sweep also has no vn_getpath/EndpointSecurity lines (other kernel lines — Sandbox, RTBuddy — are visible, so it is not a log-visibility artefact).
    • lsof -a -d txt -u $USER -F pcn census: no claude process maps a deleted inode (the native layout keeps old versions on disk).
    • But 18 other long-lived processes do run from deleted inodes (crashpad handlers, an updater running out of the Trash, three codex app-server trees whose brew-cask binaries were replaced under them — two of them 54 days ago); proc_pidpath() on them returns ENOENT — the exact 0x2 condition — 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 EndpointSecurity file subscriptions can amplify macOS kalloc.1024 kernel leak groundwater/Tractor#8 describes for a third-party tool with a global NOTIFY_OPEN subscription. On an Apple-only machine the per-file-op leg apparently has no subscriber.
    Controlled negatives (bounds, not explanations)

    Each probe bracketed by zprint samples ~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/close of /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 and proc_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 claude TUIs 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 tiny cc probe (linker ad-hoc signed) and check it is alive during the phase.

    Correlation sweep (weak lead)

    180 one-second samples of zone Δ vs per-process CPU Δ (ps): syspolicyd r = +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 whatever syspolicyd is doing in those bursts, it is not "every exec".

    Mechanism note for (A), from primary sources — the last step is inference
    • This Mac's /System/Library/KernelCollections/BootKernelExtensions.kc contains EndpointSecurityEventManager::es_vnode_check_open(ucred*, vnode*, label*, int) (one of 79 es_* 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" (%#x of ENOENT = the 0x2 in the log line).
    • XNU allocates MAXPATHLEN path buffers from ZONE_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 the vfs.namei tag @DimitarTachev reported.
    • build_path_with_parent (bsd/vfs/vfs_cache.c) returns ENOENT for 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.
    • So the leaked object is most plausibly the per-message scratch (or its path buffer) on the Either<int, …> error branch of allocScratchWithAttribution/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.
    Probe source (delprobe.c)
    /* delprobe: a non-platform binary we can delete while it runs, to test whether a
     * process executing from a deleted inode leaks kernel memory on (a) open/close,
     * (b) fork+exec of children, (c) stat.  Usage: delprobe <mode> <count> <delay_s>
     * It sleeps delay_s first (so the caller can unlink the binary), then does the ops. */
    #include <fcntl.h>
    #include <spawn.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/stat.h>
    #include <sys/wait.h>
    #include <unistd.h>
    
    extern char **environ;
    
    int main(int argc, char **argv) {
        if (argc < 4) { fprintf(stderr, "usage: %s openclose|exec|stat <count> <delay_s>\n", argv[0]); return 2; }
        const char *mode = argv[1];
        long count = strtol(argv[2], NULL, 10);
        int delay = atoi(argv[3]);
        sleep(delay);
        long ok = 0, fail = 0;
        for (long i = 0; i < count; i++) {
            if (strcmp(mode, "openclose") == 0) {
                int fd = open("/etc/hosts", O_RDONLY);
                if (fd >= 0) { close(fd); ok++; } else fail++;
            } else if (strcmp(mode, "stat") == 0) {
                struct stat st;
                if (stat("/etc/hosts", &st) == 0) ok++; else fail++;
            } else if (strcmp(mode, "exec") == 0) {
                pid_t pid; char *args[] = {"/usr/bin/true", NULL};
                if (posix_spawn(&pid, "/usr/bin/true", NULL, NULL, args, environ) == 0) {
                    int status; waitpid(pid, &status, 0); ok++;
                } else fail++;
            } else { fprintf(stderr, "bad mode\n"); return 2; }
        }
        printf("delprobe %s done ok=%ld fail=%ld\n", mode, ok, fail);
        return 0;
    }

    Build with cc -O1 -o delprobe delprobe.c (linker ad-hoc signs it), copy it, start the copy, unlink the copy, confirm proc_pidpath(pid) fails with ENOENT while it is still alive, bracket with zprint. Raw samplers and the correlation script are available on request.

  18. davidgibbons commented on Sep 2, 2026

    @davidgibbons

    Counter-datapoint to the npm-orphaning root cause: native install, no orphaned Claude binary, vn_getpath meter 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_getpath fails → 1 KB vfs.namei buffer 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

    data.kalloc.1024          inuse = 7,128,392  (6.80 GB)   <- 33% of the 21.3M ceiling
    data_shared.kalloc.1024   inuse = 3          <- clean here
    

    (Re @leonasdev's naming note: on this box the leaking zone is the one in the title, not data_shared.)

    • 7,128,392 elements over 5 d 21 h = 14.0 elems/s averaged across the boot
    • Instantaneous rate across four bracketed windows today, same 5 sessions, rising through the day: 62.9 → 84.1 → 84.1 → 106.0 elems/s — monotonic within every window, never reclaiming
    • At 106/s this box reaches the 21.3M ceiling in ~1.5 days

    Why this refutes orphaning as the mechanism here

    1. npm ls -g has never contained @anthropic-ai/claude-code; install is install.sh only.
    2. No claude process is running a deleted binary — every session's txt vnode resolves (table below). Caveat on the obvious one-liner: lsof -d txt | grep -c deleted is not a safe orphan check, because macOS ships a daemon literally named deleted (/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 the txt path per pid.
      Per-session txt vnodes, 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 txt still 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_getpath meter 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:

    • The log pipeline is live: 21,381 kernel-process messages in a 2-minute sample.
    • log show hides 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.
    • Broadened to eventMessage CONTAINS[c] "getpath" OR CONTAINS[c] "endpointsecurity" — zero rows over the whole window.
    • One gotcha worth naming for anyone reproducing on zsh: log is a zsh builtin and shadows /usr/bin/log, failing with too 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/log by 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_getpath correlation 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/gamecontrollerd XPC reconnect storm, Razer Synapse GUI layer, LM Studio (killed outright: 64.1 → 63.9 elems/s, zero change), the (IOSurface) SID: 0x0 WindowServer error loop (its rate anti-correlated with the leak), a genuinely wedged SMB mount, and shell spawning (positive control: 3,779 /bin/sh -c true spawns 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. zprint itself 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 2 loop takes ~137 s, which faked a "zprint self-contamination" result that doesn't exist.

  19. davidgibbons commented on Sep 19, 2026

    @davidgibbons

    macOS 27.0 appears to fix it: data.kalloc.1024 is 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.1024 rate 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 zprint over 90 s, and nothing grows anywhere near 85/s. The top steady grower was kalloc.192 at 8.4/s.

    Caveats:

    • The OS and Claude Code changed together, so this doesn't separate a kernel fix from a CLI fix. My 26.6.2 panics went through AMFI's mac_vnode_check_* exec hooks, which points at the kernel side, but that's inference.
    • This covers one day with light load: 2 sessions, one active. I'll post again if it creeps back under heavier agent load.

    If you're on 27.0 with any Claude Code version, a zprint data.kalloc.1024 reading plus your uptime would show which change fixed it.

  20. souliane commented on Sep 20, 2026

    @souliane

    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.
    • Wired memory: 20.41 GB of 24 GB. Free: 0.06 GB.
    • Uptime at the time: about 4 days, with parallel agents throughout.
    • No third-party kernel extensions. kextstat | grep -v com.apple returned nothing.
    • Nothing in user space reclaimed it. All user processes together used about 1.5 GB. sudo purge freed 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.1024 every 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.

    10:30  1584   wired 2.63GB
    10:50  1680   wired 2.71GB
    10:54  1744   wired 2.65GB
    11:19  1744   wired 2.51GB
    11:23  1632   wired 2.46GB
    11:28  1616   wired 2.46GB
    
    • Net change: +32 elements in 58 minutes. That is 0.009 elements per second.
    • The count also falls. It reached 1744, then returned to 1616. The kernel reclaims the zone now.
    • Wired memory stayed between 2.44 GB and 2.83 GB for the whole window.

    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.

  21. Exelery commented on Sep 25, 2026

    @Exelery

    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.namei

    $ sudo zprint data.kalloc.1024
    data.kalloc.1024            1024   11683760K     -------   11683760     -------    11683647    16K     16  C
    zone views                                                                                              inuse
    data.kalloc.1024[raw]                                                                                     18K
    data.kalloc.1024[vfs.namei]                                                                         11683623K
    

    So the leaked 1 KiB elements are MAXPATHLEN pathname buffers from the VFS path-lookup code (namei), not generic data buffers. data_shared.kalloc.1024 stays small (~60 MB) on this machine.

    Numbers

    • ~11.6 GB after 21 days of uptime, still growing. Pages wired down went to ~14 GB out of 24 GB.
    • Growth measured with zprint once a minute: ~500–1,500 elements/min during the day while working, ~300/min overnight with the sessions idle. It never decreases.
    • About 11–15 long-running claude sessions were open at the same time, some of them for days.

    What I ruled out (per-minute counter, each test 2–3 min)

    • Hammering these syscalls from a plain unsandboxed C program, 20–250 million calls each: stat, lstat, open, openat, access, readlink, realpath, getattrlist, getxattr, listxattr, fsgetpath, and stat on a nonexistent path. The growth rate did not change.
    • Photon's nested-shebang repro (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.
    • Pausing a set of unrelated long-running node/tsx processes for 4 minutes: no change.

    What moved it

    • SIGSTOP on the other claude processes for ~4 minutes dropped the rate from ~550/min to ~360/min. It went back up after SIGCONT. 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 namei path, is driven at least partly by claude.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 or fs_usage capture if that helps.

  22. willmcginnis commented on Sep 25, 2026

    @willmcginnis

    A per-exec trigger that contributes to "(B)"-type growth. In the macOS 26.x configurations we tested, successful #! script execs can leave about one data.kalloc.1024 element (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:

    • About 1 KiB was retained per level of shell scripts launching shell scripts: about 0.016, 0.99, 2.00 and 3.01 KiB at depths 1 to 4.
    • Compiled binaries stayed flat.
    • An LLDB dump of 600 retained buffers found a shell-script pathname in every one.

    This comment adds five things:

    • how the trigger reaches Claude Code hooks;
    • a difference between the configurations we tested;
    • evidence that failed execs leave nothing;
    • an allocation site from a kernel trace;
    • a mitigation.

    It follows up @ariccio's (A)/(B) split. The machine we measured most on is a native install. Its vn_getpath failed count 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 zprint readings 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

    • In the 26.6.2 VM, only successful interpreter execs retained anything (about one element each, from a #! 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 bash is still a #! exec. On the M1, an env-shebang parent launching another shebang script measured 1.27 elements per trial, so going through env did not avoid the trigger.
    • The two Macs differed:
      • On an M4 running 26.4.1, every successful #! exec we tested left an element, whatever the parent.
      • On an M1 running 26.6.2, the cost came from repeated #! execs within one process lineage, even with a Mach-O relay such as env or sh -c in between. The first #! exec in a lineage was cheaper (0.01 to 0.37).
      • We have not separated the OS build from the chip.
    • In the 26.6.2 VM, as root, nothing we tried got the memory back. We tried 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/bash run 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 true spawns and @spectre6000's /usr/bin/true loop 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/sh is a Mach-O binary, but when command is 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.

    • The simulated call cost 19.1, 19.6 and 20.1 elements over three reps.
    • The same chain, with an explicit interpreter in front of each hook, cost about 0. That was one rep, run back to back with an unprefixed rep, 200 calls each.

    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.

    Where the rest came from (estimates)

    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: bats test suites (about 1 element per test) and test-harness scripts. ps cannot 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.

    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:

    { "type": "command", "command": "/bin/bash ~/.claude/hooks/my-guard.sh" }

    rather than naming the script alone:

    { "type": "command", "command": "~/.claude/hooks/my-guard.sh" }
    • Use the interpreter the shebang resolves to, with any options on the shebang line. For #!/bin/bash -e, write /bin/bash -e script. Keep the hook's existing arguments after the script path. #!/usr/bin/env bash may mean Homebrew's bash rather than /bin/bash 3.2, and a different bash behaves differently.
    • The interpreter must be a Mach-O binary, not a script.
    • Keep the shell form: exec form (args) does not expand ~ or $HOME.
    • /bin/bash script also runs a script that has lost its executable bit, which the bare form would refuse.
    • Check each changed hook against its own allow and deny cases. We did this for 5 hooks: stdout, stderr and exit status were byte-identical before and after.

    The same applies anywhere a script launches a script: bash tool.sh rather than ./tool.sh.

    Possible upstream changes

    1. A note in the hooks docs suggesting the explicit-interpreter form for script hooks on macOS 26.x.
    2. An explicit interpreter option 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.
    3. Pass it to Apple as well. It is a kernel bug that Claude Code workloads amplify. We have a Feedback Assistant report drafted; it would help if Anthropic could also raise it through its own contacts. The trace below may also help with @ariccio's second ask, since it needed no zlog boot-arg.
    Allocation-site trace (SIP-disabled 26.6.2 VM, DTrace, N=800 per arm)

    SIP was already off in the stock Tart base image we used, so this needed no Recovery boot. We paired DTrace vminfo:::kalloc and vminfo:::zfree events by address over a 16-second window per arm. Allocations in the 1024 size class that were still unfreed when the window ended:

    • #! arm: 948
    • explicit-interpreter control: 108

    The 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's AppleSystemPolicy::credLabelUpdateExecve, reached from AppleSystemPolicy's own cred_label_update_execve MAC 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's syspolicyd CPU correlation.

    Reproducer (no root, no Claude Code involved; on an affected kernel it adds ~2 MB at N=2000 that may persist until reboot)

    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 in data_shared.kalloc.1024 instead (@leonasdev saw this on 26.3.1), so the script sums both zones.

    The script writes two small files into its own mktemp directory and removes them on exit. It subtracts the background rate measured during an idle window. It prints an ERROR instead of a verdict if zprint fails, 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.

    #!/bin/bash
    # Does this kernel retain a kalloc.1024 element per #! script exec?
    set -u
    N=${N:-2000}
    case $N in ''|*[!0-9]*) echo "ERROR: N must be a positive integer"; exit 2;; esac
    [ "$N" -gt 0 ] || { echo "ERROR: N must be a positive integer"; exit 2; }
    W=$(mktemp -d /tmp/kallocprobe.XXXXXX) || { echo "ERROR: mktemp failed"; exit 2; }
    trap 'rm -f "$W/leaf" "$W/drv"; rmdir "$W" 2>/dev/null' EXIT
    z(){ out=$(zprint 2>/dev/null) || return 1
      printf '%s\n' "$out" | awk '$1=="data.kalloc.1024"||$1=="data_shared.kalloc.1024"{
        f=1; if (NF<7 || ($7+0)""!=$7) bad=1; else s+=$7 } END{ if (f && !bad) print s }'; }
    num(){ case $1 in ''|*[!0-9]*) echo "ERROR: no zprint reading"; exit 2;; esac; }
    { printf '#!/bin/bash\nexit 0\n' > "$W/leaf" && chmod +x "$W/leaf" &&
      printf '#!/bin/bash\ni=0;while [ $i -lt %s ];do "%s/leaf" || exit 1;i=$((i+1));done\n' \
        "$N" "$W" > "$W/drv" && chmod +x "$W/drv"; } || { echo "ERROR: setup failed"; exit 2; }
    c0=$(z); num "$c0"; t0=$(date +%s); sleep 10; c1=$(z); num "$c1"; t1=$(date +%s)
    b=$(z); num "$b"; s0=$(date +%s)
    "$W/drv" || { echo "ERROR: workload failed"; exit 2; }
    sleep 3; a=$(z); num "$a"; s1=$(date +%s)
    awk -v c0="$c0" -v c1="$c1" -v t="$((t1-t0))" -v b="$b" -v a="$a" -v s="$((s1-s0))" \
        -v n="$N" -v os="$(sw_vers -productVersion)" -v bld="$(sw_vers -buildVersion)" '
    BEGIN{ bg=(c1-c0)/t; expect=bg*s; r=((a-b)-expect)/n
      if (expect>n/2 || -expect>n/2) v="INCONCLUSIVE (background growth too high; close Claude Code and other agents, then rerun)"
      else if (r>0.5) v="SIGNAL DETECTED (confirm with the control arm)"; else if (r<-0.2) v="INCONCLUSIVE (zone shrank during the run)"
      else if (r<0.2) v="NO SIGNAL DETECTED"; else v="INCONCLUSIVE (rerun with N=8000)"
      printf "%s  macOS %s (%s)  N=%d  idle=%+.1f/s over %ds  active=%+d over %ds  per_exec=%.4f\n",
             v, os, bld, n, bg, t, a-b, s, r }'

    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 two per_exec values.

  23. rezzminator commented on Oct 1, 2026

    @rezzminator

    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.1024 held 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. zprint hides the cur-size column from a normal user, but it still prints elem size and cur inuse. Sampling cur inuse 30–60 s apart gives the leak rate:

    zprint data.kalloc.1024 | awk '$1=="data.kalloc.1024"{for(i=2;i<=NF;i++) if($i ~ /^[0-9]+$/){print $(i+5); exit}}'

    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/sh loop 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 --assess and codesign -v all showed no excess.

    What made it huge here: a wrapper exec loop.

    • sudo eslogger exec, with args and env stripped through jq before writing, captured 10 s of launches.
    • The capture showed 609 execs of a 2-line #!/bin/sh claude wrapper shim, all in one pid.
    • The wrapper exec'd a launcher. Under a misconfigured environment (a test harness that changed the launcher's home but kept the real PATH), the launcher resolved the wrapper itself as the "real" claude and exec'd it again. That ran about 61 times/s for 33 h.
    • Killing that one process took zone growth from 70/s to about 5/s at once. The remainder is ordinary hook and statusline traffic.

    Notes for anyone hunting their own source:

    • Rank .event.exec.script.path and .process.responsible_audit_token.pid from eslogger exec. Sources that stay alive are easy to find this way. Short-lived ones are invisible to ps sampling.
    • eslogger needs Full Disk Access on the app that hosts the shell. Inside VS Code's terminal, that means VS Code, not Terminal.
    • dtrace proc:::exec-success is unavailable under SIP.
    • A claude wrapper that is itself a #! script costs about 1 KiB per invocation on affected builds. Wrappers that end in exec "$real_claude" from a compiled binary, or that are invoked through an explicit interpreter, avoid it.
    • Re-entry guards in wrapper/launcher chains (e.g. an env depth marker) are worth having anyway: an accidental self-exec loop on an affected macOS is a few GB per day.

    Reports above say macOS 27.0 fixes it. I haven't verified that on this machine yet.

  24. renchris commented on Oct 19, 2026

    @renchris

    Sequoia data point: macOS 15.7.9 (24G830), MacBookPro18,2 (M1 Max, 64 GB). It panicked, and the #! per-exec trigger reproduces on 15.x.

    Panic (2026-10-18), after 18.05 days of uptime:
    zalloc[3]: zone map exhausted while allocating from zone [data.kalloc.1024], likely due to memory leak in zone [data.kalloc.1024] (19G, 20955520 elements allocated)

    • Kernel: xnu-11417.140.69.711.44~1. The panicked task was nesessionmanager, a routine plist save going through APFS IOMallocZeroData. It is a victim.
    • The zone map's DATA range is 25.66 GiB. The panic came at 20.96M elements, the same ceiling as the 20–21.3M cluster reported in this thread.
    • For about 8 hours before the panic the kernel logged hundreds of memorystatus: killing_idle_process ... osr_code: 9 lines. (Corrected wording: code 9 is the generic JETSAM_REASON_MEMORY_IDLE_EXIT, not the zone-map-specific code 10, so these lines do not name the zone map. They were still absent on a healthy boot of the same machine (0 in 8 h) and only appeared under pressure, so they work as a pre-panic sign, though not a zone-specific one. Killing idle daemons frees nothing the leak holds.)

    Per-exec A/B on 15.7.9, no root. 96 interleaved ON/OFF cycles at load ~360. The signal is data.kalloc.1024 − cred inuse (zprint column 7): subtracting cred removes most of the fleet noise, taking the residual SD per cycle from 636 down to 133.

    arm retained elements per run (±1 SE)
    /usr/bin/true −0.02 ± 0.02
    one #!/bin/sh leaf +0.94 ± 0.06 (repeat: +1.07 ± 0.08)
    2 / 3 nested #! scripts +1.98 ± 0.13 / +2.86 ± 0.20
    #! script that execs a Mach-O binary +0.89 ± 0.09
    /bin/sh leaf.sh, /bin/bash leaf.sh ≈ 0

    Things that did not leak in the same harness: sandbox-exec with a unique or a shared profile, setuid ps, launchctl, osascript, codesign, and bash -c.

    What that means for Claude Code users. Hooks configured as a bare script path ("command": "~/.claude/hooks/x.sh") go through a #! exec on every event. Ours had about 100 such hooks, roughly 20 of them on every Bash tool call. Across the last boot that leaked 1.1 GiB/day on average, and 3.6–4.4 GiB/day under heavy multi-session load. The workaround is to give every hook and statusLine command an explicit interpreter (/bin/bash ~/.claude/hooks/x.sh).

    It would help a lot if Claude Code invoked script hooks through their interpreter itself, or documented the explicit-interpreter form as the recommended one for macOS.

  25. CodeWarrior4Life commented on Nov 2, 2026

    @CodeWarrior4Life

    Disclosure: a Claude Code agent did the measuring and drafted this comment. The account owner approved posting it.

    Data point: macOS 26.5.2 (25F84), Mac17,6 (128 GB), Claude Code 2.1.316. It panicked at 20,815,424 elements after ~10 days of uptime. On this build the leak sits in the [raw] view, not in [vfs.namei].

    Zone view, ~5 h after the reboot:

    data.kalloc.1024[raw]        155577K
    

    No [vfs.namei] view appears at all. So on 25F84 this does not look like the npm-orphan / vn_getpath chain. It matches the #! per-exec trigger described by Photon and by willmcginnis and rezzminator above. That is a bracketed zprint A/B with idle controls: 2000 execs per arm, 4 reps, with a Mach-O python3 parent at depth 1.

    Exec Elements per exec, above idle
    #!/bin/sh script, by path +1.035
    #!/usr/bin/env bash script, by path +0.983
    same script as sh a.sh +0.033
    Mach-O /bin/sh -c, /usr/bin/true ~0

    Two of our results may help others:

    1. Even at depth 1, a by-path #! exec costs ~1 element when the parent is a non-shell (Mach-O) process. In Photon's 26.5.x depth results, depth 1 was ~0.016. Our launchers were Python and node, so hook runners and statusline helpers that subprocess a script by path pay the full rate.

    2. ES exec events name the offender directly. eslogger exec sets event.exec.script only when a #! script is exec'd. Group by it:

      sudo timeout 60 eslogger exec | python3 -c 'import json,sys,collections; c=collections.Counter(json.loads(l)["event"]["exec"]["script"]["path"] for l in sys.stdin if json.loads(l)["event"]["exec"].get("script")); [print(v,k) for k,v in c.most_common(20)]'

      On our host the steady-state offenders were:

      • a statusline whose pinned ccstatusline copy had gone missing, so it fell back to npx (two #! node scripts per refresh, per session);
      • statusline helper .sh files;
      • a credential-resolver .sh exec'd by path from Python hooks.

      Test suites with shell-script PATH shims (fake ssh/curl/docker) gave the bursts: one suite drove ~16 elements/s for 41 min. After we fixed the statusline, our host's #! exec rate fell from 4.0/s to 1.3/s (part of that drop was lighter test load).

  26. leonasdev commented on Nov 13, 2026

    @leonasdev

    Follow-up to my 08-09 comment: the per-session leak on this machine was the statusline. Running it through an explicit interpreter stopped it.

    Same M4 Mac mini (16 GB), macOS 26.3.1 (25D2128), Claude Code 2.1.323, native install. The status line was set up as:

    "statusLine": { "type": "command", "command": "~/personal/claude-statusline/statusline.sh", "refreshInterval": 1 }

    statusline.sh is a #!/bin/bash wrapper that execs a Go binary. With refreshInterval: 1, every session runs one by-path #! exec per second, so about 60 per minute. That is the trigger described in willmcginnis's comment and Photon's write-up. I changed one line, with nothing else touched and no sessions restarted:

    "command": "bash ~/personal/claude-statusline/statusline.sh"

    data_shared.kalloc.1024 in-use elements, sampled every 30 s for 5 min, with the same 7 sessions, 15 min after a reboot:

    before after
    5 min change 3,356 → 5,702 (+2,346) 6,235 → 6,230 (−5)
    rate ~466/min (~67 per session) ~0, moving between 6,192 and 6,273

    So the "constant per-session rate, even when idle" that I reported (and that spectre6000 saw on 07-03) is the statusline refreshing on its own schedule. It doesn't depend on any session activity. CodeWarrior4Life also named a statusline among the offenders above. This is a clean single-variable A/B for that case. On this machine it accounted for essentially all of the leak.

    If your leak rate scales with the session count, check your statusLine.command first. If it is a path to a script, put bash (or sh) in front of it, or point it at the compiled binary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghas reproHas detailed reproduction stepsperf:memoryplatform:macosIssue specifically occurs on macOS

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions