Skip to content
Data
HotHook

Hooks

What mempalace-mempalace runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.

Guide
From plugin
mempalace-mempalace
59k3 skills5 commands3 hooks1 MCP
Install
> /plugin marketplace add MemPalace/mempalace
> /plugin install mempalace@mempalace

Ships with mempalace-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-mempalace

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