Data
PopularHook
Hooks
What mempalace runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Install
> /plugin marketplace add MemPalace/mempalace > /plugin install mempalace@mempalace
Ships with mempalace. Installing the plugin gets these hooks.
Where it lives
- hooks/mempal_precompact_hook.shGitHub
Read the script
#!/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 gates on a message-count threshold and on # MEMPAL_VERBOSE), this runs the mine synchronously on every PreCompact # event — compaction is always worth saving before. The hook itself # returns ``{}`` so it does not emit a ``decision: block`` to Claude # Code; the "always run" semantics live in the mine call, not in the # Stop-hook block protocol. # # === 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 # transcript_path — path to the JSONL transcript file # # The hook runs the transcript mine synchronously (the foreground # ``mempalace mine`` call below blocks until it returns), then prints # ``{}`` to stdout so Claude Code proceeds with the compaction. We do # not emit a ``decision: block`` to the hook protocol — the # "always save before compaction" guarantee is provided by the # synchronous mine, not by the Stop-hook block contract. # # === MEMPALACE CLI === # The hook ALWAYS mines the active conversation transcript synchronously # before compaction (via `mempalace mine <transcript-dir> --mode convos`). # MEMPAL_DIR is an *additional*, optional target for project files — it # does not replace the conversation mine. STATE_DIR="$HOME/.mempalace/hook_state" mkdir -p "$STATE_DIR" # Optional: project directory (code / notes / docs) to also mine before # compaction. Mined with `--mode projects`. The conversation transcript # is always mined regardless — this is purely additive. # Example: MEMPAL_DIR="$HOME/projects/my_app" 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 # ── Silent mode / opt-out ────────────────────────────────────────────── # Set MEMPALACE_HOOKS_AUTO_SAVE=false to disable auto-save blocking entirely. if [ -n "$MEMPALACE_HOOKS_AUTO_SAVE" ]; then case "$MEMPALACE_HOOKS_AUTO_SAVE" in false|0|no) echo "{}"; exit 0 ;; esac else CONFIG_FILE="$HOME/.mempalace/config.json" if [ -f "$CONFIG_FILE" ]; then AUTO_SAVE=$("$MEMPAL_PYTHON_BIN" -c " import json, sys try: cfg = json.load(open(sys.argv[1])) print(str(cfg.get('hooks', {}).get('auto_save', True)).lower()) except Exception: print('true') " "$CONFIG_FILE" 2>/dev/null) if [ "$AUTO_SAVE" = "false" ]; then echo "{}" exit 0 fi fi fi # Read JSON input from stdin INPUT=$(cat) # Parse session_id and transcript_path in one call. Sanitize both, then # read sanitized values from one-per-line stdout into shell variables # (avoids ``eval`` on generated code, #1231 review). Uses ``sed -n 'Np'`` # rather than the bash 4-only ``mapfile`` so the script also runs on # macOS /bin/bash 3.2.57 (Apple GPLv3 freeze, 2006), where ``mapfile`` # silently caused every parsed value to fall back to its default (#1440). # # The leading ``__MEMPAL_PARSE_OK__`` sentinel lets the defense-in-depth # guard below distinguish "Python parsed cleanly" from "Python crashed # and printed nothing". Same parsing contract as mempal_save_hook.sh. # Python stderr is captured to last_python_err.log so the guard below can # distinguish "bad user input" from "broken interpreter / future regression # in this inline script". Same diagnostic contract as mempal_save_hook.sh. # # ``umask 077`` inside the command-substitution subshell makes the # ``2>$STATE_DIR/last_python_err.log`` redirect create the file at mode # 0600 atomically, closing the TOCTOU window between creation at # umask-default and the ``chmod 600`` below. ``printf '%s'`` replaces # ``echo`` so payloads beginning with ``-n``/``-e``/``-E`` or containing # backslashes are not mangled by echo flag parsing. _mempal_parsed=$( umask 077 printf '%s' "$INPUT" | "$MEMPAL_PYTHON_BIN" -m mempalace.hook_shell parse-precompact \ 2>"$STATE_DIR/last_python_err.log" ) # Drop the empty file on success; chmod 600 on failure to mirror # last_input.log's privacy contract. if [ -s "$STATE_DIR/last_python_err.log" ]; then chmod 600 "$STATE_DIR/last_python_err.log" 2>/dev/null else rm -f "$STATE_DIR/last_python_err.log" fi _MEMPAL_PARSE_MARKER=$(printf '%s\n' "$_mempal_parsed" | sed -n '1p') SESSION_ID=$(printf '%s\n' "$_mempal_parsed" | sed -n '2p') TRANSCRIPT_PATH=$(printf '%s\n' "$_mempal_parsed" | sed -n '3p') SESSION_ID="${SESSION_ID:-unknown}" TRANSCRIPT_PATH="${TRANSCRIPT_PATH:-}" # Defense-in-depth: if INPUT was non-empty but Python never reached the # print() calls (sentinel missing), parsing silently failed. Surface the # raw payload so the next debugger does not lose a day to hook.log lines # that say "Session unknown". Bounded to 4 KB and overwritten on each # failure (not appended) to keep ~/.mempalace/hook_state/ from growing # unbounded under a repeating misconfiguration. chmod 600 so the dump, # which mirrors the Claude Code PreCompact payload (includes # transcript_path revealing the user's home + project layout) - hooks/mempal_save_hook.shGitHub
Read the script
#!/bin/bash # MEMPALACE SAVE HOOK — Auto-save every N exchanges # # Claude Code "Stop" hook. After every assistant response: # 1. Counts human messages in the session transcript # 2. Every SAVE_INTERVAL messages, BLOCKS the AI from stopping # 3. Returns a reason telling the AI to save structured diary + palace entries # 4. AI does the save (topics, decisions, code, quotes → organized into palace) # 5. Next Stop fires with stop_hook_active=true → lets AI stop normally # # The AI does the classification — it knows what wing/hall/closet to use # because it has context about the conversation. No regex needed. # # === INSTALL === # Add to .claude/settings.local.json: # # "hooks": { # "Stop": [{ # "matcher": "*", # "hooks": [{ # "type": "command", # "command": "/absolute/path/to/mempal_save_hook.sh", # "timeout": 30 # }] # }] # } # # For Codex CLI, add to .codex/hooks.json: # # "Stop": [{ # "type": "command", # "command": "/absolute/path/to/mempal_save_hook.sh", # "timeout": 30 # }] # # === HOW IT WORKS === # # Claude Code sends JSON on stdin with these fields: # session_id — unique session identifier # stop_hook_active — true if AI is already in a save cycle (prevents infinite loop) # transcript_path — path to the JSONL transcript file # # When we block, Claude Code shows our "reason" to the AI as a system message. # The AI then saves to memory, and when it tries to stop again, # stop_hook_active=true so we let it through. No infinite loop. # # === MEMPALACE CLI === # The hook ALWAYS mines the active conversation transcript automatically # (via `mempalace mine <transcript-dir> --mode convos`). MEMPAL_DIR is an # *additional*, optional target for project files — it does not replace # the conversation mine. # # === CONFIGURATION === SAVE_INTERVAL=15 # Save every N human messages (adjust to taste) STATE_DIR="$HOME/.mempalace/hook_state" mkdir -p "$STATE_DIR" # Optional: project directory (code / notes / docs) to also mine each # save trigger. Mined with `--mode projects`. The conversation transcript # is always mined regardless — this is purely additive. # Example: MEMPAL_DIR="$HOME/projects/my_app" MEMPAL_DIR="" # Resolve the Python interpreter the hook should use. # # Why this is nontrivial: GUI-launched Claude Code on macOS (or any harness # that doesn't inherit the user's shell PATH) may find a `python3` on PATH # that lacks mempalace — e.g. /usr/bin/python3 while the user installed # mempalace into a venv or pyenv. Users in that situation can point the # hook at the right interpreter by exporting MEMPAL_PYTHON. # # Resolution order (first hit wins): # 1. $MEMPAL_PYTHON — explicit user override (absolute path) # 2. $(command -v python3) — first python3 on the hook's PATH # 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)" fi # ── Silent mode / opt-out ────────────────────────────────────────────── # Set MEMPALACE_HOOKS_AUTO_SAVE=false to disable auto-save blocking entirely. # The hook stays installed but passes through without interrupting the session. # Can also be set in ~/.mempalace/config.json: {"hooks": {"auto_save": false}} if [ -n "$MEMPALACE_HOOKS_AUTO_SAVE" ]; then case "$MEMPALACE_HOOKS_AUTO_SAVE" in false|0|no) echo "{}"; exit 0 ;; esac else # Check config.json if env var is not set CONFIG_FILE="$HOME/.mempalace/config.json" if [ -f "$CONFIG_FILE" ]; then AUTO_SAVE=$("$MEMPAL_PYTHON_BIN" -c " import json, sys try: cfg = json.load(open(sys.argv[1])) print(str(cfg.get('hooks', {}).get('auto_save', True)).lower()) except Exception: print('true') " "$CONFIG_FILE" 2>/dev/null) if [ "$AUTO_SAVE" = "false" ]; then echo "{}" exit 0 fi fi fi # Read JSON input from stdin INPUT=$(cat) # Parse all fields in a single Python call (3x faster than separate invocations) # without invoking ``eval`` on generated code: Python prints a parse-success # sentinel followed by one sanitized value per line, the shell reads each # line via ``sed -n 'Np'`` and does plain variable assignment. Same data, # smaller blast radius if the sanitizer is ever bypassed (#1231 review). # # Why ``sed -n 'Np'`` and not ``mapfile`` / ``readarray``: macOS ships # GNU bash 3.2.57 (frozen at Apple's GPLv3 cutoff in 2006), and both # array-read builtins only landed in bash 4.0 (2009). On a stock macOS # the previous ``mapfile`` form errored, every value fell back to its # default, and the hook silently produced zero saves (#1440). # # The leading ``__MEMPAL_PARSE_OK__`` sentinel lets the defense-in-depth # guard below distinguish "Python parsed cleanly, user set session_id to # the literal string 'unknown'" or "session_id was non-ASCII and got # sanitized to empty" from the actual failure mode ("Python crashed, # nothing was printed"). Without the sentinel the guard false-fires # every Stop hook for users on i18n harnesses or unusual harness configs. # Python stderr is captured to last_python_err.log so the guard below can # distinguish "bad user input" (JSONDecodeError) from "broken interpreter # / future regression in this inline script" (ImportError, SyntaxError, # ModuleNotFoundError). Without the stderr capture, last_input.log shows # a valid payload while the actual root cause stays hidden. # # Two extra hardenings inside the command-substitution subshell: # # * ``umask 077`` so the ``2>$STATE_DIR/last_python_err.log`` redirect # creates the file at mode 0600 atomically. Without it, the file # appeared briefly at the parent process's umask (often 0644) before # the explicit ``chmod 600`` below closed it — a small TOCTOU window # where another local user on a shared box could read the traceback, # which can - hooks/mempal_session_end_hook.shGitHub
Read the script
#!/bin/bash # MemPalace SessionEnd Hook — final save on clean exit. # # Claude Code documents a default SessionEnd hook timeout of 1.5s; a per-hook # "timeout" in settings.local.json can raise it (up to 60s), but a # plugin-provided timeout cannot (https://code.claude.com/docs/en/hooks). A cold # `mempalace` start alone can exceed 1.5s, so we background the hook and return # immediately; the detached child finishes the save after the session has # exited. All logic lives in mempalace.hooks_cli for cross-harness extensibility. run_mempalace_hook() { if command -v mempalace >/dev/null 2>&1; then exec mempalace hook run "$@" fi 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 if "$MEMPAL_PYTHON_BIN" -c "import mempalace" >/dev/null 2>&1; then exec "$MEMPAL_PYTHON_BIN" -m mempalace hook run "$@" fi if command -v python >/dev/null 2>&1 && python -c "import mempalace" >/dev/null 2>&1; then exec python -m mempalace hook run "$@" fi echo "MemPalace hook error: could not find a runnable mempalace command or module" >&2 exit 1 } # Capture stdin (the SessionEnd JSON) before backgrounding — the parent's # stdin is gone once we return. Forward it to the detached worker, which runs # the final mine on its own time and outlives this process. payload="$(cat)" ( printf '%s' "$payload" | run_mempalace_hook --hook session-end --harness "${MEMPALACE_HOOK_HARNESS:-claude-code}" ) >/dev/null 2>&1 </dev/null & disown 2>/dev/null || true # Return immediately so the harness never blocks on session exit. printf '{}'
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Ships withmempalace
Local-first AI memory. Verbatim storage, pluggable backend, 96.6% R@5 raw on LongMemEval — zero API calls. ![][release-link] ![][python-link] ![][license-link] ![][discord-link] Beware of impostor sites. MemPalace has no other official websites.
Get the whole plugin

