Plugin: claude-mem@thedotmack v12.3.8
Repo: https://github.com/thedotmack/claude-mem
Reporter platform: Windows 11 Pro, Claude Code, Git Bash (MSYS2), Bun v1.x, Node v22+
Symptom: Enabling the plugin causes Claude Code startup to spawn ~8–9 bash processes and makes the UI unresponsive for ~1 minute. Typed commands are ignored or severely delayed even after startup.
All file references are inside the plugin cache:
~/.claude/plugins/cache/thedotmack/claude-mem/12.3.8/
Root cause of the ~1-minute freeze and 8-9 bash processes:
- hooks.json:43 — UserPromptSubmit blocks every prompt up to 10 seconds doing sleep 1 health polling. This is why the UI is unresponsive when typing.
- hooks.json:16-36 — SessionStart fires 3 hooks in sequence, hooks #2 and #3 each have their own 20-iteration sleep 1 curl-poll loops. Worst case: 40 seconds of polling on top of everything else.
- Every hook prefix is $SHELL -lc 'echo $PATH' — a full login-shell fork per hook to read PATH. On Windows Git Bash this is the most expensive bash operation. ~2s wasted per session just on that.
- smart-install.js runs on every SessionStart, not just once. Forks bun, uv, version checks each time.
- PostToolUse has matcher * — every single tool call forks the whole Node→bun→worker chain. That's the post-startup sluggishness.
- PreToolUse Read timeout is 2000 — likely a seconds/ms typo (Claude Code hook timeouts are in seconds, so that's 33 minutes).
- Ships a 61 MB macOS-only binary that can't run on Windows.
- The plugin's own hooks/bugfixes-2026-01-10.md acknowledges #638 "Worker startup missing JSON output causes hooks to appear stuck" and #646 "Plugin bricks Claude Code — stdin fstat EINVAL crash" — both still open at the time this version shipped.
The report itemizes 19 issues grouped by severity, each with specific file references and suggested fixes. The last section is a 6-step minimum patch list the author could ship to restore basic usability.
hooks/hooks.json line 43. Before every prompt the user types, this command runs synchronously:
_HEALTH=0; curl -sf http://localhost:$PORT/health >/dev/null 2>&1 && _HEALTH=1 \
|| for i in 1 2 3 4 5 6 7 8 9 10; do
sleep 1;
curl -sf http://localhost:$PORT/health >/dev/null 2>&1 && _HEALTH=1 && break;
done
[ "$_HEALTH" = "1" ] && node "$_R/scripts/bun-runner.js" ... session-initWhy it breaks the UX: If the worker is slow to come up or has died, every keystroke submission waits up to 10 × (sleep 1 + curl fork) ≈ 10+ seconds. On Windows where each curl/sleep fork adds real overhead, this is what the user perceives as "UI frozen when I type."
Fix: Make the hook non-blocking (fork to background, let worker-service accept the event asynchronously, or skip when the worker isn't healthy instead of polling). A UserPromptSubmit hook must never synchronously sleep.
2. SessionStart fires three hooks in sequence, two of which do their own 20-second health-check polling loops
hooks/hooks.json lines 16–36. The SessionStart block has three separate hooks entries. Hooks #2 and #3 each contain:
for i in 1 2 3 ... 20; do
curl -sf http://localhost:$PORT/health >/dev/null 2>&1 && break
sleep 1
doneWorst case: 20 + 20 = 40 seconds of cumulative polling before the session becomes responsive. Plus whatever smart-install.js and the worker spawn itself cost (see Issue 4). That matches the user's report of ~1 minute of unresponsiveness.
Fix: The worker-service should expose a readiness mechanism (a file, a socket, stdout JSON) — not a polling loop across multiple independent hooks. Or merge the three SessionStart hooks into one command that starts and waits once.
hooks/hooks.json line 51 ("matcher": "*"). Every time Claude Code runs ANY tool (Read, Edit, Bash, Grep, WebFetch, etc.), the plugin spawns:
bash → $SHELL -lc 'echo $PATH' (another bash) → node bun-runner.js → bun worker-service.cjs hook claude-code observation
On Windows each fork in that chain costs ~100–300 ms. A modest tool-heavy turn (10 tool calls) burns several extra seconds on this alone, even after the worker is healthy. This compounds the "sluggish UI" feeling throughout the session, not just at startup.
Fix: Batch observations via a persistent IPC channel (the worker already exposes HTTP on localhost — use it with curl --max-time 0.1 -d @- in fire-and-forget mode) instead of fork-execing a 1.9 MB bundled CJS script per tool use.
hooks/hooks.json lines 4–15 (Setup) and lines 16–24 (SessionStart hook #1). Both invoke:
node "$_R/scripts/smart-install.js"scripts/smart-install.js unconditionally:
- Forks
bun --version(line 78) - Forks
uv --version(line 178) - Re-parses
package.jsonand.install-version - Calls
getBunVersion()andgetUvVersion()again (these fork again)
Even on a fully installed system, this is 8+ process forks on every SessionStart just for install-verification. On Windows, where bash -c spawn is inherently 150–500 ms cold, this contributes multiple seconds of blocked startup time.
Fix: Install should run only on Setup (or when .install-version indicates a version mismatch). SessionStart should short-circuit immediately if the install marker is current.
Every hook command in hooks/hooks.json starts with:
export PATH="$($SHELL -lc 'echo $PATH' 2>/dev/null):$PATH"This forks a login shell (-l) on every hook invocation to read .profile/.bashrc/.zshrc just to pick up PATH additions. On Windows bash (MSYS2/Git Bash), login-shell startup is the single most expensive bash operation — easily 300–700 ms.
Per session start: 4 hooks × 1 extra login shell = ~2 seconds wasted just on PATH reads that return the same PATH every time.
Fix: Read $PATH once at Setup, cache it to the plugin's install marker, and have subsequent hooks source that. Or don't re-read PATH at all — claude-mem already controls ~/.bun/bin and ~/.local/bin explicitly and can prepend them without a login shell.
hooks/hooks.json line 10 (Setup hook):
export PATH="$HOME/.nvm/versions/node/v$(ls "$HOME/.nvm/versions/node" 2>/dev/null | sed 's/^v//' | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)/bin:$HOME/.local/bin:/usr/local/bin:/opt/homebrew/bin:$PATH"Problems:
ls ~/.nvm/versions/node | sed 's/^v//'strips thev, thenv$(...)re-adds it — fine, but if the user doesn't use nvm the first path segment becomes$HOME/.nvm/versions/node/v/bin, an invalid directory polluting$PATH. Harmless but sloppy.- This inconsistency between Setup and SessionStart (which uses
$SHELL -lc 'echo $PATH') means PATH differs across hook phases.
Fix: Unify PATH resolution into a single helper shell snippet, or better, do it inside bun-runner.js (which has access to the actual install paths).
hooks/hooks.json line 68. Claude Code hook timeouts are specified in seconds. 2000 seconds = 33 minutes. That's almost certainly a typo for 2000 ms (2 seconds) or for 20 (seconds).
Even if the hook body returns quickly in practice, this is alarming if a future bug causes the script to hang — Claude Code will wait 33 minutes before giving up. Every single Read tool call currently forks the whole claude-mem stack just to record file context.
Fix: Correct the timeout to something bounded like 20 seconds or less, and skip the hook entirely when the worker isn't healthy rather than blocking.
On Windows, enabling the plugin and starting one session spawns at minimum:
| Phase | Spawns |
|---|---|
| Setup hook | 1 bash (hook shell) + 1 ls + 1 node + smart-install.js forks (bun, uv, bun --version, version detection) |
| SessionStart #1 | 1 bash + 1 $SHELL -lc login bash + 1 ls + 1 id + 1 node + all smart-install.js forks again |
| SessionStart #2 | 1 bash + 1 $SHELL -lc login bash + 1 ls + 1 id + 1 node (bun-runner) + 1 cmd /c + 1 bun + 1 node (worker-wrapper) + 1 node (worker-service) + up to 20 curls + up to 20 sleeps |
| SessionStart #3 | Same as #2 minus the worker start |
| UserPromptSubmit | 1 bash + 1 $SHELL -lc + up to 10 curls + 10 sleeps + 1 node + 1 bun |
Total: easily 40+ child processes in the first minute, which maps directly to the user's observation of "8-9 bash processes" (those are just the bash ones visible in Process Explorer; the node/bun/curl/sleep children are hidden or collapsed).
Fix: Collapse the hook commands into a single entry-point script (scripts/hook-entry.js) that handles PATH, install-check, health-check, and dispatch in a single Node process. That drops per-hook overhead from N forks to 1.
scripts/claude-mem is a 61 MB Mach-O 64-bit binary (verified by smart-install.js line 510–541). On Windows and Linux it cannot execute (Exec format error). checkBinaryPlatformCompatibility() only warns about this — it doesn't remove the file or skip it during install.
Ramifications:
- 61 MB disk waste per version
- Full download on every plugin update
- Misleading (plugin directory listing suggests a native binary exists)
Fix: Ship the platform binary via optionalDependencies or postinstall download, not bundled in the plugin tarball. Windows/Linux users have no use for this file.
scripts/bun-runner.js lines 158–163:
setTimeout(() => {
process.stdin.removeAllListeners();
process.stdin.pause();
resolve(chunks.length > 0 ? Buffer.concat(chunks) : null);
}, 5000);If Claude Code ever passes stdin without closing it (which has happened — see #1560 referenced in the code comments), every single hook invocation hangs for 5 seconds before proceeding. Since hooks run serially per SessionStart, this can add 15 seconds by itself.
Fix: Use a much shorter safety timeout (250 ms) — Claude Code either pipes data immediately or not at all; a multi-second window is just a latency hazard.
Every hook computes the port as:
$((37700 + $(id -u 2>/dev/null || echo 77) % 100))On Windows Git Bash, id -u returns MSYS2 UID (often 197121 for regular users), giving port 37721. But:
- If the worker was started by a previous Node-launched install with a different derived port, the health-check hits the wrong port.
iditself is a fork per hook (adds latency).- No validation that the computed port is free before start, no fallback if something else is bound.
Fix: Write the actual bound port to a known file (~/.claude/claude-mem.port) at worker startup; hooks read that file instead of re-deriving.
scripts/worker-wrapper.cjs spawns the inner worker with stdio: ['inherit', 'inherit', 'inherit', 'ipc']. On Windows, inheriting stdio keeps the child tied to the parent's console. If the launching shell (Claude Code hook) exits while the worker is still starting, the worker can be killed by the console-disconnect signal.
The hooks work around this with || true and for i in 1..20; sleep 1 loops, but the underlying issue is the worker isn't using detached: true + unref() or windowsHide with stdio: 'ignore' for daemon mode.
Fix: When invoked with start, spawn the worker-service with { detached: true, stdio: 'ignore' } and child.unref(), then exit the parent immediately. Don't make hooks poll at all.
The plugin tarball includes hooks/bugfixes-2026-01-10.md — an internal sprint-planning document listing critical known bugs like:
- #646: "Plugin bricks Claude Code - stdin fstat EINVAL crash" (marked Critical, unchecked)
- #623: "Crash-recovery loop when memory_session_id not captured" — "Infinite loop consuming API tokens, growing queue unbounded"
- #638: "Worker startup missing JSON output causes hooks to appear stuck"
- #641/#609: "CLAUDE.md files in subdirectories" — "setting exists but doesn't work"
- #635: "JSON parsing error prevents folder context generation"
Several of these directly match the user's symptoms (especially #638 "hooks appear stuck"). Shipping this file in the plugin distribution is unusual — development docs shouldn't be in the production artifact — but more importantly, these are the actual bugs the plugin has, and at least one (#646) is rated by the author's own team as "bricks Claude Code."
Fix: Exclude development docs from the published tarball, AND prioritize fixing the items in that file before the next release.
If needsInstall() returns true mid-session (e.g., plugin version was just updated on disk), the install runs synchronously inside the SessionStart hook:
execSync(`${bunCmd} install`, { cwd: ROOT, stdio: installStdio, shell: IS_WINDOWS });This can take 30–120 seconds on first run for tree-sitter grammars. The session hangs completely during this.
Fix: If install is needed, return a JSON response saying "install in progress, reload when done" and do it in a detached background process. Do not block SessionStart on dependency install.
bun-runner.js checks enabledPlugins['claude-mem@thedotmack'] === false to no-op. But there is no mechanism to say "plugin is enabled but the worker won't start — disable hooks temporarily." So when the worker is broken, every hook invocation pays the full bash-chain + node-startup + bun-spawn cost just to fail at the health check.
Fix: Write a "broken" sentinel file when the worker fails to start after N attempts. Hooks check that sentinel first and exit fast. Clear the sentinel on plugin reinstall or manual reset.
If the user runs /clear or /compact during a session (both of which fire SessionStart again per the matcher startup|clear|compact), the full install + worker-start + health-check + context-generate sequence runs again. There's no short-circuit on "we already did this for this session."
Fix: Record session-start completion in the worker; hooks check "has this session been initialized in the last 5 minutes?" and skip if so.
Many hooks end with || true or || ...echo JSON, which silently swallows hook failures. The plugin never surfaces why it failed. Users see "Claude Code is slow" with no feedback.
Fix: Structured logging to ~/.claude/logs/claude-mem-hooks.log with a timestamp, hook name, duration, and exit code.
scripts/worker-wrapper.cjs is shipped as a minified one-liner (line 2 is ~2 KB of packed code). This makes debugging impossible for end users. worker-service.cjs is 1.9 MB and likely also minified/bundled.
Fix: Ship source maps or at minimum keep wrapper scripts readable — they're small enough that minification buys nothing.
smart-install.js installCLI() checks markerPath to skip, but the outer main function always runs installCLI() unconditionally at line 633. Minor, but indicative of a larger pattern: every early-exit is local, nothing short-circuits the whole script.
Fix: Top-level if (fastPathOK()) { emit JSON success; exit; } before any of the step-by-step checks run.
| # | Severity | Issue | Direct User Impact |
|---|---|---|---|
| 1 | Critical | UserPromptSubmit blocks 10s on health poll |
UI frozen when typing |
| 2 | Critical | SessionStart triple-hook with 20s polls each |
~40s hang on session start |
| 3 | Critical | PostToolUse matcher * forks per tool call |
Sluggish during tool-heavy turns |
| 4 | Critical | smart-install.js re-runs on every session |
+5–15s per startup |
| 5 | High | Every hook forks a login shell for PATH | +~2s per session start |
| 6 | High | Setup hook PATH construction is platform-fragile | Latent PATH breakage |
| 7 | High | PreToolUse Read timeout = 2000s (typo?) |
Potential 33-min hang |
| 8 | High | 40+ processes per first minute | Matches user's "8-9 bash" observation |
| 9 | High | 61 MB macOS-only binary in cross-platform tarball | Disk/network waste |
| 10 | High | 5s stdin-collect timeout | +5s per hang |
| 11 | Medium | Fragile port-derivation from id -u |
Worker lost after reinstall |
| 12 | Medium | Worker daemon not properly detached on Windows | Worker dies with shell |
| 13 | Medium | Ships own open-bug list in tarball | Acknowledges #638 matches our symptom |
| 14 | Medium | bun install can run synchronously mid-session |
Minutes-long session hangs |
| 15 | Medium | No fast-fail when worker is known broken | Full hook chain per broken run |
| 16 | Medium | Not idempotent on /clear / /compact |
Re-runs full startup |
| 17 | Low | ` | |
| 18 | Low | Minified worker-wrapper.cjs |
Hard to debug |
| 19 | Low | Duplicate install-marker logic | Wasted work |
If the author wants to unblock users fast without a full rewrite:
- Delete the polling loops in
UserPromptSubmitandSessionStarthooks #2 and #3. Replace with a singlecurl --max-time 0.2 -sf ... || truethat gives up immediately. A slow context-injection is infinitely better than a frozen UI. - Remove
smart-install.jsfrom theSessionStartchain. Setup runs it once — that's enough. - Make
PostToolUsematcher-restricted (e.g., onlyEdit|Write|Bash) instead of*, and pipe to an HTTP endpoint withcurl --max-time 0.1fire-and-forget. - Remove the
$SHELL -lc 'echo $PATH'prefix from every hook. Use a cached PATH written at Setup time. - Correct the
PreToolUsetimeout from2000to20. - Delete the 61 MB macOS-only binary from the Windows/Linux plugin tarball.
These six changes alone should take typical session startup from ~60s back to <2s on Windows.