Skip to content
Automation
Hook

Hooks

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

From plugin
maxdmyers-recall
132 hooks

Where it lives

  • hooks/capture-session.shGitHub
    Read the script
    #!/usr/bin/env bash
    # recall :: capture hook (Stop)
    # Idempotent per session_id. Writes a lightweight markdown dump pointing at the
    # full transcript, so the nightly distill can read sessions without an LLM call here.
    # Pure shell + jq + git. Never blocks Claude: always exits 0.
    
    set -uo pipefail
    
    VAULT="${RECALL_VAULT:-$HOME/Documents/Vault/recall}"
    SESSIONS="$VAULT/sessions"
    
    # Don't capture distill's own headless claude runs (recursion guard).
    [ -n "${RECALL_DISTILL:-}" ] && exit 0
    
    PAYLOAD=$(cat)
    
    # Only act on the Stop event regardless of how we're wired.
    EVENT=$(printf '%s' "$PAYLOAD" | jq -r '.hook_event_name // empty')
    [ "$EVENT" = "Stop" ] || { [ -n "$EVENT" ] && exit 0; }
    
    SESSION_ID=$(printf '%s' "$PAYLOAD" | jq -r '.session_id // empty')
    CWD=$(printf '%s' "$PAYLOAD" | jq -r '.cwd // empty')
    TRANSCRIPT=$(printf '%s' "$PAYLOAD" | jq -r '.transcript_path // empty')
    LAST_MSG=$(printf '%s' "$PAYLOAD" | jq -r '.last_assistant_message // empty' | tr '\n' ' ' | head -c 500)
    
    [ -n "$SESSION_ID" ] || exit 0
    [ -n "$CWD" ] || CWD="$PWD"
    
    # Project identity: git repo root basename, else launch-dir basename.
    if REPO=$(git -C "$CWD" rev-parse --show-toplevel 2>/dev/null); then
      PROJECT=$(basename "$REPO")
      BRANCH=$(git -C "$CWD" rev-parse --abbrev-ref HEAD 2>/dev/null)
      DIFFSTAT=$(git -C "$CWD" diff --stat 2>/dev/null | tail -40)
      STATUS=$(git -C "$CWD" status --short 2>/dev/null | head -40)
    else
      PROJECT=$(basename "$CWD")
      BRANCH=""
      DIFFSTAT=""
      STATUS=""
    fi
    
    SHORT="${SESSION_ID:0:8}"
    mkdir -p "$SESSIONS"
    
    # One dump per session_id — NOT per project. A session that changes git-repo
    # context mid-run (cd into another repo, or a repo rename) would otherwise fork
    # into a second dump and get distilled twice. Reuse the existing dump if one
    # already exists for this session_id, keeping its original project-named file.
    FILE=""
    for existing in "$SESSIONS"/*__"$SHORT".md; do
      [ -f "$existing" ] || continue
      if grep -q "^session_id: $SESSION_ID\$" "$existing"; then FILE="$existing"; break; fi
    done
    [ -n "$FILE" ] || FILE="$SESSIONS/${PROJECT}__${SHORT}.md"
    
    # Preserve original Started timestamp across turns.
    NOW=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
    if [ -f "$FILE" ]; then
      STARTED=$(grep -m1 '^started:' "$FILE" | sed 's/^started: //')
    fi
    [ -n "${STARTED:-}" ] || STARTED="$NOW"
    
    {
      echo "---"
      echo "session_id: $SESSION_ID"
      echo "project: $PROJECT"
      echo "branch: ${BRANCH:-}"
      echo "cwd: $CWD"
      echo "started: $STARTED"
      echo "updated: $NOW"
      echo "transcript: $TRANSCRIPT"
      echo "distilled: false"
      echo "tags: [recall/session]"
      echo "---"
      echo
      echo "# Session $SHORT — $PROJECT"
      echo
      echo "## Last assistant message"
      echo "${LAST_MSG:-(none)}"
      echo
      echo "## Git diff --stat"
      echo '```'
      echo "${DIFFSTAT:-(no repo / no changes)}"
      echo '```'
      echo
      echo "## Git status"
      echo '```'
      echo "${STATUS:-(no repo / clean)}"
      echo '```'
    } > "$FILE.tmp" && mv "$FILE.tmp" "$FILE"
    
    exit 0
    
  • hooks/inject-knowledge.shGitHub
    Read the script
    #!/usr/bin/env bash
    # recall :: SessionStart hook — injects vault knowledge indexes as
    # additional context. Read-only, never modifies project files. Fails open:
    # any error -> exit 0 silently so a broken vault never blocks session start.
    #
    # Fires on all SessionStart sources (startup, resume, clear, compact) so
    # context survives /clear and auto-compact too.
    
    set +e  # never die
    
    VAULT="${RECALL_VAULT:-$HOME/Documents/Vault/recall}"
    KNOWLEDGE="$VAULT/knowledge"
    [ -d "$KNOWLEDGE" ] || exit 0
    
    # Project = git root basename, else cwd basename. Matches capture-session.sh.
    CWD="$PWD"
    if REPO=$(git -C "$CWD" rev-parse --show-toplevel 2>/dev/null); then
      PROJECT=$(basename "$REPO")
      SKILLS_DIR="$REPO/.claude/skills"
    else
      PROJECT=$(basename "$CWD")
      SKILLS_DIR="$CWD/.claude/skills"
    fi
    
    # Compose the context block. Tagged so Claude can recognize it as background.
    CTX=$(
      echo "<recall-knowledge>"
      echo "Indexes from your knowledge vault. Read full notes on demand:"
      echo "  $KNOWLEDGE/global/<name>.md  or  $KNOWLEDGE/projects/$PROJECT/<name>.md"
      echo
    
      [ -f "$KNOWLEDGE/global/INDEX.md" ] && { cat "$KNOWLEDGE/global/INDEX.md"; echo; }
    
      PROJ_INDEX="$KNOWLEDGE/projects/$PROJECT/INDEX.md"
      if [ -f "$PROJ_INDEX" ]; then
        cat "$PROJ_INDEX"; echo
      else
        echo "# $PROJECT knowledge"
        echo "(no project notes yet — distill will populate as sessions accumulate)"
        echo
      fi
    
      # Live list of installed skills for this project (read each SKILL.md's
      # frontmatter description). Computed live so newly-installed skills appear
      # the same day, without waiting on a vault refresh.
      if [ -d "$SKILLS_DIR" ]; then
        found=0
        for sk in "$SKILLS_DIR"/*/SKILL.md; do
          [ -f "$sk" ] || continue
          [ "$found" -eq 0 ] && { echo "# $PROJECT installed skills"; found=1; }
          sk_name=$(basename "$(dirname "$sk")")
          sk_desc=$(awk '/^description:/ {sub(/^description: */,""); print; exit}' "$sk")
          echo "- $sk_name — ${sk_desc:-<no description>}"
        done
        [ "$found" -eq 1 ] && echo
      fi
    )
    
    # Enforce an injection budget so a large index can't bloat every session's
    # context. Mirrors Claude Code's auto-memory cap — first 200 lines OR 25KB,
    # whichever comes first. Truncates on whole-line boundaries (keeps valid UTF-8)
    # and leaves a visible marker, so nothing is dropped silently.
    MAX_LINES="${RECALL_INJECT_MAX_LINES:-200}"
    MAX_BYTES="${RECALL_INJECT_MAX_BYTES:-25600}"
    truncated=0
    if [ "$(printf '%s\n' "$CTX" | wc -l | tr -d ' ')" -gt "$MAX_LINES" ]; then
      CTX="$(printf '%s\n' "$CTX" | head -n "$MAX_LINES")"; truncated=1
    fi
    while [ "$(printf '%s' "$CTX" | wc -c | tr -d ' ')" -gt "$MAX_BYTES" ] \
       && [ "$(printf '%s\n' "$CTX" | wc -l | tr -d ' ')" -gt 1 ]; do
      CTX="$(printf '%s\n' "$CTX" | sed '$d')"; truncated=1
    done
    
    # Read the hook event name from stdin (SessionStart for all sources). Default
    # to SessionStart if stdin parse fails — the value is informational only.
    STDIN_JSON=$(cat 2>/dev/null)
    EVENT=$(printf '%s' "$STDIN_JSON" | jq -r '.hook_event_name // "SessionStart"' 2>/dev/null)
    [ -z "$EVENT" ] && EVENT="SessionStart"
    
    # Emit JSON with additionalContext. jq -R -s --arg handles all escaping safely.
    {
      printf '%s\n' "$CTX"
      [ "$truncated" -eq 1 ] && printf '… recall: knowledge index truncated to the injection budget (%s lines / %s bytes). Read the full indexes on demand under %s\n' "$MAX_LINES" "$MAX_BYTES" "$KNOWLEDGE"
      echo "</recall-knowledge>"
    } | jq -R -s --arg event "$EVENT" \
      '{hookSpecificOutput: {hookEventName: $event, additionalContext: .}}'
    
    exit 0
    

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 withmaxdmyers-recall

A self-improving layer for Claude Code: it learns from your sessions, retains knowledge in a vault, and proposes skills/automations from recurring patterns. This repo holds the machinery (hooks, distill prompt + runner, dashboard, install config).

Get the whole plugin
Stats
13
Stars
0
Forks
Maintained
Maintenance
Shell
Language
2mo ago
Last commit
4mo ago
Created

Repo: maxdmyers/recall