Claude Code Guide

Hooks & Automation

Automatic context injection that fires without user action. mem0 memories at session start, retro lessons before skills, and context recovery after compaction.

Skills Are Manual. Hooks Are Automatic.

Skills like /prime require you to type a command. Hooks fire silently in the background at specific lifecycle events — session start, before tool use, after edits. The user never sees them run, but Claude receives the injected context.

This is the difference between "tell Claude what happened last session" (manual, via /prime) and "Claude already knows what happened last session" (automatic, via hooks). Hooks provide the baseline context layer; skills provide the deep dive when you need it.

SESSION LIFECYCLE SessionStart hook fires mem0 memories injected silently (Claude sees recent context without asking) User types /prime Deep project briefing (git, issues, indexes) (optional — for when you need the full picture) User invokes a skill PreToolUse hook fires retro lessons injected before skill runs ("last time you ran /spec, you forgot X") ... work continues ... Context window fills up Claude compacts conversation SessionStart hook fires again HEAVY MODE recovers handoff state + recent context Session continues with restored memory
SessionStart

Fires at session launch and after compaction. Two modes: lightweight (5 memories) on startup, heavy (10 memories + handoff state) after compact.

PreToolUse

Fires before a tool executes. Matcher filters to specific tools (e.g., only before Skill invocations). Injects retro lessons relevant to the skill being called.

UserPromptSubmit

Fires when the user submits a prompt. Used for status bar updates, input validation, or prompt preprocessing.

How Hooks Work

A hook is a command that Claude Code runs at a lifecycle event. It receives context on stdin and returns instructions on stdout as JSON.

Input (stdin)

Claude Code sends a JSON payload with event-specific fields:

// SessionStart — source tells you why it fired
{"source": "startup"}     // fresh session
{"source": "resume"}      // reconnected
{"source": "compact"}     // context was compacted

// PreToolUse — tool_input has the tool's parameters
{
  "tool_name": "Skill",
  "tool_input": {"skill": "spec", "args": "STAK-498"}
}

// UserPromptSubmit — the user's message
{"message": "fix the price scraper bug"}

Output (stdout)

The hook prints JSON to stdout. The key field is additionalContext — a string that gets silently injected into Claude's context.

// Inject context — Claude sees this but the user doesn't
{
  "additionalContext": "Recent context: you worked on STAK-498 yesterday..."
}

// SessionStart has a special wrapper format
{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "Recent mem0 context for StakTrakr..."
  }
}

// Empty output = hook ran but has nothing to inject
{}
Performance matters
Hooks block the event they're attached to. A slow SessionStart hook delays the entire session boot. Keep hooks under 4 seconds — use timeouts on all network calls, fail silently on errors, and return {} when there's nothing useful to inject.
settings.json Hook Config

Hooks are registered in ~/.claude/settings.json under the hooks key. Each event maps to an array of hook commands.

~/.claude/settings.json
{
  "hooks": {
    "SessionStart": {
      "matcher": "",
      "hooks": [
        {
          "type": "command",
          "command": "python3 ~/.claude/hooks/mem0-session-start.py"
        }
      ]
    },
    "PreToolUse": {
      "matcher": "Skill",
      "hooks": [
        {
          "type": "command",
          "command": "python3 ~/.claude/hooks/retro-recall.py"
        }
      ]
    }
  }
}

Matcher field

The matcher filters when the hook fires:

EventMatcherEffect
SessionStart"" (empty)Fires on every session start and compaction
PreToolUse"Skill"Only fires before Skill tool invocations
PreToolUse"Edit"Only fires before file edits
PreToolUse"" (empty)Fires before every tool call (use carefully)
mem0 Session Start

The core hook. Searches mem0 for recent project memories and injects them as context so Claude knows where you left off — without you typing anything.

Two modes

ModeWhenBehavior
Normal source: "startup" or "resume" Lightweight — 5 recent memories, last 7 days, brief format
Compact recovery source: "compact" Heavy — searches for pre-compact handoff state + 10 context memories, formatted with recovery header

