Skip to content

fix(hooks): real python-resolution for .sh hooks, with MEMPAL_PYTHON override#833

Merged
igorls merged 1 commit intodevelopfrom
fix/hooks-python-resolution
Apr 21, 2026
Merged

fix(hooks): real python-resolution for .sh hooks, with MEMPAL_PYTHON override#833
igorls merged 1 commit intodevelopfrom
fix/hooks-python-resolution

Conversation

@igorls
Copy link
Copy Markdown
Collaborator

@igorls igorls commented Apr 13, 2026

Summary

The legacy shell hooks (mempal_save_hook.sh, mempal_precompact_hook.sh) call python3 -m mempalace mine … directly. On macOS GUI launches of Claude Code, PATH comes from launchd (/usr/bin:/bin:/usr/sbin:/sbin), which usually does not contain the Python where the user installed mempalace. 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

  1. MEMPAL_PYTHON override. Users whose GUI-launch PATH lacks the right interpreter can export MEMPAL_PYTHON=/path/to/venv/python and hooks pick it up first. Resolution order:

    • $MEMPAL_PYTHON if set and executable
    • $(command -v python3) on PATH
    • bare python3 as a last resort
  2. Import 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.log and skips the ingest — preserving the Claude Code contract of returning 0.

  3. Applied consistently. The JSON parse and transcript-count calls use the same resolution, so MEMPAL_PYTHON consistently controls the whole script.

  4. README Known Limitations — documents the macOS GUI PATH behavior and the MEMPAL_PYTHON workaround.

  5. 5 regression tests in tests/test_hooks_shell.py:

    • MEMPAL_PYTHON override wins over PATH (proven via marker-emitting shim).
    • Non-executable MEMPAL_PYTHON falls back to PATH rather than crashing.
    • Unset MEMPAL_PYTHON resolves via PATH.
    • Both hooks exit 0 and log the warning when the resolved interpreter can't import mempalace.
    • Tests skip on Windows (POSIX-only bash scripts). Full suite 810/810 locally; new tests ~0.2s total.

Why the original fix wasn't enough

The abandoned fix/hook-bugs branch replaced python3 with \$(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 via mempalace hook run …) already uses sys.executable and is trivially correct. No changes there.
  • Users who are already happy with bare python3 (most Linux and CLI-launched setups) need to do nothing — the fallback chain is unchanged for them.
  • Users hitting the macOS GUI issue now have a documented, testable escape hatch.

Co-Authored-By: MSL 232237854+milla-jovovich@users.noreply.github.com

@sha2fiddy
Copy link
Copy Markdown
Contributor

+1 — hit this today with uv tool install. Three failure modes in one session, all arguing for an explicit invocation path over auto-detection:

1. CWD-dependent silent failure. User has uv tool install mempalace plus a shell rc auto-activating a sibling project's .venv. The venv lacks mempalace, so python3 -m mempalace fails — but it appears to work when CWD puts the mempalace source on sys.path (e.g. ~/Repos/mempalace). Same user, same install, different repo → different outcome.

2. No module named mempalace.__main__ variant. A stale editable install (ours came from a Claude Code subagent's deleted worktree) leaves .pth resolving but __main__.py missing. You get the above error instead of the cleaner ModuleNotFoundError, which throws diagnosis off.

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 onnxruntime has a broken CoreML cache. Direct MCP calls kept working because the MCP server runs in uv-tool's isolated env. The hook needs the specific Python + site-packages that owns the vetted mempalace, not just a python.

Design thoughts:

Related: #545, #408, #650; sibling PRs #325, #410, #670.

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>
@igorls igorls force-pushed the fix/hooks-python-resolution branch from 6c2840c to 48eb627 Compare April 21, 2026 04:43
Copilot AI review requested due to automatic review settings April 21, 2026 04:43
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

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 to mempal_save_hook.sh and mempal_precompact_hook.sh.
  • Add a new pytest integration test file for validating shell-hook Python resolution behavior.
  • Document the MEMPAL_PYTHON override in hooks/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.

Comment thread hooks/README.md
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.
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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.

Copilot uses AI. Check for mistakes.
Comment thread tests/test_hooks_shell.py
Comment on lines +4 to +13
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.
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread tests/test_hooks_shell.py
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"),
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

_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'].

Suggested change
"PATH": os.environ.get("PATH", "/usr/bin:/bin"),
# Use a deterministic minimal PATH so hook resolution is hermetic.
"PATH": "/usr/bin:/bin",

Copilot uses AI. Check for mistakes.
Comment thread hooks/mempal_save_hook.sh
Comment on lines +75 to +78
# 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)"
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
# 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

Copilot uses AI. Check for mistakes.
if [ -z "$MEMPAL_PYTHON_BIN" ] || [ ! -x "$MEMPAL_PYTHON_BIN" ]; then
MEMPAL_PYTHON_BIN="$(command -v python3 2>/dev/null || echo python3)"
fi

Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
@igorls igorls merged commit 1b00f93 into develop Apr 21, 2026
10 checks passed
@igorls igorls deleted the fix/hooks-python-resolution branch April 21, 2026 04:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/hooks Claude Code hook scripts (Stop, PreCompact, SessionStart) bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants