Skip to content

Instantly share code, notes, and snippets.

@futuremotiondev
Created April 21, 2026 04:08
Show Gist options
  • Select an option

  • Save futuremotiondev/a5cc12881828ec5caa031579f9691254 to your computer and use it in GitHub Desktop.

Select an option

Save futuremotiondev/a5cc12881828ec5caa031579f9691254 to your computer and use it in GitHub Desktop.
Current catastrophic bugs with thedotmack/claude-mem on Windows. Broken hooks, 8+ process forks on every start, etc.

claude-mem Plugin — Bug Report

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/

TLDR

Here's the short version of what's wrong:

Root cause of the ~1-minute freeze and 8-9 bash processes:

  1. 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.
  2. 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.
  3. 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.
  4. smart-install.js runs on every SessionStart, not just once. Forks bun, uv, version checks each time.
  5. PostToolUse has matcher * — every single tool call forks the whole Node→bun→worker chain. That's the post-startup sluggishness.
  6. PreToolUse Read timeout is 2000 — likely a seconds/ms typo (Claude Code hook timeouts are in seconds, so that's 33 minutes).
  7. Ships a 61 MB macOS-only binary that can't run on Windows.
  8. 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.


Critical Issues

1. UserPromptSubmit hook blocks every prompt for up to 10 seconds

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-init

Why 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
done

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


3. PostToolUse hook with matcher * fires on EVERY tool call

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.


4. smart-install.js runs on BOTH Setup AND every SessionStart

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.json and .install-version
  • Calls getBunVersion() and getUvVersion() 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.


5. Every hook forks a login shell just to read $PATH

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.


6. Setup hook export PATH is broken on Linux/macOS

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 the v, then v$(...) 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).


7. PreToolUse hook on Read has a "timeout": 2000

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.


High-Priority Issues

8. Process count per session start is unreasonable

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.


9. Bundled 61 MB macOS-arm64 binary shipped to Windows/Linux users

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.


10. bun-runner.js has a 5-second stdin-collection timeout

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.


11. Worker health-check port derivation is fragile

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.
  • id itself 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.


12. Worker daemon is not detached properly on Windows

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.


Medium-Priority Issues

13. hooks/bugfixes-2026-01-10.md ships known open bugs to users

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.


14. smart-install.js installDeps() can run during a live session

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.


15. No short-circuit when plugin is known to be broken

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.


16. Hook commands are not idempotent on re-entry

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.


Low-Priority / Code Quality

17. Inconsistent use of || fallbacks

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.


18. worker-wrapper.cjs is minified

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.


19. Duplicate .cli-installed marker logic doesn't prevent re-runs

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.


Summary Table

# 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

Suggested Minimum Fix for the Reported Hang

If the author wants to unblock users fast without a full rewrite:

  1. Delete the polling loops in UserPromptSubmit and SessionStart hooks #2 and #3. Replace with a single curl --max-time 0.2 -sf ... || true that gives up immediately. A slow context-injection is infinitely better than a frozen UI.
  2. Remove smart-install.js from the SessionStart chain. Setup runs it once — that's enough.
  3. Make PostToolUse matcher-restricted (e.g., only Edit|Write|Bash) instead of *, and pipe to an HTTP endpoint with curl --max-time 0.1 fire-and-forget.
  4. Remove the $SHELL -lc 'echo $PATH' prefix from every hook. Use a cached PATH written at Setup time.
  5. Correct the PreToolUse timeout from 2000 to 20.
  6. 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.

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