The compact recovery mode is critical. When Claude's context window fills up and compacts, it loses the conversation history. This hook detects compaction and injects a heavier context block with a ## Context restored after compaction header, including any handoff state that was saved before the compact event.

Project detection

The hook detects which project you're in by reading the git root directory name and mapping it to a mem0 agent_id tag. This ensures you only get memories relevant to the current project, not cross-contamination from other work.

# Project mapping — git directory name → mem0 agent_id
PROJECT_MAP = {
    "StakTrakr": "staktrakr",
    "HexTrackr": "hextrackr",
    "MyMelo": "mymelo",
    "Forge": "forge",
    # ... add your projects
}

# The hook reads the git root:
#   git rev-parse --show-toplevel → /path/to/StakTrakr
#   basename → StakTrakr
#   PROJECT_MAP["StakTrakr"] → "staktrakr"
# Then searches mem0 with agent_id="staktrakr"

Source filtering

Not all mem0 memories are equal. The hook prefers memories from high-signal sources (session hooks, compaction hooks) over bulk-imported or migrated memories. If preferred-source memories exist, lower-quality ones are filtered out.

# High-signal sources — prefer these
PREFERRED_SOURCES = {"stop-hook", "pre-compact-hook", "post-tool-use-hook"}

# If any preferred-source memories exist, only show those
# Otherwise, fall back to showing everything

What Claude sees

On a normal startup, Claude receives something like:

Recent mem0 context for StakTrakr (last 7 days):
  1. [2026-04-03] (session-digest) Fixed JM Bullion price scraper —
     eCheck column was being read as Card/PayPal. PR #903 merged.
  2. [2026-04-02] (retro-learning) Always check column order in
     vendor-specific parsers before assuming index positions.
  3. [2026-04-01] (session-digest) Deployed poller v3.33.83 to
     Portainer Stack 7 and Fly.io. All prices verified.
mem0-session-start.py
#!/usr/bin/env python3
"""
mem0 SessionStart hook for Claude Code.

Two modes:
- startup/resume: Lightweight — 5 recent memories
- compact: Heavy — handoff state + 10 context memories

Reads MEM0_API_KEY from ~/.claude/.mcp.json.
Detects project from git root directory name.
"""

import json, os, re, subprocess, sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError

MEM0_API_URL = "https://api.mem0.ai/v1/memories/search/"
MCP_CONFIG = Path.home() / ".claude" / ".mcp.json"
MAX_NORMAL = 5
MAX_COMPACT = 10
LOOKBACK_DAYS = 7
EMPTY = json.dumps({})

# Map git directory names to mem0 agent_id tags
PROJECT_MAP = {
    "StakTrakr": "staktrakr",
    "HexTrackr": "hextrackr",
    "MyMelo": "mymelo",
    "Forge": "forge",
    # Add your projects here
}

PREFERRED_SOURCES = {"stop-hook", "pre-compact-hook", "post-tool-use-hook"}


def get_api_key():
    """Read MEM0_API_KEY from .mcp.json."""
    try:
        with open(MCP_CONFIG) as f:
            text = re.sub(r",\s*([}\]])", r"\1", f.read())
        config = json.loads(text)
        servers = config.get("mcpServers", config)
        return servers.get("mem0", {}).get("env", {}).get("MEM0_API_KEY")
    except Exception:
        return None


def get_project():
    """Detect project from git root."""
    try:
        r = subprocess.run(["git", "rev-parse", "--show-toplevel"],
                           capture_output=True, text=True, timeout=5)
        if r.returncode == 0:
            name = os.path.basename(r.stdout.strip())
            return name, PROJECT_MAP.get(name)
    except Exception:
        pass
    name = os.path.basename(os.getcwd())
    return name, PROJECT_MAP.get(name)


