Skip to content

fix(claude-mem-heal): create legacy marketplace junction/symlink (BUG-012)#70

Merged
mlorentedev merged 3 commits into
mainfrom
fix/BUG-012-claude-mem-marketplace-junction
May 21, 2026
Merged

fix(claude-mem-heal): create legacy marketplace junction/symlink (BUG-012)#70
mlorentedev merged 3 commits into
mainfrom
fix/BUG-012-claude-mem-marketplace-junction

Conversation

@mlorentedev
Copy link
Copy Markdown
Owner

@mlorentedev mlorentedev commented May 21, 2026

Summary

  • thedotmack/claude-mem plugin's bundled hooks.json hardcodes the fallback discovery path marketplaces/thedotmack/plugin/scripts/... (the marketplace's declared name), but Claude Code clones the marketplace under the GitHub repo name marketplaces/thedotmack-claude-mem/. When CLAUDE_PLUGIN_ROOT is unset/stale (typical Windows case — plugin cache stays unpopulated), plugin hooks hit the broken fallback, exit 1, and block UserPromptSubmit.
  • Same mismatch silently neutered scripts/claude-mem-heal.{sh,ps1} — both hardcoded marketplaces/thedotmack/plugin, no-op'd on dir-absent, never applied upstream .mcp.json / missing-zod patches to the actual install.
  • Fix: both heal scripts now create a junction (Windows) / symlink (Linux) from marketplaces/thedotmackmarketplaces/thedotmack-claude-mem when the source exists and target is absent. Heal walks extended to cover both legacy and current marketplace paths.

Why this bug only surfaced on Windows

On Linux, Claude Code typically injects CLAUDE_PLUGIN_ROOT correctly at hook invocation, so the broken fallback is never consulted. On Windows the plugin cache (cache/thedotmack/claude-mem/) stays empty, the env var stays unset or stale, and the fallback IS consulted — landing on the non-existent marketplaces/thedotmack/plugin/. The visible Windows symptom (printf: write error: Permission denied) is Git Bash + Claude Code's hook subprocess sandbox quirk; the underlying exit 1 is cross-OS. Fix applied to both heal scripts for parity.

Empirical validation (Windows)

BEFORE: thedotmack exists = False
[claude-mem-heal] created legacy marketplace junction:
  C:\Users\Manu\.claude\plugins\marketplaces\thedotmack -> thedotmack-claude-mem
AFTER: thedotmack exists = True
LinkType: Junction
Target: C:\Users\Manu\.claude\plugins\marketplaces\thedotmack-claude-mem

Second run is silent (exit 0, no output) — silent-on-healthy contract preserved.

Test plan

  • CI bats job green — 4 new asserts in tests/setup-linux.bats + 2 in tests/setup-windows.bats.
  • shellcheck --severity=error scripts/claude-mem-heal.sh clean.
  • pwsh -Command "Invoke-ScriptAnalyzer -Path scripts/claude-mem-heal.ps1 -Severity Error" clean (locally: clean).
  • bash -n scripts/claude-mem-heal.sh clean (locally: clean).
  • PowerShell AST parse of claude-mem-heal.ps1 (locally: clean).
  • Linux empirical: pending — no Linux test machine in this session; logic mirrors PowerShell with identical guards.

Discovered during

BUG-011 hook-failure diagnosis (PR #69). Sibling fix — same plugin, different layer.

Spec

specs/BUG-012-claude-mem-marketplace-junction/{proposal,tasks,verification}.md

mlorentedev and others added 3 commits May 20, 2026 19:41
…-012)

The thedotmack/claude-mem plugin's bundled hooks.json hardcodes the fallback
discovery path `marketplaces/thedotmack/plugin/scripts/...` (the marketplace's
declared `name`), but Claude Code clones the marketplace under the GitHub repo
name `marketplaces/thedotmack-claude-mem/`. When CLAUDE_PLUGIN_ROOT is unset or
stale (the typical Windows case where the plugin cache stays unpopulated),
plugin hooks fall through to that path and fail with `claude-mem: plugin
scripts not found` -> exit 1, blocking UserPromptSubmit. On Windows the
visible symptom is `printf: write error: Permission denied` (Git Bash + hook
subprocess sandbox quirk); on Linux it would fail more cleanly with the
not-found message.

Same root cause silently neuters scripts/claude-mem-heal.{sh,ps1}: both
hardcoded `marketplaces/thedotmack/plugin` -> dir absent -> heal no-ops.
Upstream .mcp.json / missing-zod patches never reach the actual install.

Fix (both heal scripts):
- New ensure_marketplace_compat_symlink() / Repair-MarketplaceCompatJunction
  function. Creates symlink (Linux) / Junction (Windows, no admin) from
  `marketplaces/thedotmack` -> `marketplaces/thedotmack-claude-mem` when the
  source exists and the target is absent. Idempotent: skip otherwise.
- Extend marketplace healing walks to cover both legacy and current path
  variants (path-aware backstop in case junction creation fails).
- Bats parity assertions in tests/setup-{linux,windows}.bats lock the
  junction-creation block in both heal scripts.

Empirically validated on Windows: junction created on first run (LinkType
= Junction, Target = thedotmack-claude-mem), silent on second run (exit 0,
no output) -- contract preserved. Linux side: bash -n + manual review only;
no Linux test machine available this session.

Specs: specs/BUG-012-claude-mem-marketplace-junction/{proposal,tasks,verification}.md

