48eb6271a7
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>
81 lines
2.7 KiB
Bash
Executable File
81 lines
2.7 KiB
Bash
Executable File
#!/bin/bash
|
|
# MEMPALACE PRE-COMPACT HOOK — Emergency save before compaction
|
|
#
|
|
# Claude Code "PreCompact" hook. Fires RIGHT BEFORE the conversation
|
|
# gets compressed to free up context window space.
|
|
#
|
|
# This is the safety net. When compaction happens, the AI loses detailed
|
|
# context about what was discussed. This hook forces one final save of
|
|
# EVERYTHING before that happens.
|
|
#
|
|
# Unlike the save hook (which triggers every N exchanges), this ALWAYS
|
|
# blocks — because compaction is always worth saving before.
|
|
#
|
|
# === INSTALL ===
|
|
# Add to .claude/settings.local.json:
|
|
#
|
|
# "hooks": {
|
|
# "PreCompact": [{
|
|
# "hooks": [{
|
|
# "type": "command",
|
|
# "command": "/absolute/path/to/mempal_precompact_hook.sh",
|
|
# "timeout": 30
|
|
# }]
|
|
# }]
|
|
# }
|
|
#
|
|
# For Codex CLI, add to .codex/hooks.json:
|
|
#
|
|
# "PreCompact": [{
|
|
# "type": "command",
|
|
# "command": "/absolute/path/to/mempal_precompact_hook.sh",
|
|
# "timeout": 30
|
|
# }]
|
|
#
|
|
# === HOW IT WORKS ===
|
|
#
|
|
# Claude Code sends JSON on stdin with:
|
|
# session_id — unique session identifier
|
|
#
|
|
# We always return decision: "block" with a reason telling the AI
|
|
# to save everything. After the AI saves, compaction proceeds normally.
|
|
#
|
|
# === MEMPALACE CLI ===
|
|
# This repo uses: mempalace mine <dir>
|
|
# or: mempalace mine <dir> --mode convos
|
|
# Set MEMPAL_DIR below if you want the hook to auto-ingest before compaction.
|
|
# Leave blank to rely on the AI's own save instructions.
|
|
|
|
STATE_DIR="$HOME/.mempalace/hook_state"
|
|
mkdir -p "$STATE_DIR"
|
|
|
|
# Optional: set to the directory you want auto-ingested before compaction.
|
|
# Example: MEMPAL_DIR="$HOME/conversations"
|
|
# Leave empty to skip auto-ingest (AI handles saving via the block reason).
|
|
MEMPAL_DIR=""
|
|
|
|
# Resolve the Python interpreter. Same contract as mempal_save_hook.sh:
|
|
# MEMPAL_PYTHON (explicit override) → $(command -v python3) → bare python3.
|
|
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)"
|
|
fi
|
|
|
|
# Read JSON input from stdin
|
|
INPUT=$(cat)
|
|
|
|
SESSION_ID=$(echo "$INPUT" | "$MEMPAL_PYTHON_BIN" -c "import sys,json; print(json.load(sys.stdin).get('session_id','unknown'))" 2>/dev/null)
|
|
|
|
echo "[$(date '+%H:%M:%S')] PRE-COMPACT triggered for session $SESSION_ID" >> "$STATE_DIR/hook.log"
|
|
|
|
# Optional: run mempalace ingest synchronously so memories land before compaction
|
|
if [ -n "$MEMPAL_DIR" ] && [ -d "$MEMPAL_DIR" ]; then
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_DIR="$(dirname "$SCRIPT_DIR")"
|
|
mempalace mine "$MEMPAL_DIR" >> "$STATE_DIR/hook.log" 2>&1
|
|
fi
|
|
|
|
# Silent: return empty JSON to not block. "decision": "allow" is invalid —
|
|
# only "block" or {} are recognized.
|
|
echo '{}'
|