Desktop app SIGKILLs its own Claude Code workers on a healthy Mac, then blames endpoint security (gates on raw free pages, not memory pressure)

Status Open
Reported on v2.1.246
Maintainer reply None cached
Activity 4 comments · opened Aug 27, 2026

Summary

The desktop app SIGKILLs its own Claude Code worker processes on a healthy machine, and reports the cause to the user as "Blocked by endpoint security — ask your IT team to allowlist signing Team ID Q6L2SF6YDW."

There is no endpoint-security software on this machine. The kills correlate with sys_free_raw (raw free pages) falling below ~800 MB — a metric that is near-zero on every healthy macOS system by design, because macOS deliberately uses idle RAM for cache and compressed pages.

Two bugs:

  1. The gate uses the wrong memory metric. Low raw-free memory is the normal steady state on macOS, not a distress signal. The OS exposes kern.memorystatus_vm_pressure_level for exactly this purpose.
  2. The error message misattributes the cause, sending users to their IT department for a problem the app created. On a personal Mac with no MDM there is nothing to allowlist.

Environment

  • Claude desktop 1.37937.3 (also observed on 1.30096.5 and 1.32885.1 — this is not new)
  • Claude Code 2.1.246
  • macOS 26.6.1, Apple M4, 16 GB RAM, no MDM, no EDR, no MDM profiles

Evidence

The machine is not under memory pressure at any point:

| macOS metric | value |
|---|---|
| kern.memorystatus_vm_pressure_level | 1 (normal) |
| vm.swapusage used | 0.00 MB |
| memory_pressure "system-wide memory free" | 78% |
| Jetsam reports in /Library/Logs/DiagnosticReports | 0, ever |
| log show --predicate 'process == "kernel"' around each kill, matching kill/jetsam/memorystatus | 0 lines |

No endpoint-security software exists to have done it:

| check | result |
|---|---|
| systemextensionsctl list — endpoint security extensions | 0 |
| /Library/LaunchDaemons/ | empty |
| EDR processes (CrowdStrike, SentinelOne, Sophos, Defender, Jamf, Santa…) | 0 running |
| unified-log endpoint-security events in each kill window | 0 |

The app's own log shows it doing the killing. Every kill is recorded by the app with its own error class:

[error] Session <id> query error: Claude Code process terminated by signal SIGKILL
{ errorClass: 'process_killed_by_signal', signal: 'SIGKILL',
  stack: 'Error: ... at vq.getProcessExitError (…/app.asar/.vite/build/index.chunk-*.js) …' }

The correlation — 4 kills in one day vs 211 [process-memory] readings

| kill time | tree_rss_sum | children | sys_free_raw | sys_free |
|---|---|---|---|---|
| 11:54:35 | 8928 MB | 142 | 63 MB | 2174 MB |
| 11:59:57 | 4419 MB | 29 | 350 MB | 3037 MB |
| 12:07:03 | 4692 MB | 39 | 697 MB | 3360 MB |
| 15:04:18 | 4195 MB | 26 | 268 MB | 4530 MB |

  • Tree size does not discriminate — 4.2–8.9 GB and 26–142 children across the four kills.
  • sys_free_raw does — every kill occurred below 800 MB; not one occurred above it (0 of 30 readings ≥800 MB were followed by a kill within 60 s).
  • It is a precondition, not a trigger: 177 readings below 800 MB were not followed by a kill.

Note the 11:54 row: the app logged sys_free=2174MB alongside sys_free_raw=63MB at the same instant. It appears to gate on the raw figure.

Why this is the wrong metric

macOS keeps raw free pages near zero on purpose — unused RAM is used for file cache and compressed memory, and released on demand. A machine with 63 MB "free", 0 bytes of swap, and pressure level 1 is healthy. Gating on that figure means the app will reap workers on any well-utilised Mac, which is what happens here: the kills land while the OS itself reports normal pressure and has never once needed to jetsam a process.

Impact

  • Long-running agent work is destroyed mid-flight, repeatedly (12 sessions killed in a single second in one batch).
  • The error message sends users to IT for a non-existent security product. Considerable time was lost chasing that before the app's own logs identified the real killer.
  • Workaround that shouldn't be necessary: keeping raw free memory artificially high by quitting other applications.