Discovered during BUG-011 hook-failure diagnosis. Sibling of PR #69.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
Two failures in CI test job for PR #70:

1. `grep -qF '-ItemType Junction'` -> GNU grep parses `-ItemType` as flag set
   before checking the pattern. Fix: add `--` separator before the pattern.
   Same convention used by the existing `--ignore-scripts` assert above.
2. `grep -B2 -A6 'ln -s.*thedotmack-claude-mem'` -> the literal `ln -s` line
   in claude-mem-heal.sh is `ln -s "$actual" "$legacy"` (no marketplace name
   on that line; the name is in the earlier variable assignment). The
   regex never matched, so `-B2 -A6` returned empty and the piped grep
   failed. Rewrite to grep the actual guard expressions:
   `[ ! -d "$actual" ]` (source-exists check) and
   `[ -e "$legacy" ]` (target-absent check) -- both required for the
   idempotence contract.

All four patterns verified locally against the heal scripts.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
@mlorentedev mlorentedev merged commit 3009b21 into main May 21, 2026
6 checks passed
@mlorentedev mlorentedev deleted the fix/BUG-012-claude-mem-marketplace-junction branch May 21, 2026 01:55
mlorentedev added a commit that referenced this pull request May 21, 2026
… pattern (cross-OS) (#83)

User encountered `/mcp Failed to reconnect to plugin:claude-mem:mcp-search: -32000`
in this session AFTER BUG-014 (PR #75) restored the install AND BUG-015
(PR #81) shipped the detection layer. Root cause traced to claude-mem-heal
silently no-oping against v13.3.0 installs.

scripts/claude-mem-heal.{sh,ps1}::Repair-McpJson was authored in PR #57
against v12.7.4's broken pattern (literal `${_R%/}`). v13.0.0+ ships a
different broken pattern: `sh -c` with cascading-printf pipe that
triggers the same EPIPE race documented in
thedotmack/claude-mem#2607. The original detection regex (`${_R%/}`)
doesn't match v13.x at all -> heal exits silent, leaving the .mcp.json
broken -> MCP server fails to start -> -32000.

Changes:

1. scripts/claude-mem-heal.sh::heal_mcp_json:
   - Detection extended: triggers on EITHER v12.7.4 `${_R%/}` OR
     v13.x signature (`"sh".*"-c"` OR `while IFS= read`).
   - Replacement template: canonical race-free form using
     `done | head -n1` instead of `done` with inner `break`.
     This consumes the entire producer pipe (no leftover writes ->
     no EPIPE -- Option A from upstream issue #2607 applied locally).
   - Log message distinguishes which signature was patched.

2. scripts/claude-mem-heal.ps1::Repair-McpJson: equivalent on Windows.

3. tests/setup-linux.bats: 3 new asserts:
   - both heal scripts grep `while IFS=` (v13.x signature)
   - both heal scripts contain `head -n1` (race-free template)
   - both reference BUG-016 + claude-mem#2607

Empirical validation on user's daily-driver Windows during implementation:
3 affected .mcp.json files (cache 13.3.0 + marketplace junction +
marketplace canonical) patched on first heal run (exit 0). Second run
silent (idempotent -- no v12/v13 signature left to detect).

Spec at specs/BUG-016-claude-mem-heal-v13-refresh/.

Diff: +63 prod (heal scripts) + 28 test = 91 LOC. Production diff at
spec-gate threshold; spec folder ships per discipline.

Lesson candidate (post-merge): "heal scripts must be versioned against
the upstream bug class they paper over; when upstream's bug pattern
changes, the heal's detection regex MUST be refreshed in the same PR
that discovers the new pattern."

Companion to BUG-015 (PR #81 detection layer) and BUG-012 (PR #70
junction fix). Pairs with upstream issue thedotmack/claude-mem#2607
where Option A `head -n1` is recommended as the canonical upstream fix
for the same pipe race.
mlorentedev added a commit that referenced this pull request May 21, 2026
Move from specs/ to specs/archive/ per SDD lifecycle close (the
folder move IS the archive marker; status: archived frontmatter
update deferred to per-spec follow-up if needed).

This session shipped (today, 2026-05-21):
  - AI-014-opencode-windows-bootstrap (PR #78)
  - BUG-014-claude-mem-marketplace-register (PR #75)
  - BUG-016-claude-mem-heal-v13-refresh (PR #83)
  - BUG-017-claude-mem-heal-hooks-json-race (PR #84)
  - BUG-018-userpromptsubmit-continue-directive (PR #85)
  - REFACTOR-003-diff-check-ps1 (PR #82)

Catch-up archive (merged earlier weeks but specs/ folder lingered):
  - BUG-007-remove-github-plugin-broken (PR #65, 2026-05-19)
  - BUG-011-mcp-loop-claude-json-guard (PR #69, 2026-05-20)
  - BUG-012-claude-mem-marketplace-junction (PR #70, 2026-05-20)
  - SDD-005-github-copilot-instructions-sync (PR #62, 2026-05-19)
  - SDD-006-vault-integrity-check (PR #63, 2026-05-19)

Active specs remaining in specs/ (not yet merged):
  - REFACTOR-002-paths-in-env-contract (queued, still draft)
  - WIN-002-windows-smoke-sweep (partial closure via PR #73, full
    clean-VM sweep still open)

33 file moves total (3 files per spec × 11 specs). Zero content change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant