Files
mempalace/hooks
Igor Lins e Silva 48eb6271a7 fix(hooks): MEMPAL_PYTHON override for .sh hooks' internal python3 calls
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>
2026-04-21 01:43:08 -03:00
..

MemPalace Hooks — Auto-Save for Terminal AI Tools

These hook scripts make MemPalace save automatically. No manual "save" commands needed.

What They Do

Hook When It Fires What Happens
Save Hook Every 15 human messages Blocks the AI, tells it to save key topics/decisions/quotes to the palace
PreCompact Hook Right before context compaction Emergency save — forces the AI to save EVERYTHING before losing context

The AI does the actual filing — it knows the conversation context, so it classifies memories into the right wings/halls/closets. The hooks just tell it WHEN to save.

Install — Claude Code

Add to .claude/settings.local.json:

{
  "hooks": {
    "Stop": [{
      "matcher": "*",
      "hooks": [{
        "type": "command",
        "command": "/absolute/path/to/hooks/mempal_save_hook.sh",
        "timeout": 30
      }]
    }],
    "PreCompact": [{
      "hooks": [{
        "type": "command",
        "command": "/absolute/path/to/hooks/mempal_precompact_hook.sh",
        "timeout": 30
      }]
    }]
  }
}

Make them executable:

chmod +x hooks/mempal_save_hook.sh hooks/mempal_precompact_hook.sh

Install — Codex CLI (OpenAI)

Add to .codex/hooks.json:

{
  "Stop": [{
    "type": "command",
    "command": "/absolute/path/to/hooks/mempal_save_hook.sh",
    "timeout": 30
  }],
  "PreCompact": [{
    "type": "command",
    "command": "/absolute/path/to/hooks/mempal_precompact_hook.sh",
    "timeout": 30
  }]
}

Configuration

Edit mempal_save_hook.sh to change:

  • SAVE_INTERVAL=15 — How many human messages between saves. Lower = more frequent saves, higher = less interruption.
  • STATE_DIR — Where hook state is stored (defaults to ~/.mempalace/hook_state/)
  • MEMPAL_DIR — Optional. Set to a conversations directory to auto-run mempalace mine <dir> on each save trigger. Leave blank (default) to let the AI handle saving via the block reason message.

mempalace CLI

The relevant commands are:

mempalace mine <dir>               # Mine all files in a directory
mempalace mine <dir> --mode convos # Mine conversation transcripts only

The hooks resolve the repo root automatically from their own path, so they work regardless of where you install the repo.

How It Works (Technical)

Save Hook (Stop event)

User sends message → AI responds → Claude Code fires Stop hook
                                            ↓
                                    Hook counts human messages in JSONL transcript
                                            ↓
                              ┌─── < 15 since last save ──→ echo "{}" (let AI stop)
                              │
                              └─── ≥ 15 since last save ──→ {"decision": "block", "reason": "save..."}
                                                                    ↓
                                                            AI saves to palace
                                                                    ↓
                                                            AI tries to stop again
                                                                    ↓
                                                            stop_hook_active = true
                                                                    ↓
                                                            Hook sees flag → echo "{}" (let it through)

The stop_hook_active flag prevents infinite loops: block once → AI saves → tries to stop → flag is true → we let it through.

PreCompact Hook

Context window getting full → Claude Code fires PreCompact
                                        ↓
                                Hook ALWAYS blocks
                                        ↓
                                AI saves everything
                                        ↓
                                Compaction proceeds

No counting needed — compaction always warrants a save.

Debugging

Check the hook log:

cat ~/.mempalace/hook_state/hook.log

Example output:

[14:30:15] Session abc123: 12 exchanges, 12 since last save
[14:35:22] Session abc123: 15 exchanges, 15 since last save
[14:35:22] TRIGGERING SAVE at exchange 15
[14:40:01] Session abc123: 18 exchanges, 3 since last save

Known Limitations

Hooks require session restart after install. Claude Code loads hooks from settings.json at session start only. If you run mempalace init or manually edit hook config mid-session, the hooks won't fire until you restart Claude Code. This is a Claude Code limitation.

MEMPAL_PYTHON override for the hook's internal Python calls. The save hook parses its JSON input and counts transcript messages with python3. When the harness is launched from a GUI on macOS — open -a, Spotlight, the dock — its PATH is the minimal /usr/bin:/bin:/usr/sbin:/sbin inherited from launchd, not your shell PATH. If python3 isn't on that PATH, those internal calls fail and the hook can't count exchanges.

Point the hook at any Python 3 interpreter to fix it:

export MEMPAL_PYTHON="/usr/bin/python3"                   # system Python is fine
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.

Note: the mempalace mine auto-ingest runs via the mempalace CLI, so that command also needs to be on the hook's PATH. Installing with pipx install mempalace or uv tool install mempalace puts it on a stable global location; otherwise extend the hook environment's PATH to include your venv's bin/.

Cost

Zero extra tokens. The hooks notify the AI that saves happened in the background — the AI doesn't need to write anything in the chat. All filing is handled automatically. Previous versions asked the AI to write diary entries and drawer content in the chat window, which cost ~$1/session in retransmitted tokens.