Skip to content

fix(plugin): restore venv-aware Python resolution for hooks + MCP#1115

Open
jphein wants to merge 1 commit intoMemPalace:developfrom
jphein:fix/claude-plugin-venv-aware-python
Open

fix(plugin): restore venv-aware Python resolution for hooks + MCP#1115
jphein wants to merge 1 commit intoMemPalace:developfrom
jphein:fix/claude-plugin-venv-aware-python

Conversation

@jphein
Copy link
Copy Markdown
Collaborator

@jphein jphein commented Apr 22, 2026

Summary

Three recent .claude-plugin/ changes regressed hook and MCP invocation for installs that depend on a local venv:

  • 5fe0c1cmempal-stop-hook.sh → PATH-only (command -v mempalace / bare python3 -m mempalace)
  • be9214amempal-precompact-hook.sh → same regression
  • 9f5b8f5.mcp.json → bare "mempalace-mcp" command

All three assume a system-wide install via pipx/uv/pip-global — fine for that path, but they break:

  1. Editable dev installs (pip install -e . inside a repo venv). mempalace / mempalace-mcp and the module only live in the repo venv, not on PATH. The stop/precompact hooks fall through all three checks and print:

    MemPalace hook error: could not find a runnable mempalace command or module
    

    MCP fails to spawn because bare mempalace-mcp isn't on PATH.

  2. Claude Code plugin installs that create a per-plugin venv at ${CLAUDE_PLUGIN_ROOT}/venv/. Neither the hooks nor .mcp.json look there, so anything that isn't also system-installed breaks.

Fix