def search_mem0(api_key, query, limit, agent_id=None):
    """Search mem0 API, post-filter by project and date."""
    since = (datetime.now(timezone.utc)
             - timedelta(days=LOOKBACK_DAYS)).strftime("%Y-%m-%d")
    payload = {"query": query, "user_id": "lbruton",
               "limit": limit * 4 if agent_id else limit * 2}
    req = Request(MEM0_API_URL, data=json.dumps(payload).encode(),
                  headers={"Authorization": f"Token {api_key}",
                           "Content-Type": "application/json"},
                  method="POST")
    try:
        with urlopen(req, timeout=10) as resp:
            data = json.loads(resp.read().decode())
    except Exception:
        return []
    if not isinstance(data, list):
        data = data.get("results", [])

    filtered = []
    for mem in data:
        if mem.get("created_at", "")[:10] < since:
            continue
        if agent_id:
            proj = mem.get("metadata", {}).get("project", "").lower()
            if proj and proj != agent_id:
                continue
        filtered.append(mem)
    return filtered[:limit]


def filter_preferred(memories):
    preferred = [m for m in memories
                 if m.get("metadata", {}).get("source") in PREFERRED_SOURCES]
    return preferred if preferred else memories


def format_normal(memories, project_name):
    if not memories:
        return None
    memories = filter_preferred(memories)
    lines = [f"Recent mem0 context for {project_name} "
             f"(last {LOOKBACK_DAYS} days):"]
    for i, mem in enumerate(memories, 1):
        text = mem.get("memory", "").strip()
        if not text:
            continue
        if len(text) > 200:
            text = text[:197] + "..."
        date = mem.get("created_at", "")[:10]
        mtype = mem.get("metadata", {}).get("type", "")
        tag = f" ({mtype})" if mtype else ""
        lines.append(f"  {i}. [{date}]{tag} {text}")
    return "\n".join(lines) if len(lines) > 1 else None


def format_compact(handoff, context, project_name):
    sections = ["## Context restored after compaction\n"]
    context = filter_preferred(context)
    if handoff:
        sections.append("### Session state before compaction")
        for mem in handoff[:2]:
            text = mem.get("memory", "").strip()
            if text:
                sections.append(text[:500])
        sections.append("")
    if context:
        sections.append("### Recent project context")
        for i, mem in enumerate(context, 1):
            text = mem.get("memory", "").strip()
            if not text:
                continue
            if len(text) > 200:
                text = text[:197] + "..."
            date = mem.get("created_at", "")[:10]
            mtype = mem.get("metadata", {}).get("type", "")
            tag = f" ({mtype})" if mtype else ""
            sections.append(f"  {i}. [{date}]{tag} {text}")
    return "\n".join(sections) if len(sections) > 2 else None


def main():
    source = "startup"
    try:
        raw = sys.stdin.read()
        if raw:
            source = json.loads(raw).get("source", "startup")
    except Exception:
        pass

    api_key = get_api_key()
    project_name, agent_id = get_project()
    if not api_key or not agent_id:
        print(EMPTY)
        return

    if source == "compact":
        handoff = search_mem0(api_key,
            "pre-compact handoff session state", 3, agent_id)
        context = search_mem0(api_key,
            "recent decision pattern insight task", MAX_COMPACT, agent_id)
        result = format_compact(handoff, context, project_name)
    else:
        memories = search_mem0(api_key,
            "recent session decision in-progress pattern", MAX_NORMAL,
            agent_id)
        result = format_normal(memories, project_name)

    if not result:
        print(EMPTY)
        return
    print(json.dumps({"hookSpecificOutput": {
        "hookEventName": "SessionStart",
        "additionalContext": result}}))


if __name__ == "__main__":
    main()
Retro Recall

Before any skill runs, this hook searches mem0 for past retro-learning memories — mistakes, patterns, and preferences — and injects them so Claude doesn't repeat the same errors.

How it works

When you type /spec, Claude Code fires the PreToolUse hook before executing the Skill tool. The hook reads the skill name from stdin, searches mem0 for retro learnings tagged to the current project, and injects categorized lessons:

