Skip to content
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.

From plugin
mempalace
58k2 skills5 commands3 hooks
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
Stats
58,264
Stars
7,495
Forks
Active
Maintenance
Python
Language
MIT
License
1d ago
Last commit
4mo ago
Created

Repo: MemPalace/mempalace