Restore the three-tier resolution:

  1. MEMPALACE_PYTHON env var (user override)
  2. $PLUGIN_ROOT/venv/bin/python3 (covers dev installs AND Claude Code's per-plugin venv)
  3. System python3 (pip --user / pipx / uv as system install — preserves the original intent of the regressing commits)

.mcp.json goes back to ${CLAUDE_PLUGIN_ROOT}/venv/bin/python -m mempalace.mcp_server. ${CLAUDE_PLUGIN_ROOT} is expanded by Claude Code before spawn, so this works for dev installs (via venv symlink) and packaged plugin installs alike.

Test plan

  • Editable dev install (pip install -e . in repo venv): both hooks return {} from a neutral cwd; MCP initialize handshake returns serverInfo = {name: mempalace, version: 3.3.1}; tools/list returns all 29 tools.
  • Fallback to system python3: confirmed by inspection — if neither MEMPALACE_PYTHON nor $PLUGIN_ROOT/venv/bin/python3 resolves, the hook executes the same python3 -m mempalace hook run ... invocation the regressing commits used.
  • ${CLAUDE_PLUGIN_ROOT}/venv/ packaged install path — works by construction (Claude Code's plugin manager creates the venv at that exact location), but not re-tested in this PR.

🤖 Generated with Claude Code

The stop/precompact hooks and .mcp.json switched to PATH-only lookups
(`command -v mempalace` / bare `mempalace-mcp`) in recent updates.
That works when mempalace is installed system-wide (pipx/uv register
the CLI on PATH and put the module on the system python) but breaks
two install paths that depend on a local venv:

1. Editable dev installs (`pip install -e .` inside ./venv/) —
   `mempalace` / `mempalace-mcp` and the module only live in the repo
   venv, not on PATH. Hooks fall through all three checks and print:
     "MemPalace hook error: could not find a runnable mempalace
      command or module"
   MCP fails to spawn because `mempalace-mcp` is not on PATH.

2. Claude Code plugin installs that create a per-plugin venv at
   `${CLAUDE_PLUGIN_ROOT}/venv/` — neither the hooks nor .mcp.json
   look there, so users whose system python doesn't have mempalace
   get the same errors.

Restore the three-tier resolution used prior to the regression:

  1. MEMPALACE_PYTHON env var (user override)
  2. $PLUGIN_ROOT/venv/bin/python3 (dev installs AND Claude Code's
     per-plugin venv)
  3. system python3 (pip --user / pipx / uv as system install)

.mcp.json goes back to `${CLAUDE_PLUGIN_ROOT}/venv/bin/python` with
`-m mempalace.mcp_server`. `${CLAUDE_PLUGIN_ROOT}` is expanded by
Claude Code before spawn, so this works for dev installs (via venv
symlink) and packaged plugin installs (per-plugin venv) alike.

Verified against an editable dev install: both hooks return `{}` from
a neutral cwd, MCP initialize handshake returns
`serverInfo={name: mempalace, version: 3.3.1}` and tools/list returns
all 29 tools.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jphein jphein requested a review from bensig as a code owner April 22, 2026 20:12
Copilot AI review requested due to automatic review settings April 22, 2026 20:12
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Restores venv-aware invocation for MemPalace’s Claude Code hooks and MCP server startup to support installs where mempalace isn’t available system-wide on PATH (e.g., per-plugin venv).

Changes:

  • Update stop/precompact hook scripts to resolve a Python interpreter via MEMPALACE_PYTHON, plugin venv, then system python3.
  • Switch MCP server launch in .claude-plugin/.mcp.json to use the plugin venv Python with -m mempalace.mcp_server.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
.claude-plugin/hooks/mempal-stop-hook.sh Adds interpreter resolution logic and runs hook via python -m mempalace ....
.claude-plugin/hooks/mempal-precompact-hook.sh Same interpreter resolution change for precompact hook.
.claude-plugin/.mcp.json Launch MCP server via ${CLAUDE_PLUGIN_ROOT}/venv/bin/python -m mempalace.mcp_server.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +19 to +20
INPUT=$(cat)
echo "$INPUT" | "$PYTHON" -m mempalace hook run --hook precompact --harness claude-code
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hook currently reads all stdin into INPUT=$(cat) and then pipes it back via echo. This can change the payload (trailing newlines are stripped by command substitution; echo is not safe for arbitrary content) and unnecessarily buffers the entire request in memory. Prefer streaming stdin directly into the Python invocation so JSON is preserved exactly.

Suggested change
INPUT=$(cat)
echo "$INPUT" | "$PYTHON" -m mempalace hook run --hook precompact --harness claude-code
"$PYTHON" -m mempalace hook run --hook precompact --harness claude-code

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +20
if [ -n "$MEMPALACE_PYTHON" ] && [ -x "$MEMPALACE_PYTHON" ]; then
PYTHON="$MEMPALACE_PYTHON"
elif [ -x "$PLUGIN_ROOT/venv/bin/python3" ]; then
PYTHON="$PLUGIN_ROOT/venv/bin/python3"
else
PYTHON="python3"
fi
INPUT=$(cat)
echo "$INPUT" | "$PYTHON" -m mempalace hook run --hook stop --harness claude-code
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous implementation emitted a clear, consistent error when no runnable mempalace command/module was available. With the current logic, if python3 is missing (or if the selected interpreter can't import mempalace), the script will fail with a generic shell/Python error and may not be obvious to users how to fix it. Consider restoring explicit checks (e.g., command -v and a quick -c "import mempalace") and printing the hook-specific error message to stderr before exiting non-zero.

Copilot uses AI. Check for mistakes.
PYTHON="$PLUGIN_ROOT/venv/bin/python3"
else
PYTHON="python3"
fi
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script no longer verifies that the resolved interpreter exists on PATH and can import mempalace before invoking it. If python3 isn't installed (or the chosen interpreter lacks the package), the hook will error with a generic shell/Python message rather than the prior explicit "could not find a runnable mempalace command or module" guidance. Consider adding back explicit checks and a clear stderr error before exiting.

Suggested change
fi
fi
if [[ "$PYTHON" == */* ]]; then
if [ ! -x "$PYTHON" ]; then
echo "mempal-precompact-hook: could not find a runnable mempalace command or module" >&2
exit 1
fi
elif ! command -v "$PYTHON" >/dev/null 2>&1; then
echo "mempal-precompact-hook: could not find a runnable mempalace command or module" >&2
exit 1
fi
if ! "$PYTHON" -c 'import mempalace' >/dev/null 2>&1; then
echo "mempal-precompact-hook: could not find a runnable mempalace command or module" >&2
exit 1
fi

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +20
INPUT=$(cat)
echo "$INPUT" | "$PYTHON" -m mempalace hook run --hook stop --harness claude-code
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hook currently slurps all stdin into a shell variable and re-emits it via echo. Command substitution strips trailing newlines and echo can subtly transform input, which can corrupt the JSON payload that mempalace hook run expects. Prefer passing stdin through directly to the Python process (no intermediate buffering) so the payload is preserved and large inputs don't get loaded into memory.

Suggested change
INPUT=$(cat)
echo "$INPUT" | "$PYTHON" -m mempalace hook run --hook stop --harness claude-code
"$PYTHON" -m mempalace hook run --hook stop --harness claude-code

Copilot uses AI. Check for mistakes.
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.

2 participants