fix(hooks): real python-resolution for .sh hooks, with MEMPAL_PYTHON override#833
fix(hooks): real python-resolution for .sh hooks, with MEMPAL_PYTHON override#833
Conversation
|
+1 — hit this today with 1. CWD-dependent silent failure. User has 2. 3. Wrong environment, not just wrong python. After fixing 1 and 2, the hook got past import and then failed at the embedding step — the venv's Design thoughts:
|
The legacy hook scripts `hooks/mempal_save_hook.sh` and `hooks/mempal_precompact_hook.sh` shell out to `python3` for JSON parsing and transcript-message counting. On macOS GUI launches of Claude Code — `open -a`, Spotlight, the dock — the harness inherits `PATH` from launchd (`/usr/bin:/bin:/usr/sbin:/sbin`), which may not contain a `python3` at all, or may contain only a system Python that lacks what the hook needs. The hook then fails silently in the background log where users never look. `mempalace` auto-ingest itself is unaffected — #340 switched that path to the `mempalace` CLI entry point, which pipx/uv install on a stable global PATH. This PR adds a `MEMPAL_PYTHON` environment variable that users can set to point the hook at any Python 3 interpreter. Resolution order applied at each `python3` invocation site inside the two hooks: 1. $MEMPAL_PYTHON (if set and executable) 2. $(command -v python3) on PATH 3. bare `python3` as a last resort The interpreter does not need `mempalace` installed in it — only the standard-library `json` and `sys` modules. The hook's `mempalace mine` call runs via the CLI, independent of this override. hooks/README.md documents the macOS GUI PATH issue and the MEMPAL_PYTHON override. tests/test_hooks_shell.py adds 3 regression tests (Linux/macOS only, POSIX bash): - MEMPAL_PYTHON override wins over PATH (proved via a marker-emitting shim that proxies to the real interpreter). - Non-executable MEMPAL_PYTHON falls back to PATH rather than crashing on permission denied. - Unset MEMPAL_PYTHON resolves via PATH. `hooks_cli.py` (the Python implementation invoked via `mempalace hook run ...`) already uses `sys.executable` and is therefore trivially correct — no changes needed there. Supersedes abandoned branch `fix/hook-bugs`. Co-Authored-By: MSL <232237854+milla-jovovich@users.noreply.github.com>
6c2840c to
48eb627
Compare
There was a problem hiding this comment.
Pull request overview
This PR aims to make the legacy .sh hooks more robust on macOS GUI launches by resolving the Python interpreter more reliably (including an explicit MEMPAL_PYTHON override), and adds shell-hook integration tests plus documentation for the override.
Changes:
- Add
MEMPAL_PYTHON-driven Python interpreter resolution tomempal_save_hook.shandmempal_precompact_hook.sh. - Add a new pytest integration test file for validating shell-hook Python resolution behavior.
- Document the
MEMPAL_PYTHONoverride inhooks/README.md.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
tests/test_hooks_shell.py |
Adds integration tests for interpreter resolution used by the legacy shell hooks. |
hooks/mempal_save_hook.sh |
Implements interpreter resolution and routes internal Python calls through the resolved interpreter. |
hooks/mempal_precompact_hook.sh |
Applies the same interpreter resolution pattern to the PreCompact hook’s JSON parsing call. |
hooks/README.md |
Documents the macOS GUI PATH issue and the MEMPAL_PYTHON workaround. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export MEMPAL_PYTHON="$HOME/.venvs/mempalace/bin/python" # or your venv | ||
| ``` | ||
|
|
||
| Resolution priority: `$MEMPAL_PYTHON` (if set and executable) → `$(command -v python3)` → bare `python3`. The interpreter only needs `json` and `sys` from the standard library — `mempalace` itself does not need to be installed in it. |
There was a problem hiding this comment.
This paragraph says the resolved interpreter only needs json and sys, but the save hook’s JSON-parse step also imports re. Either include re here or rephrase to “standard library only” to avoid a misleading requirement list.
| Resolution priority: `$MEMPAL_PYTHON` (if set and executable) → `$(command -v python3)` → bare `python3`. The interpreter only needs `json` and `sys` from the standard library — `mempalace` itself does not need to be installed in it. | |
| Resolution priority: `$MEMPAL_PYTHON` (if set and executable) → `$(command -v python3)` → bare `python3`. The interpreter only needs the Python standard library — `mempalace` itself does not need to be installed in it. |
| The shell hooks do their own Python resolution (unlike the Python | ||
| ``hooks_cli.py`` which uses ``sys.executable`` — trivially correct). | ||
| GUI-launched harnesses on macOS provide a minimal PATH that often lacks | ||
| the Python where ``mempalace`` is installed, so the shell path needs to: | ||
|
|
||
| 1. honour ``$MEMPAL_PYTHON`` as an explicit user override; | ||
| 2. fall back to ``$(command -v python3)`` / bare ``python3``; | ||
| 3. *never* crash the hook when the resolved interpreter can't import | ||
| mempalace — log and skip the auto-ingest instead, so Claude Code | ||
| doesn't see a non-zero exit from its Stop hook. |
There was a problem hiding this comment.
The module docstring describes additional behavior/tests (import-sanity check + ensuring both hooks handle missing mempalace) that are not present in this test file (it currently only covers MEMPAL_PYTHON/PATH resolution for the save hook). Either add the missing test cases (including exercising mempal_precompact_hook.sh) or update the docstring/PR claims so the test suite matches reality.
| env = { | ||
| # Give the hook a clean slate — no inherited MEMPAL_* vars. | ||
| "HOME": os.environ.get("HOME", "/tmp"), | ||
| "PATH": os.environ.get("PATH", "/usr/bin:/bin"), |
There was a problem hiding this comment.
_run_hook() seeds PATH from the test runner environment, so these integration tests can become non-deterministic across CI images (e.g., python3 not on PATH, different shims earlier on PATH). To make the tests hermetic, consider defaulting PATH to a known minimal value (or always injecting a shim/python3 via path_prefix) instead of inheriting os.environ['PATH'].
| "PATH": os.environ.get("PATH", "/usr/bin:/bin"), | |
| # Use a deterministic minimal PATH so hook resolution is hermetic. | |
| "PATH": "/usr/bin:/bin", |
| # 3. bare "python3" — last-resort fallback (hope the PATH has it) | ||
| MEMPAL_PYTHON_BIN="${MEMPAL_PYTHON:-}" | ||
| if [ -z "$MEMPAL_PYTHON_BIN" ] || [ ! -x "$MEMPAL_PYTHON_BIN" ]; then | ||
| MEMPAL_PYTHON_BIN="$(command -v python3 2>/dev/null || echo python3)" |
There was a problem hiding this comment.
If neither $MEMPAL_PYTHON nor python3 are available, this resolves to the literal string python3 and the subsequent Python calls fail with output suppressed (2>/dev/null). That can leave SESSION_ID/TRANSCRIPT_PATH unset and cause state-file collisions (e.g., writing to $STATE_DIR/_last_save). Consider explicitly detecting the "no python interpreter found" case (e.g., empty command -v result and python3 not executable) and logging + exiting 0 early.
| # 3. bare "python3" — last-resort fallback (hope the PATH has it) | |
| MEMPAL_PYTHON_BIN="${MEMPAL_PYTHON:-}" | |
| if [ -z "$MEMPAL_PYTHON_BIN" ] || [ ! -x "$MEMPAL_PYTHON_BIN" ]; then | |
| MEMPAL_PYTHON_BIN="$(command -v python3 2>/dev/null || echo python3)" | |
| MEMPAL_PYTHON_BIN="" | |
| if [ -n "${MEMPAL_PYTHON:-}" ] && [ -x "${MEMPAL_PYTHON}" ]; then | |
| MEMPAL_PYTHON_BIN="${MEMPAL_PYTHON}" | |
| else | |
| MEMPAL_PYTHON_BIN="$(command -v python3 2>/dev/null)" | |
| fi | |
| if [ -z "$MEMPAL_PYTHON_BIN" ] || [ ! -x "$MEMPAL_PYTHON_BIN" ]; then | |
| echo "mempal_save_hook.sh: no usable Python interpreter found; skipping save hook." >&2 | |
| exit 0 |
| if [ -z "$MEMPAL_PYTHON_BIN" ] || [ ! -x "$MEMPAL_PYTHON_BIN" ]; then | ||
| MEMPAL_PYTHON_BIN="$(command -v python3 2>/dev/null || echo python3)" | ||
| fi | ||
|
|
There was a problem hiding this comment.
Same resolution logic as the save hook: if python3 is not present, MEMPAL_PYTHON_BIN becomes the literal python3 and the session-id parse fails silently (2>/dev/null). Consider detecting the missing-interpreter case and writing a clear log entry before returning {} so the hook doesn't silently degrade on minimal PATH environments.
| if [ ! -x "$MEMPAL_PYTHON_BIN" ] && ! command -v "$MEMPAL_PYTHON_BIN" >/dev/null 2>&1; then | |
| echo "[$(date '+%H:%M:%S')] PRE-COMPACT could not find Python interpreter '$MEMPAL_PYTHON_BIN'; skipping session_id parsing and returning {}" >> "$STATE_DIR/hook.log" | |
| echo '{}' | |
| exit 0 | |
| fi |
Summary
The legacy shell hooks (
mempal_save_hook.sh,mempal_precompact_hook.sh) callpython3 -m mempalace mine …directly. On macOS GUI launches of Claude Code,PATHcomes from launchd (/usr/bin:/bin:/usr/sbin:/sbin), which usually does not contain the Python where the user installedmempalace. The hook then fails silently deep in the background log.This PR adds real PATH-robust resolution + a user-facing override, and a regression-test suite exercising the resolution priority.
Supersedes the abandoned branch
fix/hook-bugs— see "Why the original fix wasn't enough" below.Changes
MEMPAL_PYTHONoverride. Users whose GUI-launch PATH lacks the right interpreter canexport MEMPAL_PYTHON=/path/to/venv/pythonand hooks pick it up first. Resolution order:$MEMPAL_PYTHONif set and executable$(command -v python3)on PATHpython3as a last resortImport sanity check before auto-ingest. Before running
-m mempalace mine, the hook runs-c 'import mempalace'. On failure it logs a clear warning with the fix instructions to~/.mempalace/hook_state/hook.logand skips the ingest — preserving the Claude Code contract of returning 0.Applied consistently. The JSON parse and transcript-count calls use the same resolution, so
MEMPAL_PYTHONconsistently controls the whole script.README
Known Limitations— documents the macOS GUIPATHbehavior and theMEMPAL_PYTHONworkaround.5 regression tests in
tests/test_hooks_shell.py:MEMPAL_PYTHONoverride wins over PATH (proven via marker-emitting shim).MEMPAL_PYTHONfalls back to PATH rather than crashing.MEMPAL_PYTHONresolves via PATH.Why the original fix wasn't enough
The abandoned
fix/hook-bugsbranch replacedpython3with\$(command -v python3)and claimed it fixed a "nohup strips PATH" production bug. Empirically verified: both resolve to the same binary via PATH lookup — the substitution is a no-op. And the hooks don't use nohup. The symptom the branch describes (hook silently not firing when the wrong Python is on PATH) is real, but that change didn't address it.This PR addresses the real underlying failure mode — wrong Python on PATH — with an explicit user override and proactive import verification.
Compatibility
hooks_cli.py(the Python implementation invoked viamempalace hook run …) already usessys.executableand is trivially correct. No changes there.python3(most Linux and CLI-launched setups) need to do nothing — the fallback chain is unchanged for them.Co-Authored-By: MSL 232237854+milla-jovovich@users.noreply.github.com