Suggested fixes

  1. Gate on kern.memorystatus_vm_pressure_level (or DISPATCH_SOURCE_TYPE_MEMORYPRESSURE) rather than raw free pages. Optionally also require non-trivial swap activity before acting.
  2. Never attribute a self-inflicted SIGKILL to endpoint security. If the app's own watchdog reaped the process, say so — and ideally log the metric and threshold that fired. The current message is actively misleading on machines with no security software installed.
  3. If a memory guard is genuinely required, warn before reaping so in-flight work can be checkpointed.

Reproduction

Not deterministic, but reliably reproduced by running several concurrent Claude Code sessions plus ordinary developer processes (a dev server, a browser) on a 16 GB Mac until sys_free_raw drops below ~800 MB — visible in the app's own [process-memory] interval log lines. Kills follow within ~30 s while macOS reports normal pressure and zero swap.

Happy to supply full anonymised log extracts on request.

View original on GitHub ↗

3 Comments

klb-solutions · 1 month ago

Follow-up: two more kills tonight, with watchdog-race and restart-amplification evidence

Two further SIGKILLs on the same machine today (2026-08-28, ~18:06 and ~18:12 local), both showing the "Blocked by endpoint security / Team ID Q6L2SF6YDW" card. Still no endpoint-security software installed. This comment adds two mechanisms observed with hard data.

1. The kill races — and beats — an external mitigation sampling every 2 minutes

Since filing this issue I run a cron that samples free RAM and reaps stray processes below a threshold. Its log for tonight's first kill:

18:06:00 AUTO free_mb=76 below warn=1500 — reaping
reaped 4 stray(s) — free RAM 72 MB → 1165 MB

Free RAM collapsed to 76 MB in under one 2-minute sampling tick (a test suite + build burst), and the app's kill fired before the reap landed. Note also that the kill itself frees the dead worker's RSS, so any post-kill sample under-reports how low free RAM was at the trigger moment. All six kills observed on this machine to date are consistent with the <800 MB raw-free gate described in the original report; macOS memory pressure remained normal throughout.

2. SIGKILL amplifies its own trigger: orphaned MCP stacks and multiplied workers

Because the worker is SIGKILLed, it can never clean up its children. After tonight's two kills the process table held:

  • 5 live claude-code session workers simultaneously (272–468 MB RSS each) — three of them spawned within 20 seconds during the crash-restart sequence, plus one surviving from the previous day
  • 12 obsidian-mcp-pro, 34 context-mode, 9 fakechat MCP-server processes across those stacks
  • 6.0 GB total Claude-ecosystem RSS on a 16 GB machine

So each kill leaves the previous session's MCP servers resident, the restart spawns a fresh full stack, baseline memory ratchets upward, and the next kill arrives sooner — a feedback loop in which the watchdog manufactures the very condition it gates on. Tonight the second kill followed the first by roughly six minutes.

Happy to provide the raw ps snapshots or guard logs if useful.

klb-solutions · 1 month ago

Correction to my previous comment — point 2 was overstated

Following my own advice to verify rather than assume, I checked parentage on every process I cited, and I got part of it wrong.

The five concurrent claude-code workers all have LIVE parents (the Claude.app process and a claude CLI process). They are legitimate concurrent sessions, not orphans left by SIGKILLs. I asserted a restart-amplification feedback loop; that specific mechanism is not established by my data and I withdraw it.

What does hold up:

  • Orphaned MCP servers are real, but smaller in scale than I implied. One session's parent process was dead while its MCP children (a context-mode, an obsidian-mcp-pro and its node child) were still resident — roughly 290 MB. My reaper cleared them between two consecutive checks, which is also why my follow-up snapshot showed 0 MB reclaimable. So SIGKILL does leak MCP stacks, since a killed worker cannot clean up its children; I just cannot substantiate the ratcheting-baseline story I built on top of it.
  • Point 1 stands unchanged, and is the substantive one: free RAM fell to 76 MB inside a single 2-minute sampling window and the kill beat the mitigation. Six kills to date, all consistent with the <800 MB raw-free gate, with macOS memory pressure normal throughout.
  • The 6.0 GB total Claude-ecosystem RSS on a 16 GB machine figure is accurate as measured, but with five legitimate sessions open it reflects normal multi-session usage rather than leaked state.

