Skip to content
Productivity
Hook

Hooks

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

From plugin
claude-recap
394 skills2 hooks

Claude Code

> /plugin marketplace add hatawong/claude-recap
> /plugin install claude-recap@claude-recap-marketplace

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|compact${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh

Stop

  • ${CLAUDE_PLUGIN_ROOT}/hooks/stop.sh
Read hooks/hooks.json

Where it lives

  • hooks/session-start.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # session-start.sh — SessionStart hook: inject memory into Claude context
    # Everything printed to stdout is injected into Claude's context
    
    set -euo pipefail
    
    INPUT=$(cat)
    
    CWD=$(echo "$INPUT" | jq -r '.cwd')
    SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
    SOURCE=$(echo "$INPUT" | jq -r '.source // "unknown"')
    
    echo "[SessionStart] session=$SESSION_ID source=$SOURCE"
    PROJECT_ID="${CWD//\//-}"
    
    # MEMORY_HOME allows test isolation; defaults to ~/.memory
    MEMORY_ROOT="${MEMORY_HOME:-$HOME/.memory}"
    PROJECT_DIR="$MEMORY_ROOT/projects/$PROJECT_ID"
    SESSION_DIR="$PROJECT_DIR/$SESSION_ID"
    
    PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
    
    # Ensure project dir exists
    mkdir -p "$PROJECT_DIR"
    
    # Step: archive pending topics from previous sessions (skip during compact to reduce cognitive load)
    # Runs as background process — self-starts LLM, does not block session start or occupy Agent context
    if [ "$SOURCE" != "compact" ]; then
      "$PLUGIN_ROOT/scripts/archive-pending.sh" "$PROJECT_DIR" "$SESSION_ID" "$PLUGIN_ROOT" &>/dev/null &
    fi
    
    # Push layer 1: inject REMEMBER.md (global + project)
    GLOBAL_REMEMBER="$MEMORY_ROOT/REMEMBER.md"
    PROJECT_REMEMBER="$PROJECT_DIR/REMEMBER.md"
    
    if [ -f "$GLOBAL_REMEMBER" ]; then
      echo "=== Things You Should Remember (Global) ==="
      cat "$GLOBAL_REMEMBER"
      echo ""
    fi
    
    if [ -f "$PROJECT_REMEMBER" ]; then
      echo "=== Things You Should Remember (This Project) ==="
      cat "$PROJECT_REMEMBER"
      echo ""
    fi
    
    # Legacy: inject preferences.md if it exists (will be migrated to REMEMBER.md)
    PREFERENCES="$MEMORY_ROOT/global/preferences.md"
    if [ -f "$PREFERENCES" ]; then
      echo "=== User Preferences & Constraints (auto-injected, please follow) ==="
      cat "$PREFERENCES"
      echo ""
    fi
    
    # Push layer 2: list topic files grouped by session, sorted by recency (max 20 sessions)
    if [ -d "$PROJECT_DIR" ]; then
      # For each session dir: get newest file mtime (epoch), date, topics in seq order
      TOPIC_HISTORY=$(
        for session_dir in "$PROJECT_DIR"/*/; do
          [ ! -d "$session_dir" ] && continue
          sid=$(basename "$session_dir")
          short=$(echo "$sid" | cut -c1-8)
    
          # List topic files (exclude hidden), sorted by name (= seq order)
          topics=$(find "$session_dir" -maxdepth 1 -name "*.md" -not -name ".*" 2>/dev/null | sed 's|.*/||; s|\.md$||' | sort)
          [ -z "$topics" ] && continue
    
          # Get newest file mtime as epoch (macOS stat -f, Linux stat -c fallback)
          newest=$(find "$session_dir" -maxdepth 1 -name "*.md" -not -name ".*" -exec stat -f '%m' {} + 2>/dev/null | sort -rn | head -1)
          [ -z "$newest" ] && newest=$(find "$session_dir" -maxdepth 1 -name "*.md" -not -name ".*" -exec stat -c '%Y' {} + 2>/dev/null | sort -rn | head -1)
          [ -z "$newest" ] && continue
    
          # Format date from epoch
          dt=$(date -r "$newest" "+%m-%d %H:%M" 2>/dev/null || date -d "@$newest" "+%m-%d %H:%M" 2>/dev/null)
    
          # Output: epoch|display_line (epoch for sorting, stripped later)
          echo "${newest}|${short} (${dt}): $(echo "$topics" | paste -sd',' - | sed 's/,/, /g')"
        done | sort -rn | head -20 | cut -d'|' -f2-
      )
      if [ -n "$TOPIC_HISTORY" ]; then
        echo "=== Topic History for This Project (recent 20 sessions) ==="
        echo "$TOPIC_HISTORY"
        echo ""
      fi
    fi
    
    # Pull layer: tell Claude where to find memory files and scripts
    echo "Your persistent memory is stored at $PROJECT_DIR (session directories with topic files)."
    echo "If topic history files are listed above, check the user's first message to decide whether to cat any of them to restore context."
    echo ""
    echo "Plugin scripts path: $PLUGIN_ROOT/scripts"
    echo ""
    
    # Push layer 3: inject topic tracking state
    STATE_FILE="$SESSION_DIR/.current_topic"
    if [ -f "$STATE_FILE" ]; then
      CURRENT_TOPIC=$(cat "$STATE_FILE")
    else
      CURRENT_TOPIC="(none — use your first topic slug in the tag, the Stop hook will register it)"
    fi
    cat <<EOF
    
    === Topic Tag Rule ===
    At the START of every reply, output a topic tag in this exact format:
    › \`your-topic-slug\`
    The slug should be 2-4 words, lowercase, hyphen-separated, describing the current topic.
    If the topic hasn't changed, repeat the same slug. If it has, use the new slug.
    This tag is machine-read by the Stop hook. Always include it.
    
    Current topic: $CURRENT_TOPIC
    Topic archival is automatic — the Stop hook detects topic changes from your tag and guides you through archival. You do not need to call any skill manually.
    EOF
    
    # Feature A: Compact context recovery — extract + cold-reader summary via claude -p
    if [ "$SOURCE" = "compact" ] && [ -f "$STATE_FILE" ]; then
      CURRENT_SLUG=$(cat "$STATE_FILE")
      mkdir -p "$SESSION_DIR"
    
      # Create .compacted marker — signals archive-pending to skip this session
      touch "$SESSION_DIR/.compacted"
    
      JSONL_PATH="$HOME/.claude/projects/$PROJECT_ID/$SESSION_ID.jsonl"
      if [ -f "$JSONL_PATH" ] && [ -n "$CURRENT_SLUG" ]; then
        EXTRACT_SCRIPT="$PLUGIN_ROOT/scripts/extract-topic.js"
        SUMMARY_TEMPLATE=$(cat "$PLUGIN_ROOT/scripts/topic-tmpl.md")
    
        # Extract conversation for current topic
        EXTRACTED=$(node "$EXTRACT_SCRIPT" "$JSONL_PATH" "$CURRENT_SLUG" 2>/dev/null) || true
    
        if [ -n "$EXTRACTED" ]; then
          # Cold-reader summarization via claude -p (blocks session start)
          ARCHIVE_CWD=$(mktemp -d)
          RECOVERY=$(unset CLAUDECODE; cd "$ARCHIVE_CWD" && claude -p --model sonnet --no-session-persistence "You are summarizing a conversation extract for context recovery after compaction. Output the following TWO parts:
    
    PART 1 — Structured summary (section headings in English, content in user's language, skip empty sections):
    
    ${SUMMARY_TEMPLATE}
    
    PART 2 — Copy the last 2 User/Assistant exchanges verbatim from the conversation. Preserve the original language exactly. Use the heading format:
    ### Last exchanges
    ## User
    ...
    ## Assistant
    ...
    ## User
    ...
    ## Assistant
    ...
    
    Rules: State facts only. No AI filler language.
    
    --- CONVERSATION ---
    ${EXTRACTED}
    --- END ---" 2>/dev/null) || true
          rm -rf "$A
  • hooks/stop.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # stop.sh — Stop hook: detect topic change and trigger archival
    # Extracts › `slug` from last_assistant_message,
    # compares with .current_topic. If changed, exit 2 with direct bash command for LLM to archive.
    
    set -euo pipefail
    
    INPUT=$(cat)
    
    # Anti-recursion: if already inside a stop hook cycle, pass through
    STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
    if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
      exit 0
    fi
    
    # Extract topic tag from last assistant message
    LAST_MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // ""')
    # Only match a line that is exclusively the topic tag (anchored ^...$)
    # Format: › `slug` — Unicode arrow + backtick-wrapped slug
    # Single quotes intentional: sed regex, not shell expansion
    # shellcheck disable=SC2016
    NEW_TOPIC=$(echo "$LAST_MSG" | head -1 | sed -n 's/^› `\([a-z0-9-]*\)`$/\1/p')
    
    # Fallback: when LLM uses tools, the tag is in an earlier message (not last_assistant_message).
    # Read the JSONL transcript to find the most recent topic tag from any assistant text block.
    if [ -z "$NEW_TOPIC" ]; then
      TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // ""')
      if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
        NEW_TOPIC=$(tail -50 "$TRANSCRIPT_PATH" | \
          jq -r '.message.content[]? | select(.type == "text") | .text' 2>/dev/null | \
          sed -n 's/^› `\([a-z0-9-]*\)`$/\1/p' | tail -1) || true
        if [ -n "$NEW_TOPIC" ]; then
          echo "[stop.sh] extracted topic tag from transcript (fallback): '${NEW_TOPIC}'" >&2
        fi
      fi
    fi
    
    # Debug: log what we got
    echo "[stop.sh] extracted topic tag: '${NEW_TOPIC}'" >&2
    
    # If no tag found, pass through (LLM didn't follow the rule)
    if [ -z "$NEW_TOPIC" ]; then
      echo "[stop.sh] no topic tag found in last_assistant_message or transcript, pass through" >&2
      exit 0
    fi
    
    # Read current topic from per-session state file
    MEMORY_ROOT="${MEMORY_HOME:-$HOME/.memory}"
    CWD=$(echo "$INPUT" | jq -r '.cwd // ""')
    SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""')
    PROJECT_ID="${CWD//\//-}"
    SESSION_DIR="$MEMORY_ROOT/projects/${PROJECT_ID}/${SESSION_ID}"
    TOPIC_FILE="$SESSION_DIR/.current_topic"
    OLD_TOPIC=$(cat "$TOPIC_FILE" 2>/dev/null || echo "none")
    
    echo "[stop.sh] old_topic='${OLD_TOPIC}', new_topic='${NEW_TOPIC}'" >&2
    
    # Compare
    if [ "$NEW_TOPIC" = "$OLD_TOPIC" ]; then
      echo "[stop.sh] topic unchanged, pass through" >&2
      exit 0
    fi
    
    # First topic in session — just register, nothing to archive
    if [ "$OLD_TOPIC" = "none" ]; then
      mkdir -p "$SESSION_DIR"
      echo "$NEW_TOPIC" > "$TOPIC_FILE"
      echo "[stop.sh] first topic registered: ${NEW_TOPIC}" >&2
      exit 0
    fi
    
    # Check .ignore — skip archival for ignored topics (before LLM summary)
    PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
    PROJECT_DIR="$MEMORY_ROOT/projects/$PROJECT_ID"
    source "$PLUGIN_ROOT/scripts/ignore-topic-utils.sh"
    if topic_is_ignored "$OLD_TOPIC" "$MEMORY_ROOT" "$PROJECT_DIR"; then
      echo "[stop.sh] topic '$OLD_TOPIC' matches .ignore, skipping archival" >&2
      echo "$NEW_TOPIC" > "$TOPIC_FILE"
      exit 0
    fi
    
    # Topic changed — archive old topic via direct bash command
    # Resolve transcript_path (may already be set from fallback above, or extract now)
    TRANSCRIPT_PATH="${TRANSCRIPT_PATH:-$(echo "$INPUT" | jq -r '.transcript_path // ""')}"
    
    SUMMARY_TEMPLATE=$(cat "${PLUGIN_ROOT}/scripts/topic-tmpl.md")
    cat >&2 <<TOPIC_EOF
    Topic changed from '${OLD_TOPIC}' to '${NEW_TOPIC}'.
    
    Archive the old topic NOW. Write a factual summary of '${OLD_TOPIC}' and run this command:
    
    bash "${PLUGIN_ROOT}/scripts/set-topic.sh" "${OLD_TOPIC}" "${NEW_TOPIC}" "${SESSION_ID}" "<your_summary>" "${TRANSCRIPT_PATH}"
    
    Replace <your_summary> with a structured summary using this format (section headings in English, content in user's language, skip empty sections):
    
    ${SUMMARY_TEMPLATE}
    
    Rules: State facts only. No AI filler language. The script adds the header and time range automatically.
    TOPIC_EOF
    exit 2
    

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.

@hatawong
Ships withclaude-recap

Topic-based automatic memory for Claude Code — never lose context across sessions or compactions.

Stars on GitHub

Momentum

stars in total
39
Star history of hatawong/claude-recap on GitHub, a chart by star-history.com

More plugins that ship hooks.

i-have-adhd

i-have-adhd

A skill that shapes Claude Code's output for readers with ADHD: it leads with the next action, numbers multi-step work, restates state across turns, drops tangents, gives specific time estimates, and makes progress visible. Invoke it with /i-h.

1 skill1 hook60k
@ayghri@ayghri
Install Plugin
pua

pua

A Claude Code plugin that makes the assistant work harder when it stalls or repeats failures. It adds pressure-style coaching, plus skills for workplace reminders and senior engineer, CTO and team-lead modes for planning and execution.

12 skills+320k
@tanweai@tanweai
Install Plugin
claude-obsidian

claude-obsidian

Turns the sources you read, such as papers, articles and transcripts, into a linked wiki of Markdown notes in Obsidian. Claude keeps sources and citations so every summary can be traced back to its origin.

15 skills+216k
@agricidaniel@agricidaniel
Install Plugin
obsidian-second-brain

obsidian-second-brain

Gives Claude Code a persistent second brain stored as plain markdown in an Obsidian vault. Notes rewrite themselves as new sources arrive, and scheduled agents handle research synthesis and note capture.

1 skill+25.3k
@eugeniughelbur@eugeniughelbur
Install Plugin
arscontexta

arscontexta

A Claude Code plugin that builds a personal knowledge system through conversation, with skills for adding knowledge domains, checking vault health, getting methodology guidance, and evolving the system over time.

10 skills+23.5k
@agenticnotetaking@agenticnotetaking
Install Plugin
pro-workflow

pro-workflow

A Claude Code workflow plugin that learns from your corrections and keeps a self-correcting memory across sessions. It adds skills for agent teams, parallel worktree batch changes, bug capture as GitHub issues, project auto-setup and context compaction.

41 skills+32.9k
@rohitg00@rohitg00
Install Plugin
engram

engram

Helps you learn any topic with a first-principles curriculum, guided tutoring, and spaced review that schedules free-recall practice. Includes coaching for retention stats and study strategy.

3 skills+21.5k
@nagisanzenin@nagisanzenin
Install Plugin
hey

hey

A command line tool for HEY that lets you read and send email, manage contacts, labels, calendars, todos, habits, time tracking and journal entries from the terminal or from your coding agent.

1 skill1 hook423
@basecamp@basecamp
Install Plugin

Questions about the hooks in claude-recap.

Which coding agents can use the hooks in claude-recap?
The hooks in claude-recap install in Claude Code and Cursor: each runs Claude Code hooks from .claude/settings.json, so copy the hooks there.