# What Claude sees before running /spec:
Retro learnings relevant to `spec` — check before proceeding:
  🔴 Always read the user-template, not the default template —
     last time you used the wrong one and had to redo Phase 3
  🟡 Run codebase-search before writing requirements — skipping
     it caused duplicate API endpoints in STAK-456
  🟢 Single bundled PR worked well for the last refactor —
     user prefers this over many small PRs for related changes

Categories

IconCategoryWhat it captures
🔴errorMistakes that cost time — don't repeat these
🟡warningRisky patterns to watch for
🔵patternRecurring approaches that work
⚙️improvementProcess optimizations discovered
💜preferenceUser preferences and working style
🟢winApproaches that worked well — keep doing these

Smart filtering

The hook skips noisy low-value skills (/save, /remember, /retro itself) to avoid circular injection. It also filters by project — you only see retro learnings from the project you're currently working in, unless a cross-project lesson scored highly enough to be universal.

retro-recall.py
#!/usr/bin/env python3
"""
PreToolUse hook: surfaces past retro learnings before a skill executes.

Searches mem0 for retro-learning memories tagged to the current project
and injects them as additionalContext before the skill runs.
Timeout: 4 seconds. Silent on error.
"""

import json, os, re, subprocess, sys
from pathlib import Path
from urllib.request import Request, urlopen

MEM0_API_URL = "https://api.mem0.ai/v1/memories/search/"
MCP_CONFIG = Path.home() / ".claude" / ".mcp.json"
TIMEOUT = 4
EMPTY = json.dumps({})

# Skills that don't benefit from retro injection
SKIP = {"save", "remember", "handoff", "digest-session", "retro"}

ICONS = {"error": "🔴", "warning": "🟡", "pattern": "🔵",
         "improvement": "⚙️", "preference": "💜", "win": "🟢"}


def get_api_key():
    try:
        with open(MCP_CONFIG) as f:
            text = re.sub(r",\s*([}\]])", r"\1", f.read())
        servers = json.loads(text).get("mcpServers", {})
        return servers.get("mem0", {}).get("env", {}).get("MEM0_API_KEY")
    except Exception:
        return None


def get_project_tag():
    try:
        r = subprocess.run(["git", "rev-parse", "--show-toplevel"],
                           capture_output=True, text=True, timeout=3)
        if r.returncode == 0:
            return os.path.basename(r.stdout.strip()).lower()
    except Exception:
        return None


def search_retro(api_key, skill_name, project_tag):
    payload = {
        "query": f"retro learning lesson mistake pattern {skill_name}",
        "user_id": "lbruton",
        "limit": 10,
        "filters": {"AND": [
            {"user_id": "lbruton"},
            {"metadata": {"type": "retro-learning"}}
        ]}
    }
    req = Request(MEM0_API_URL, data=json.dumps(payload).encode(),
                  headers={"Authorization": f"Token {api_key}",
                           "Content-Type": "application/json"},
                  method="POST")
    try:
        with urlopen(req, timeout=TIMEOUT) as resp:
            data = json.loads(resp.read().decode())
    except Exception:
        return []

    results = data if isinstance(data, list) else data.get("results", [])
    relevant = []
    for mem in results:
        proj = mem.get("metadata", {}).get("project", "")
        score = mem.get("score", 0)
        if proj == project_tag or score > 0.75:
            relevant.append(mem)
    return relevant[:5]