Apologies for the noise. The unverified inference was mine, not the data's.

123andy · 1 month ago

Adding a data point from a machine that should break one of the confounds here: 48 GB Mac Studio, macOS 26.6.2, Desktop 1.37937.1 (yours is .3), same message, same Q6L2SF6YDW, systemextensionsctl list → 0 extensions, no MDM/EDR of any kind. Four of my kills also landed on 2026-08-27.

On the sys_free_raw threshold — I think it may be a base-rate artifact, and I'd rather say so than quietly agree.

I pulled every [process-memory] sample and every exit-137 from my rotated logs: 44,200 samples, 12 kills. On this machine sys_free_raw has no predictive power:

| | |
|---|---|
| median sys_free_raw | 232 MB |
| minimum observed | 14 MB (no kill) |
| share of all samples under 800 MB | 95% |
| kills under 800 MB | 11 of 12 — chance predicts 11.4 |

Ranking each kill against the machine's own distribution:

2026-08-27 11:21:23    199 MB    42nd percentile
2026-08-27 12:56:32    106 MB    20th
2026-08-27 12:58:53    694 MB    94th
2026-08-27 12:59:02    694 MB    94th
2026-09-04 12:15:34    642 MB    92nd
2026-09-04 16:28:52    400 MB    78th   <- burst of 6 in 82s
2026-09-04 16:29:01    400 MB    78th
2026-09-04 16:29:09    400 MB    78th
2026-09-04 16:29:20    400 MB    78th
2026-09-04 16:29:36    400 MB    78th
2026-09-04 16:30:14   1236 MB    98th
2026-09-04 16:34:12    129 MB    26th

Mean kill percentile is 71st — kills happen at higher free memory than typical, and the machine spends half its life below the lowest kill value without anything dying. I don't think this refutes your correlation on a 16 GB M4, where samples above 800 MB may be genuinely rare; it does suggest the threshold needs testing on a machine where that line isn't nearly always crossed. If you can post your sample count and median sys_free_raw, that would settle whether we're looking at the same effect.

Two artifacts I haven't seen quoted elsewhere in this family.

The classification is a named telemetry category, not just the dialog string:

[CycleHealth] Unhealthy cycle: {
  session_type: 'chat',
  unhealthy_reason: 'system_error',
  error_message: 'Claude Code process exited with code 137',
  error_category: 'endpoint_security_blocked' }

And I get a different code path to yours — errorClass: 'process_exited_nonzero' with exit 137, where you have process_killed_by_signal / SIGKILL. Same 128+9 event, but reaching the app as a wait-status rather than a delivered signal. Both funnel into the same endpoint_security_blocked category, which supports the mapping being unconditional.

Timing argues against a memory watchdog reaping a heavy worker. The child dies within ~1s of spawn, before the first stdin write lands:

16:28:51  [preseed] installed bundle .../claude-code/2.1.246
16:28:51  [HostLoop] Staged plugin: /var/folders/.../claude-hostloop-plugins/...
16:28:51  Session local_22bbd050...: initializing -> running
16:28:52  [HostLoop] cli.js stdin write failed (child likely exited): EPIPE
16:28:52  exited with code 137

had_first_response: false, seconds_to_outcome: 2. Nothing has run yet, so there is no large worker to reap — and it survived a full app relaunch mid-burst. It is also intermittent under identical conditions: 15 healthy vs 8 unhealthy cycles on the same day, same config.

Separately, whatever the cause turns out to be: the dialog telling users to ask IT to allowlist Anthropic's own Team ID is actively harmful on a machine with no endpoint security. It generates support tickets that cannot be closed. A fallback along the lines of "the helper process was terminated unexpectedly" would cost nothing and mislead nobody.

Showing cached comments. Read the full discussion on GitHub ↗