You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
With the default config (no config.toml), extraction.summarizer.provider = "auto" resolves to the Claude CLI whenever claude is on PATH. Every SessionEnd hook then forks a detached icm extract-pending worker, which spawns a bare claude -p --model claude-haiku-4-5. That child is a full Claude Code session: it inherits the user's global settings — including the ICM hooks that icm init installed — and boots every globally configured MCP server. When the child session ends, its own SessionEnd hook forks the next worker. The result is a self-sustaining spawn chain that never terminates while any live session keeps enqueueing rows.
There is no recursion guard, no concurrency limit, and no queue-empty pre-check anywhere in this path (verified against main @ 65ae008; no ICM_* env guard, no flock/singleton).
Real-world impact (incident, 2026-07-15)
macOS 26.5.2, MacBook Pro M4 Pro (14 cores, 24 GB), icm 0.10.50 via install script, Claude Code with hooks installed by icm init, several MCP servers in global config:
Sustained load average 46–61, >90% CPU, CPU temperature >100 °C
~100,000 processes spawned in ~3 h (unique-PID counter from a shutdown_stall spindump; after reboot the loop resumed at ~80–100 spawns/sec)
Steady state: 5 concurrent icm extract-pending --limit 20 workers + 5 claude -p --model claude-haiku-4-5 sessions, each booting all global MCP servers (uv-managed Python servers, npm exec …@latest with a registry fetch per spawn)
Two forced reboots, both producing shutdown_stall reports — dozens of live claude processes (parent: icm) blocked shutdown
detect_provider treats CLAUDECODE as a reason to pick the Claude CLI — summarizer.rs#L69-L71. Inside a Claude Code hook environment CLAUDECODE is always set, so the signal that means "you are already running inside Claude Code" selects the provider that spawns another Claude Code. The Auto fallback is Claude as well (#L81-L85).
The spawn has no isolation — summarizer.rs#L175-L179: claude -p --model claude-haiku-4-5 with no --setting-sources and no --strict-mcp-config. The child loads the user's global hooks (i.e. ICM's own hooks) and every configured MCP server — for a prompt that extracts bullet points from tool output.
SessionEnd forks unconditionally — main.rs#L3354-L3411: whenever provider != "none", cmd_hook_end spawns a detached (setsid, stdio → /dev/null) extract-pending --limit 20, with no lock, no dedup, and no check whether the queue is even non-empty. The worker's own claude -p session re-enters this exact function when it ends.
Loop mechanics
extract-pending batches all rows into a single prompt (one claude call per worker run), so a single chain is linear, not exponential:
SessionEnd → fork extract-pending → claude -p (full session, all hooks + MCP)
↑ │
└────────────── its SessionEnd ────────────┘
The chain only dies when the queue is empty — but icm hook post enqueues raw tool output from every live session (store_raw = true, extract_every = 3), so on a machine with an active Claude Desktop app / CLI sessions / Codex the queue never drains. Every ending session starts an additional parallel chain. Each link costs a full Claude Code boot plus all MCP server startups.
Side effect: N concurrent workers plus all hook processes write to the same SQLite DB, which makes the concurrent-write corruption reported in #313 (no WAL) considerably more likely to trigger.
Verified: extract-pending then drains the queue with the batched fastembed extractor in-process, zero subprocess spawns, load back to normal.
Suggested fix
All four pieces, not just one:
Reentrancy guard: set a marker env var (e.g. ICM_WORKER=1) on every CLI the summarizer spawns; cmd_hook_end (and ideally every hook subcommand) no-ops when it is set. This kills the loop even if the CLI flags below ever change.
Spawn isolation for the Claude provider: claude -p --setting-sources "" --strict-mcp-config so the child runs without user hooks and without MCP servers (both flags exist in the current Claude Code CLI).
Singleton lock: flock a lockfile next to the DB in cmd_extract_pending; exit immediately if another worker holds it.
Default: keep provider = "none" until 1–3 have landed, or gate auto behind an explicit opt-in during icm init. A default that turns every session end into a fresh LLM-CLI session is not a safe out-of-the-box behavior — Excessive memory and CPU spikes caused by ICM every time read occurs. #239 traded a 50 ms hook-latency problem for a thermal-runaway one.
Happy to provide the spindump excerpts / process listings from the incident if useful.
Summary
With the default config (no
config.toml),extraction.summarizer.provider = "auto"resolves to the Claude CLI wheneverclaudeis on PATH. Every SessionEnd hook then forks a detachedicm extract-pendingworker, which spawns a bareclaude -p --model claude-haiku-4-5. That child is a full Claude Code session: it inherits the user's global settings — including the ICM hooks thaticm initinstalled — and boots every globally configured MCP server. When the child session ends, its own SessionEnd hook forks the next worker. The result is a self-sustaining spawn chain that never terminates while any live session keeps enqueueing rows.There is no recursion guard, no concurrency limit, and no queue-empty pre-check anywhere in this path (verified against
main@ 65ae008; noICM_*env guard, no flock/singleton).Real-world impact (incident, 2026-07-15)
macOS 26.5.2, MacBook Pro M4 Pro (14 cores, 24 GB), icm 0.10.50 via install script, Claude Code with hooks installed by
icm init, several MCP servers in global config:shutdown_stallspindump; after reboot the loop resumed at ~80–100 spawns/sec)icm extract-pending --limit 20workers + 5claude -p --model claude-haiku-4-5sessions, each booting all global MCP servers (uv-managed Python servers,npm exec …@latestwith a registry fetch per spawn)shutdown_stallreports — dozens of liveclaudeprocesses (parent:icm) blocked shutdownRoot cause (code walk, main @ 65ae008)
Default provider is
auto, deliberately changed fromnonein Excessive memory and CPU spikes caused by ICM every time read occurs. #239 —config.rs#L265-L284. The change fixed hook latency (~50ms hooks, no fastembed load) but put every default install with aclaudebinary on the CLI-spawn path.detect_providertreatsCLAUDECODEas a reason to pick the Claude CLI —summarizer.rs#L69-L71. Inside a Claude Code hook environmentCLAUDECODEis always set, so the signal that means "you are already running inside Claude Code" selects the provider that spawns another Claude Code. TheAutofallback is Claude as well (#L81-L85).The spawn has no isolation —
summarizer.rs#L175-L179:claude -p --model claude-haiku-4-5with no--setting-sourcesand no--strict-mcp-config. The child loads the user's global hooks (i.e. ICM's own hooks) and every configured MCP server — for a prompt that extracts bullet points from tool output.SessionEnd forks unconditionally —
main.rs#L3354-L3411: wheneverprovider != "none",cmd_hook_endspawns a detached (setsid, stdio → /dev/null)extract-pending --limit 20, with no lock, no dedup, and no check whether the queue is even non-empty. The worker's ownclaude -psession re-enters this exact function when it ends.Loop mechanics
extract-pendingbatches all rows into a single prompt (oneclaudecall per worker run), so a single chain is linear, not exponential:The chain only dies when the queue is empty — but
icm hook postenqueues raw tool output from every live session (store_raw = true,extract_every = 3), so on a machine with an active Claude Desktop app / CLI sessions / Codex the queue never drains. Every ending session starts an additional parallel chain. Each link costs a full Claude Code boot plus all MCP server startups.Side effect: N concurrent workers plus all hook processes write to the same SQLite DB, which makes the concurrent-write corruption reported in #313 (no WAL) considerably more likely to trigger.
Workaround
Verified:
extract-pendingthen drains the queue with the batched fastembed extractor in-process, zero subprocess spawns, load back to normal.Suggested fix
All four pieces, not just one:
ICM_WORKER=1) on every CLI the summarizer spawns;cmd_hook_end(and ideally every hook subcommand) no-ops when it is set. This kills the loop even if the CLI flags below ever change.claude -p --setting-sources "" --strict-mcp-configso the child runs without user hooks and without MCP servers (both flags exist in the current Claude Code CLI).flocka lockfile next to the DB incmd_extract_pending; exit immediately if another worker holds it.provider = "none"until 1–3 have landed, or gateautobehind an explicit opt-in duringicm init. A default that turns every session end into a fresh LLM-CLI session is not a safe out-of-the-box behavior — Excessive memory and CPU spikes caused by ICM every time read occurs. #239 traded a 50 ms hook-latency problem for a thermal-runaway one.Happy to provide the spindump excerpts / process listings from the incident if useful.