fix(plugin): restore venv-aware Python resolution for hooks + MCP#1115
fix(plugin): restore venv-aware Python resolution for hooks + MCP#1115jphein wants to merge 1 commit intoMemPalace:developfrom
Conversation
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>
There was a problem hiding this comment.
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 systempython3. - Switch MCP server launch in
.claude-plugin/.mcp.jsonto 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.
| INPUT=$(cat) | ||
| echo "$INPUT" | "$PYTHON" -m mempalace hook run --hook precompact --harness claude-code |
There was a problem hiding this comment.
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.
| INPUT=$(cat) | |
| echo "$INPUT" | "$PYTHON" -m mempalace hook run --hook precompact --harness claude-code | |
| "$PYTHON" -m mempalace hook run --hook precompact --harness claude-code |
| 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 |
There was a problem hiding this comment.
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.
| PYTHON="$PLUGIN_ROOT/venv/bin/python3" | ||
| else | ||
| PYTHON="python3" | ||
| fi |
There was a problem hiding this comment.
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.
| 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 |
| INPUT=$(cat) | ||
| echo "$INPUT" | "$PYTHON" -m mempalace hook run --hook stop --harness claude-code |
There was a problem hiding this comment.
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.
| INPUT=$(cat) | |
| echo "$INPUT" | "$PYTHON" -m mempalace hook run --hook stop --harness claude-code | |
| "$PYTHON" -m mempalace hook run --hook stop --harness claude-code |
Summary
Three recent
.claude-plugin/changes regressed hook and MCP invocation for installs that depend on a local venv:5fe0c1c—mempal-stop-hook.sh→ PATH-only (command -v mempalace/ barepython3 -m mempalace)be9214a—mempal-precompact-hook.sh→ same regression9f5b8f5—.mcp.json→ bare"mempalace-mcp"commandAll three assume a system-wide install via pipx/uv/pip-global — fine for that path, but they break:
Editable dev installs (
pip install -e .inside a repo venv).mempalace/mempalace-mcpand the module only live in the repo venv, not on PATH. The stop/precompact hooks fall through all three checks and print:MCP fails to spawn because bare
mempalace-mcpisn't on PATH.Claude Code plugin installs that create a per-plugin venv at
${CLAUDE_PLUGIN_ROOT}/venv/. Neither the hooks nor.mcp.jsonlook there, so anything that isn't also system-installed breaks.Fix
Restore the three-tier resolution:
MEMPALACE_PYTHONenv var (user override)$PLUGIN_ROOT/venv/bin/python3(covers dev installs AND Claude Code's per-plugin venv)python3(pip --user / pipx / uv as system install — preserves the original intent of the regressing commits).mcp.jsongoes 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
pip install -e .in repo venv): both hooks return{}from a neutral cwd; MCPinitializehandshake returnsserverInfo = {name: mempalace, version: 3.3.1};tools/listreturns all 29 tools.python3: confirmed by inspection — if neitherMEMPALACE_PYTHONnor$PLUGIN_ROOT/venv/bin/python3resolves, the hook executes the samepython3 -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