Skip to content
Development
Hook

Hooks

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

From plugin
total-agent-memory
681 skill7 hooks1 MCP
Install
> /plugin marketplace add vbcherepanov/total-agent-memory
> /plugin install total-agent-memory@vbcherepanov

Ships with total-agent-memory. Installing the plugin gets these hooks.

What fires, and when

SessionStart

Fires once when a session begins, and again after a context compaction. It is where a plugin sets up its environment, or restores state the compaction dropped.

  • Matchesstartup|resume|clear${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh

UserPromptSubmit

Fires before Claude sees each prompt you send. A plugin can use it to inject context, so the same instruction reaches the model every turn instead of only at session start.

  • ${CLAUDE_PLUGIN_ROOT}/hooks/user-prompt-submit.sh

PreToolUse

  • MatchesEdit|Write|MultiEdit|NotebookEdit${CLAUDE_PLUGIN_ROOT}/hooks/pre-edit.sh

PostToolUse

  • MatchesEdit|Write|MultiEdit|NotebookEdit|Bash${CLAUDE_PLUGIN_ROOT}/hooks/post-tool-use.sh

PostToolUseFailure

  • MatchesBash${CLAUDE_PLUGIN_ROOT}/hooks/on-bash-error.sh

Stop

  • ${CLAUDE_PLUGIN_ROOT}/hooks/on-stop.sh

SessionEnd

  • ${CLAUDE_PLUGIN_ROOT}/hooks/session-end.sh
Read hooks/hooks.json

Where it lives

  • hooks/auto-capture.ps1GitHub
  • hooks/auto-capture.shGitHub
  • hooks/codex-notify.ps1GitHub
  • hooks/codex-notify.shGitHub
  • hooks/memory-trigger.ps1GitHub
  • hooks/memory-trigger.shGitHub
  • hooks/on-bash-error.ps1GitHub
  • hooks/on-bash-error.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # ===========================================
    # PostToolUse hook for Bash — v7.0 learn_error trigger
    #
    # Fires on non-zero bash exit with a distinguishable root cause.
    # Emits a reminder to call learn_error(...) — N≥3 similar patterns
    # auto-consolidate into a rule on the MCP side.
    # ===========================================
    
    source "$(dirname "$0")/lib/common.sh"
    
    TOOL=$(hook_get '.tool_name')
    [ "$TOOL" != "Bash" ] && exit 0
    
    EXIT_CODE=$(hook_get '.tool_response.exit_code')
    # Empty or zero → no error, skip
    [ -z "$EXIT_CODE" ] || [ "$EXIT_CODE" = "0" ] && exit 0
    
    COMMAND=$(hook_get '.tool_input.command' | head -c 200)
    STDERR=$(hook_get '.tool_response.stderr' | head -c 500)
    
    # Skip noise: user aborts, interactive prompts, benign warnings
    case "$STDERR" in
        *"permission denied by user"*|*"User denied"*|*"SIGINT"*) exit 0 ;;
    esac
    
    # Only react if stderr has actionable signal
    [ -z "$STDERR" ] && exit 0
    
    cat <<EOF
    <system-reminder>
    v7.0 learn_error trigger: bash exited $EXIT_CODE. If the root cause is
    reproducible and fixable, call:
      learn_error(
          file="<path if relevant>",
          error="<short stderr>",
          root_cause="<what actually failed>",
          fix="<what resolves it>",
          pattern="<short slug, e.g. sqlite-locked-during-ddl>"
      )
    Skip if this is user-aborted, interactive, or benign. After N≥3 same patterns
    it auto-consolidates into a rule — do not re-log if you just fixed it and the
    root cause is identical to an earlier call this turn.
    </system-reminder>
    EOF
    
    exit 0
    
  • hooks/on-stop.ps1GitHub
  • hooks/on-stop.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # ===========================================
    # On Stop Hook — Portable version
    #
    # Saves session context when Claude stops
    # (context limit, error, user stop, etc.)
    #
    # 1. Saves git state + context to recovery file
    # 2. Reminds about saving knowledge
    # 3. Cleans old recovery files
    #
    # Hook: Stop (matcher: "")
    # ===========================================
    
    source "$(dirname "$0")/lib/common.sh"
    source "$(dirname "$0")/lib/memory-nudge.sh"
    
    STOP_ACTIVE=$(hook_get 'stop_hook_active')
    [ "$STOP_ACTIVE" = "true" ] && exit 0
    
    TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
    DATE_SHORT=$(date '+%Y%m%d-%H%M%S')
    CTX=$(hook_context)
    CWD=$(hook_get 'cwd')
    [ -z "$CWD" ] && CWD="$PWD"
    PROJECT=$(basename "$CWD")
    SESSION_ID=$(hook_get 'session_id')
    [ -z "$SESSION_ID" ] && SESSION_ID="${CLAUDE_SESSION_ID:-unknown}"
    
    # Recovery directory
    mkdir -p "$HOOK_RECOVERY_DIR"
    
    # Collect git context for recovery (read-only commands only)
    GIT_BRANCH=""
    GIT_STATUS=""
    GIT_RECENT=""
    if [ -d "$CWD/.git" ] || git -C "$CWD" rev-parse --git-dir >/dev/null 2>&1; then
        GIT_BRANCH=$(cd "$CWD" 2>/dev/null && git rev-parse --abbrev-ref HEAD 2>/dev/null)
        GIT_STATUS=$(cd "$CWD" 2>/dev/null && git status --short 2>/dev/null | head -20)
        GIT_RECENT=$(cd "$CWD" 2>/dev/null && git log --oneline -5 2>/dev/null)
    fi
    
    # Save recovery file
    RECOVERY_FILE="$HOOK_RECOVERY_DIR/pending-${DATE_SHORT}.md"
    cat > "$RECOVERY_FILE" <<EOF
    # Session Recovery - ${TIMESTAMP}
    
    ## Context
    - **Project**: ${PROJECT}
    - **Path**: ${CWD}
    - **Branch**: ${GIT_BRANCH:-N/A}
    - **Stopped**: ${TIMESTAMP}
    - **Reason**: Session stopped (likely context limit)
    
    ## Git State
    ### Modified files:
    \`\`\`
    ${GIT_STATUS:-No git changes detected}
    \`\`\`
    
    ### Recent commits:
    \`\`\`
    ${GIT_RECENT:-No recent commits}
    \`\`\`
    
    ## Recovery Action
    1. Read this file to understand what was being worked on
    2. Use memory_recall() to find related knowledge
    3. Ask user what needs to be continued
    4. Save any unrecovered knowledge to memory
    EOF
    
    # Keep only last 5 recovery files
    ls -t "$HOOK_RECOVERY_DIR"/pending-*.md 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null
    
    # Notify user
    hook_notify "$CTX | Stopped - context saved for recovery" "Claude Memory | Recovery" "Basso"
    hook_log "STOPPED: $CTX - recovery saved to $RECOVERY_FILE"
    
    echo "Session stopped at $TIMESTAMP"
    echo "Recovery context saved to: $RECOVERY_FILE"
    echo ""
    
    # Emit nudge summary based on this session's write/save counters.
    # Suppressed when nothing relevant happened (writes==0 and saves==0).
    if [ "${MEMORY_NUDGE_DISABLE:-0}" != "1" ]; then
        SUMMARY_LINE=$(nudge_summary "$SESSION_ID" "$PROJECT")
        if [ -n "$SUMMARY_LINE" ]; then
            echo "$SUMMARY_LINE"
            echo ""
        fi
    fi
    
    echo "MEMORY_WARNING: Session ending. Before closing:"
    echo "  1. Save important knowledge with memory_save(project=\"${PROJECT}\")"
    echo "  2. Record a reflection: self_reflect(reflection=\"...\", task_summary=\"...\", project=\"${PROJECT}\")"
    echo ""
    echo "IMPORTANT: Session context was auto-saved for recovery."
    echo "On next session, pending knowledge will be restored."
    
    # Prune nudge state files older than 7 days to keep the dir small.
    find "$NUDGE_STATE_DIR" -name "nudge-*.json" -mtime +7 -delete 2>/dev/null
    
  • hooks/post-tool-use.ps1GitHub
  • hooks/post-tool-use.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # ===========================================
    # PostToolUse Hook — observation capture + memory_save nudges
    #
    # Runs AFTER a tool call. Two responsibilities:
    #
    #  1. (opt-in) when MEMORY_POST_TOOL_CAPTURE=1, enqueue a deferred
    #     observation for the extractor.
    #  2. (always-on) bump per-session counters (writes / edits / saves)
    #     and, when the writes-without-saves ratio crosses a threshold,
    #     echo a nudge line to stdout so Claude sees it on the next turn.
    #     This addresses the "model never calls memory_save on its own"
    #     pattern (reported 2026-05-14 by client running Sonnet).
    #
    # Env:
    #   MEMORY_POST_TOOL_CAPTURE  — "1" to enable observation capture
    #   MEMORY_NUDGE_DISABLE      — "1" to disable nudges entirely
    #   MEMORY_NUDGE_SOFT/HARD/STEP — tune thresholds (see memory-nudge.sh)
    #   CLAUDE_MEMORY_INSTALL_DIR — install root (auto-resolved)
    #   CLAUDE_MEMORY_DIR         — memory storage (~/.claude-memory)
    #
    # Hook: PostToolUse (matcher: "*")
    # ===========================================
    
    CLAUDE_MEMORY_INSTALL_DIR="${CLAUDE_MEMORY_INSTALL_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd)}"
    CLAUDE_MEMORY_DIR="${CLAUDE_MEMORY_DIR:-$HOME/.claude-memory}"
    
    HOOK_PYTHON="${CLAUDE_MEMORY_INSTALL_DIR}/.venv/bin/python"
    if [ ! -x "$HOOK_PYTHON" ]; then
        HOOK_PYTHON="python3"
    fi
    
    SRC_DIR="${CLAUDE_MEMORY_INSTALL_DIR}/src"
    
    # Cache stdin so the background process can read it after this shell exits.
    TMP_INPUT="$(mktemp -t cmm-pthook.XXXXXX)"
    cat > "$TMP_INPUT"
    
    # ---------- Nudge counter + emission (synchronous, in-shell) ----------
    # Runs BEFORE the opt-in capture exit so it fires unconditionally. The
    # python below is short and reads only the cached temp file — no DB I/O,
    # no network, well under 50ms typical.
    if [ "${MEMORY_NUDGE_DISABLE:-0}" != "1" ]; then
        NUDGE_LINE=$(
            "$HOOK_PYTHON" - "$TMP_INPUT" "$CLAUDE_MEMORY_DIR" <<'PY' 2>/dev/null
    import json, os, sys, time, pathlib
    
    tmp_path = sys.argv[1]
    memory_dir = sys.argv[2]
    try:
        data = json.loads(pathlib.Path(tmp_path).read_text() or "{}")
    except Exception:
        sys.exit(0)
    
    tool = (data.get("tool_name") or "").strip()
    if not tool:
        sys.exit(0)
    
    sid_raw = data.get("session_id") or os.environ.get("CLAUDE_SESSION_ID") or "unknown"
    sid = "".join(c if c.isalnum() or c in "._-" else "_" for c in sid_raw)
    project_cwd = data.get("cwd") or os.getcwd()
    project = os.path.basename(project_cwd) or "unknown"
    
    state_dir = pathlib.Path(memory_dir) / "state"
    state_dir.mkdir(parents=True, exist_ok=True)
    state_path = state_dir / f"nudge-{sid}.json"
    
    try:
        state = json.loads(state_path.read_text())
    except Exception:
        state = {"session_id": sid_raw,
                 "started_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
                 "writes": 0, "edits": 0, "bashes": 0,
                 "memory_saves": 0,
                 "last_nudge_writes": 0, "nudge_count": 0}
    
    # Classify the tool. memory_save fires for any MCP tool whose name
    # contains "memory_save" or the dedicated save_decision/save_intent
    # entrypoints — covers all three save flavours.
    field = None
    t_lower = tool.lower()
    if "memory_save" in t_lower or t_lower.endswith("save_decision") or t_lower.endswith("save_intent"):
        field = "memory_saves"
    elif tool in ("Edit", "MultiEdit") or t_lower.endswith("__edit"):
        field = "edits"
    elif tool == "Write" or t_lower.endswith("__write"):
        field = "writes"
    elif tool == "Bash" or t_lower.endswith("__bash"):
        field = "bashes"
    
    if field:
        state[field] = int(state.get(field, 0)) + 1
        state_path.write_text(json.dumps(state))
    
    writes_total = int(state.get("writes", 0)) + int(state.get("edits", 0))
    saves = int(state.get("memory_saves", 0))
    last = int(state.get("last_nudge_writes", 0))
    
    SOFT = int(os.environ.get("MEMORY_NUDGE_SOFT", "3"))
    HARD = int(os.environ.get("MEMORY_NUDGE_HARD", "7"))
    STEP = int(os.environ.get("MEMORY_NUDGE_STEP", "3"))
    
    # Don't nudge on save/bash events themselves — only after write/edit.
    if field not in ("edits", "writes"):
        sys.exit(0)
    
    # Recently saved → back off until divergence grows again.
    if saves > 0 and (writes_total - last) < STEP * 2:
        sys.exit(0)
    if writes_total < SOFT:
        sys.exit(0)
    # Hard threshold escalation always fires (after one prior soft nudge):
    # the throttle should never prevent the urgent message.
    escalating = writes_total >= HARD and saves == 0 and last < HARD
    if not escalating and writes_total - last < STEP:
        sys.exit(0)
    
    if writes_total >= HARD and saves == 0:
        msg = (
            f"MEMORY_NUDGE [hard]: {writes_total} significant edits this session, "
            f"0 memory_save calls. Save decisions/solutions NOW while context is "
            f"fresh: memory_save(content=..., type='decision'|'solution', "
            f"project='{project}', tags=['reusable', ...]). "
            f"Skipping saves is the #1 cause of session amnesia."
        )
    elif saves == 0:
        msg = (
            f"MEMORY_NUDGE [soft]: {writes_total} edits without memory_save. "
            f"When the next decision/fix is finalized, call "
            f"memory_save(project='{project}'). Don't batch for end of session."
        )
    else:
        msg = (
            f"MEMORY_NUDGE: {writes_total} writes vs {saves} saves. "
            f"If a non-trivial new fact landed, memory_save now while it's hot."
        )
    
    print(msg)
    state["last_nudge_writes"] = writes_total
    state["nudge_count"] = int(state.get("nudge_count", 0)) + 1
    state_path.write_text(json.dumps(state))
    PY
        )
        if [ -n "$NUDGE_LINE" ]; then
            echo "$NUDGE_LINE"
        fi
    fi
    
    # Opt-in guard — observation capture only runs when explicitly enabled.
    if [ "${MEMORY_POST_TOOL_CAPTURE:-0}" != "1" ]; then
        rm -f "$TMP_INPUT" 2>/dev/null
        exit 0
    fi
    
    (
        "$HOOK_PYTHON" -c '
    import json, os, sys
    from pathlib import Path
    
    src_dir = sys.argv[1]
    memory_dir = sys.argv[2]
    tmp = sys.argv[3]
    
    if src_dir not in sys.path:
        sys.path.insert(0, src_dir)
    
    os.environ.setdefault("CLAUDE_MEMORY_DIR", memory_dir)
    
    try:
        raw = Path(tmp).read_text()
    except
  • hooks/pre-edit.ps1GitHub
  • hooks/pre-edit.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # ===========================================
    # PreToolUse hook for Write|Edit — v7.0 file_context guard
    #
    # Emits a reminder to call file_context(path) BEFORE editing a file.
    # The agent then calls the tool itself and reads warnings/risk_score.
    # ===========================================
    
    source "$(dirname "$0")/lib/common.sh"
    
    TOOL=$(hook_get '.tool_name')
    FILE_PATH=$(hook_get '.tool_input.file_path')
    
    # Only guard Write/Edit, not NotebookEdit
    case "$TOOL" in
        Write|Edit) ;;
        *) exit 0 ;;
    esac
    
    [ -z "$FILE_PATH" ] && exit 0
    
    # Skip trivial / dotfile / small paths
    case "$FILE_PATH" in
        */.git/*|*/node_modules/*|*/.venv/*|/tmp/*) exit 0 ;;
    esac
    
    cat <<EOF
    <system-reminder>
    v7.0 pre-edit guard: before editing \`$FILE_PATH\`, call
      file_context(path="$FILE_PATH")
    If risk_score > 0.3, read the returned warnings (past errors / hot spots) and
    incorporate them into the edit. Skip if file_context was already called for this
    path in the current turn.
    </system-reminder>
    EOF
    
    exit 0
    
  • hooks/session-end.ps1GitHub
  • hooks/session-end.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # ===========================================
    # Session End Hook — Portable version
    #
    # 1. Saves recovery context from transcript
    # 2. Calls auto_session_save.py for context preservation
    # 3. Calls auto_episode_capture.py for episode capture
    # 4. Cleans old recovery files
    #
    # Hook: SessionEnd (matcher: "")
    # ===========================================
    
    source "$(dirname "$0")/lib/common.sh"
    
    REASON=$(hook_get 'reason')
    CTX=$(hook_context)
    TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
    DATE_SHORT=$(date '+%Y%m%d-%H%M%S')
    CWD=$(hook_get 'cwd')
    [ -z "$CWD" ] && CWD="$PWD"
    PROJECT=$(basename "$CWD")
    
    # Human-readable reason
    case "$REASON" in
        clear)                       REASON_TEXT="User ran /clear" ;;
        compact)                     REASON_TEXT="User ran /compact" ;;
        logout)                      REASON_TEXT="User logged out" ;;
        prompt_input_exit)           REASON_TEXT="User exited" ;;
        bypass_permissions_disabled) REASON_TEXT="Permissions changed" ;;
        *)                           REASON_TEXT="Session ended ($REASON)" ;;
    esac
    
    # Recovery directory
    mkdir -p "$HOOK_RECOVERY_DIR"
    
    # Try to extract info from transcript
    TRANSCRIPT=$(hook_get 'transcript_path')
    TRANSCRIPT_SUMMARY=""
    LAST_USER_MSGS=""
    LAST_ASSISTANT=""
    
    if [ -n "$TRANSCRIPT" ] && [ -f "$TRANSCRIPT" ]; then
        LINES=$(wc -l < "$TRANSCRIPT" 2>/dev/null | tr -d ' ')
    
        # Extract last user messages and assistant context via Python
        EXTRACTED=$("$HOOK_PYTHON" -c "
    import json, sys
    
    transcript = '$TRANSCRIPT'
    user_msgs = []
    assistant_msgs = []
    
    try:
        with open(transcript) as f:
            for line in f:
                try:
                    d = json.loads(line.strip())
                    role = d.get('role', '')
                    content = d.get('content', '')
    
                    if isinstance(content, list):
                        text = ' '.join(c.get('text', '') for c in content if c.get('type') == 'text')
                    elif isinstance(content, str):
                        text = content
                    else:
                        continue
    
                    if not text.strip():
                        continue
    
                    if role == 'user' and d.get('type') == 'human':
                        user_msgs.append(text.strip()[:500])
                    elif role == 'assistant':
                        assistant_msgs.append(text.strip()[:500])
                except:
                    continue
    
        # Output last 5 user messages
        print('USER_MSGS_START')
        for msg in user_msgs[-5:]:
            print(msg)
        print('USER_MSGS_END')
    
        print('ASSISTANT_START')
        for msg in assistant_msgs[-3:]:
            print(msg[:2000])
        print('ASSISTANT_END')
    except:
        pass
    " 2>/dev/null)
    
        LAST_USER_MSGS=$(echo "$EXTRACTED" | sed -n '/^USER_MSGS_START$/,/^USER_MSGS_END$/p' | sed '1d;$d')
        LAST_ASSISTANT=$(echo "$EXTRACTED" | sed -n '/^ASSISTANT_START$/,/^ASSISTANT_END$/p' | sed '1d;$d' | head -c 2000)
        TRANSCRIPT_SUMMARY="~${LINES} transcript events"
    
        # Save transcript recovery with context
        if [ -n "$LAST_USER_MSGS" ] || [ -n "$LAST_ASSISTANT" ]; then
            RECOVERY_FILE="$HOOK_RECOVERY_DIR/pending-${DATE_SHORT}.md"
            cat > "$RECOVERY_FILE" <<EOF
    # Session Recovery - ${TIMESTAMP}
    
    ## Context
    - **Project**: ${PROJECT}
    - **Path**: ${CWD}
    - **Reason**: ${REASON_TEXT}
    - **Transcript**: ${TRANSCRIPT_SUMMARY}
    
    ## Last User Requests
    ${LAST_USER_MSGS:-No user messages extracted}
    
    ## Last Assistant Context
    ${LAST_ASSISTANT:-No assistant content extracted}
    
    ## Recovery Action
    1. Review what was being discussed
    2. Use memory_recall() to find related knowledge
    3. Continue where left off or save summary to memory
    EOF
            hook_log "SESSION_END: Recovery saved to $RECOVERY_FILE"
        fi
    fi
    
    # Keep only last 5 recovery files
    ls -t "$HOOK_RECOVERY_DIR"/pending-*.md 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null
    
    # ======= AUTO-SAVE SESSION CONTEXT (via auto_session_save.py) =======
    if [ -n "$LAST_USER_MSGS" ]; then
        hook_run_script "auto_session_save.py" \
            --project "$PROJECT" \
            --cwd "$CWD" \
            --reason "$REASON_TEXT" \
            --user-context "$LAST_USER_MSGS" \
            --assistant-context "$(echo "$LAST_ASSISTANT" | head -c 1000)"
        hook_log "AUTO_SESSION_SAVE: Started for project $PROJECT"
    fi
    
    # ======= AUTO EPISODE CAPTURE (via auto_episode_capture.py) =======
    EXTRACT_SESSION_ID=""
    if [ -n "$TRANSCRIPT" ] && [ -f "$TRANSCRIPT" ]; then
        EXTRACT_SESSION_ID=$(basename "$TRANSCRIPT" .jsonl)
    else
        # Fallback: find latest transcript by CWD
        PROJECT_HASH=$(echo "$CWD" | sed 's|^/||;s|/|-|g')
        TRANSCRIPT_DIR="$HOME/.claude/projects/-${PROJECT_HASH}"
        FALLBACK_TRANSCRIPT=$(ls -t "$TRANSCRIPT_DIR"/*.jsonl 2>/dev/null | head -1)
        if [ -n "$FALLBACK_TRANSCRIPT" ] && [ -f "$FALLBACK_TRANSCRIPT" ]; then
            EXTRACT_SESSION_ID=$(basename "$FALLBACK_TRANSCRIPT" .jsonl)
        fi
    fi
    
    if [ -n "$EXTRACT_SESSION_ID" ]; then
        hook_run_script "auto_episode_capture.py" \
            --session-id "$EXTRACT_SESSION_ID" \
            --project "$PROJECT"
        hook_log "AUTO_EPISODE: Started capture for session $EXTRACT_SESSION_ID (project: $PROJECT)"
    fi
    
    # Build notification
    STATS=""
    [ -n "$TRANSCRIPT_SUMMARY" ] && STATS=" | $TRANSCRIPT_SUMMARY"
    NOTIFY_MSG="$CTX | $REASON_TEXT$STATS"
    
    hook_notify "$NOTIFY_MSG" "Claude Memory | Done" "Submarine"
    hook_log "SESSION_END: $NOTIFY_MSG"
    
    echo "Session ended: $TIMESTAMP ($REASON_TEXT)"
    echo ""
    echo "MEMORY_AUTO_SAVE: Session context auto-saved to recovery + memory."
    
    # ======= CLEAN UP ERROR-FIX STATE =======
    if [ -d "$HOOK_STATE_DIR" ]; then
        rm -f "$HOOK_STATE_DIR"/last-error-* 2>/dev/null
    fi
    
  • hooks/session-start.ps1GitHub
  • hooks/session-start.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # ===========================================
    # Session Start Hook — Portable version
    #
    # 1. Prints memory recall hint for current project
    # 2. Checks for pending recovery files
    # 3. Loads SOUL rules summary
    # 4. Shows project knowledge count
    # 5. Cleans old error-fix state
    #
    # Hook: SessionStart (matcher: "")
    # ===========================================
    
    source "$(dirname "$0")/lib/common.sh"
    
    CWD=$(hook_get 'cwd')
    [ -z "$CWD" ] && CWD="$PWD"
    PROJECT=$(hook_project_name)
    BRANCH=$(hook_git_branch)
    MODEL=$(hook_model_short)
    CTX=$(hook_context)
    SOURCE=$(hook_get 'source')
    
    # Human-readable source
    case "$SOURCE" in
        startup) SOURCE_TEXT="New session" ;;
        resume)  SOURCE_TEXT="Resumed" ;;
        clear)   SOURCE_TEXT="After /clear" ;;
        compact) SOURCE_TEXT="After /compact" ;;
        *)       SOURCE_TEXT="Started" ;;
    esac
    
    # Notification
    NOTIFY_MSG="$CTX | $SOURCE_TEXT | $MODEL"
    hook_notify "$NOTIFY_MSG" "Claude Memory | Session" "Glass"
    hook_log "SESSION_START: $NOTIFY_MSG"
    
    echo "Session: $NOTIFY_MSG"
    echo ""
    
    # ======= MEMORY HINT =======
    HINT="MEMORY_HINT: Project: ${PROJECT}"
    [ -n "$BRANCH" ] && HINT="${HINT}, Branch: ${BRANCH}"
    HINT="${HINT}. Use memory_recall(query=\"your task\", project=\"${PROJECT}\") to search past knowledge."
    HINT="${HINT} Also run self_rules_context(project=\"${PROJECT}\") to load behavioral rules."
    echo "$HINT"
    echo ""
    
    # ======= RECOVERY CHECK =======
    mkdir -p "$HOOK_RECOVERY_DIR"
    PENDING_FILES=$(ls -t "$HOOK_RECOVERY_DIR"/pending-*.md 2>/dev/null | head -3)
    
    if [ -n "$PENDING_FILES" ]; then
        RECOVERY_COUNT=$(echo "$PENDING_FILES" | wc -l | tr -d ' ')
        echo "RECOVERY_ALERT: Found $RECOVERY_COUNT pending recovery file(s) from previous session(s)!"
        echo ""
    
        # Show the most recent recovery file
        LATEST_RECOVERY=$(echo "$PENDING_FILES" | head -1)
        echo "--- LATEST RECOVERY ($LATEST_RECOVERY) ---"
        cat "$LATEST_RECOVERY"
        echo "--- END RECOVERY ---"
        echo ""
        echo "ACTION_REQUIRED: Review the recovery context above and:"
        echo "  1. Save any important knowledge to MCP memory (memory_save)"
        echo "  2. Delete recovery files after processing: rm $HOOK_RECOVERY_DIR/pending-*.md"
        echo ""
    
        hook_notify "Recovery files found! Check session start output." "Claude Memory | Recovery" "Sosumi"
    fi
    
    # ======= SOUL RULES CHECK =======
    MEMORY_DB="$CLAUDE_MEMORY_DIR/memory.db"
    
    if [ -f "$MEMORY_DB" ]; then
        # Check active SOUL rules
        RULES_INFO=$("$HOOK_PYTHON" -c "
    import sqlite3, os
    db_path = '$MEMORY_DB'
    try:
        db = sqlite3.connect(db_path)
        # Check if rules table exists
        tables = [r[0] for r in db.execute(\"SELECT name FROM sqlite_master WHERE type='table'\").fetchall()]
    
        if 'rules' in tables:
            active = db.execute(\"SELECT COUNT(*) FROM rules WHERE status='active'\").fetchone()[0]
            if active > 0:
                print(f'SOUL_RULES: {active} active behavioral rule(s) loaded.')
                rows = db.execute(
                    \"SELECT priority, substr(content, 1, 80) FROM rules WHERE status='active' ORDER BY priority DESC LIMIT 5\"
                ).fetchall()
                for p, c in rows:
                    print(f'  - [P{p}] {c}')
                print(f'  -> Call self_rules_context(project=\"$PROJECT\") at session start')
    
        # Check error patterns
        if 'errors' in tables:
            patterns = db.execute('''
                SELECT category, COUNT(*) AS cnt FROM errors
                WHERE status != 'insight_extracted' AND created_at > datetime('now', '-30 days')
                GROUP BY category HAVING cnt >= 3
            ''').fetchall()
            if patterns:
                print(f'PATTERN_ALERT: {len(patterns)} error pattern(s) detected (3+ in 30 days).')
                for cat, cnt in patterns[:5]:
                    print(f'  - {cat} ({cnt}x)')
                print('  -> Call self_patterns(view=\"full_report\") to analyze')
    
        # Project knowledge summary
        if 'knowledge' in tables and '$PROJECT' and '$PROJECT' != os.path.basename(os.path.expanduser('~')):
            kcount = db.execute(
                \"SELECT COUNT(*) FROM knowledge WHERE project=? AND status='active'\", ('$PROJECT',)
            ).fetchone()[0]
            scount = db.execute(
                \"SELECT COUNT(*) FROM knowledge WHERE project=? AND status='active' AND type='solution'\", ('$PROJECT',)
            ).fetchone()[0]
            if kcount > 0:
                print(f'PROJECT_MEMORY: \"{PROJECT}\" has {kcount} knowledge records ({scount} solutions)')
                print(f'  -> memory_recall(query=\"<task>\", project=\"$PROJECT\") before starting work')
            else:
                print(f'PROJECT_MEMORY: No prior knowledge for \"{PROJECT}\". Start fresh.')
    
        db.close()
    except Exception as e:
        pass
    " 2>/dev/null)
    
        if [ -n "$RULES_INFO" ]; then
            echo ""
            echo "$RULES_INFO"
        fi
    fi
    
    # ======= CLEAN UP OLD STATE =======
    if [ -d "$HOOK_STATE_DIR" ]; then
        # Remove error states older than 1 hour
        find "$HOOK_STATE_DIR" -name "last-error-*" -mmin +60 -delete 2>/dev/null
    fi
    
  • hooks/user-prompt-submit.ps1GitHub
  • hooks/user-prompt-submit.shRunsGitHub

All 20 scripts are listed above. The source is inlined for 6 of them, starting with whatever hooks.json actually runs. See all of them in the repo.

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 withtotal-agent-memory

Persistent memory for your facts, decisions and working practices. Persistent, local memory for AI coding agents: Claude Code, Codex CLI, Cursor, any MCP client.

Get the whole plugin
Stats
69
Stars
17
Forks
Active
Maintenance
Python
Language
MIT
License
1d ago
Last commit
7mo ago
Created

Repo: vbcherepanov/total-agent-memory