def main():
    skill_name = "unknown"
    try:
        raw = sys.stdin.read()
        if raw:
            skill_name = json.loads(raw).get("tool_input", {}) \
                .get("skill", "unknown")
    except Exception:
        pass

    if skill_name in SKIP:
        print(EMPTY); return

    api_key = get_api_key()
    project_tag = get_project_tag()
    if not api_key or not project_tag:
        print(EMPTY); return

    memories = search_retro(api_key, skill_name, project_tag)
    if not memories:
        print(EMPTY); return

    lines = [f"Retro learnings relevant to `{skill_name}`:"]
    for mem in memories:
        text = mem.get("memory", "").strip()
        if not text: continue
        cat = mem.get("metadata", {}).get("category", "")
        icon = ICONS.get(cat, "•")
        lines.append(f"  {icon} {text[:180]}")

    if len(lines) > 1:
        print(json.dumps({"additionalContext": "\n".join(lines)}))
    else:
        print(EMPTY)


if __name__ == "__main__":
    main()
The Layered Context Model

Hooks and skills work together as layers. Each layer adds more context, but only when needed. The session starts fast with just hook-injected memories, and the user can go deeper with manual skills.

LayerMechanismCostWhen
1. Hook baseline SessionStart hook injects 5 recent mem0 memories ~500 tokens, 2-3 seconds Every session, automatic
2. Retro guard PreToolUse hook injects relevant retro lessons ~300 tokens, 1-2 seconds Before each skill, automatic
3. /prime deep load Reads vault, checks git, indexes, gathers issues ~2000 tokens, 15-30 seconds On demand, when you need full context
4. /session-oracle Semantic search across all past session logs Variable, runs as subagent On demand, when you need to find something specific

Most sessions only need layers 1-2. You start Claude, the hook silently reminds it of yesterday's work, and you pick up where you left off. /prime is for Monday mornings or context switches. /session-oracle is for "when did we decide X?" questions.

Compaction recovery is the killer feature
Long sessions hit context limits and compact. Without the hook, Claude loses everything and you have to manually re-explain. With the hook, compaction fires a source: "compact" event, the hook searches mem0 for the pre-compact handoff state, and injects it with a ## Context restored after compaction header. Claude picks up where it left off without the user even noticing the compaction happened.
Add Hooks to Your Setup

1. Save the hook scripts

mkdir -p ~/.claude/hooks
# Save mem0-session-start.py and retro-recall.py
chmod +x ~/.claude/hooks/mem0-session-start.py
chmod +x ~/.claude/hooks/retro-recall.py

2. Get a mem0 API key

Sign up at mem0.ai and add the key to your MCP config at ~/.claude/.mcp.json. The hooks read the key from the same config file your MCP server uses — single source of truth.

3. Register the hooks

Prompt for Claude: Register hooks in settings.json
I want to register two hooks in my Claude Code settings:

1. SessionStart: Run python3 ~/.claude/hooks/mem0-session-start.py on every session start (empty matcher)
2. PreToolUse: Run python3 ~/.claude/hooks/retro-recall.py before Skill tool invocations (matcher: "Skill")

Add these to ~/.claude/settings.json under the hooks key. Keep any existing settings intact.

4. Add your project mappings

Edit the PROJECT_MAP dictionary in mem0-session-start.py to map your git directory names to mem0 agent_id tags. The hook only fires for mapped projects — unmapped directories get no context injection.

5. Verify it works

# Start a new Claude Code session in a mapped project
cd ~/MyProject
claude

# Claude should show injected context in its first response
# You can also test the hook directly:
echo '{"source": "startup"}' | python3 ~/.claude/hooks/mem0-session-start.py
Minimum viable hook
You don't need mem0 to start. The simplest useful hook reads a local file and injects it:
#!/usr/bin/env python3
import json
from pathlib import Path
notes = Path.home() / ".claude" / "session-notes.md"
if notes.exists():
    text = notes.read_text()[:1000]
    print(json.dumps({"hookSpecificOutput": {
        "hookEventName": "SessionStart",
        "additionalContext": text}}))
else:
    print("{}")
Write your notes to ~/.claude/session-notes.md at session end, and they'll be injected at the next session start. No API needed.
Part of the Session Memory Pipeline

Hooks are one layer of the broader Session Memory Pipeline. Together with skills, agents, and the knowledge vault, they form a complete system for cross-session continuity.