Build This Now
Build This Now
Keyboard ShortcutsStatus Line Guide
CLAUDE.md, Skills, Subagents, Hooks: When to Use WhichClaude Code Subagents: The 3 to 5 Agent Sweet SpotCLAUDE.md Best Practices: The File That Makes Claude Code ReliableHow to Fix Claude Code Running Out of Context
speedy_devvkoen_salo
Blog/Toolkit/Hooks/Claude Code Session Hooks

Claude Code Session Hooks

Four Claude Code session lifecycle hooks: run init on demand, inject project context on SessionStart, back up transcripts, and log cleanup on SessionEnd exit.

Stop configuring. Start building.

SaaS builder templates with AI orchestration.

See what we build for companies →
speedy_devvWritten by speedy_devvPublished Feb 12, 2026Toolkit hubHooks index

Problem: Every new session starts blind. You re-explain the branch you're on, the task queue, and the env vars your scripts need. When the session ends, cleanup that should happen never does.

Quick Win: Paste this into settings.json. Git context lands in the chat on every start:

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo '## Git' && git branch --show-current && git status --short | head -10"
          }
        ]
      }
    ]
  }
}

Every session now opens with context. Zero manual setup.

The Four Session Lifecycle Hooks

Session behaviour is driven by four hook types:

HookWhen It FiresCan Block?Use Case
SetupWith --init or --maintenanceNOOne-time setup, migrations
SessionStartEvery session start/resumeNOLoad context, set env vars
PreCompactBefore context compactionNOBackup transcripts
SessionEndSession terminatesNOCleanup, logging

SessionStart: Load Context Every Time

SessionStart runs whenever a session begins or resumes. Reach for it when something should always be in Claude's head.

Basic Context Injection

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo '## Project State' && cat .claude/tasks/session-current.md 2>/dev/null || echo 'No active session'"
          }
        ]
      }
    ]
  }
}

With JSON Output

For structured context injection:

#!/usr/bin/env python3
import json
import sys
import subprocess
 
def get_project_context():
    try:
        branch = subprocess.check_output(
            ['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
            text=True, stderr=subprocess.DEVNULL
        ).strip()
        status = subprocess.check_output(
            ['git', 'status', '--porcelain'],
            text=True, stderr=subprocess.DEVNULL
        ).strip()
        changes = len(status.split('\n')) if status else 0
    except:
        branch, changes = "unknown", 0
 
    return f"""=== SESSION CONTEXT ===
Git Branch: {branch}
Uncommitted Changes: {changes}
=== END ===""".strip()
 
output = {
    "hookSpecificOutput": {
        "hookEventName": "SessionStart",
        "additionalContext": get_project_context()
    }
}
print(json.dumps(output))
sys.exit(0)

SessionStart Matchers

Target specific session events:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup",
        "hooks": [{ "type": "command", "command": "echo 'Fresh session'" }]
      },
      {
        "matcher": "resume",
        "hooks": [{ "type": "command", "command": "echo 'Resumed session'" }]
      },
      {
        "matcher": "compact",
        "hooks": [{ "type": "command", "command": "echo 'Post-compaction'" }]
      }
    ]
  }
}
  • startup - New session
  • resume - From --resume, --continue, or /resume
  • clear - After /clear
  • compact - After compaction

Persist Environment Variables

SessionStart has access to CLAUDE_ENV_FILE for setting session-wide environment variables:

#!/bin/bash
 
# Persist environment changes from nvm, pyenv, etc.
ENV_BEFORE=$(export -p | sort)
 
# Setup commands that modify environment
source ~/.nvm/nvm.sh
nvm use 20
 
if [ -n "$CLAUDE_ENV_FILE" ]; then
  ENV_AFTER=$(export -p | sort)
  comm -13 <(echo "$ENV_BEFORE") <(echo "$ENV_AFTER") >> "$CLAUDE_ENV_FILE"
fi
 
exit 0

Anything written to CLAUDE_ENV_FILE shows up in every bash command Claude runs after that.

Setup: One-Time Operations

Setup hooks only run when you explicitly invoke --init, --init-only, or --maintenance. Good for work you don't want fired on every new session.

When to Use Setup vs SessionStart

OperationUse SetupUse SessionStart
Install dependenciesYesNo
Run database migrationsYesNo
Load git statusNoYes
Set environment variablesYesYes
Inject project contextNoYes
Cleanup temp filesYes (maintenance)No

Setup Configuration

{
  "hooks": {
    "Setup": [
      {
        "matcher": "init",
        "hooks": [
          {
            "type": "command",
            "command": "npm install && npm run db:migrate"
          }
        ]
      },
      {
        "matcher": "maintenance",
        "hooks": [
          {
            "type": "command",
            "command": "npm prune && npm dedupe && rm -rf .cache"
          }
        ]
      }
    ]
  }
}

Invoke with:

claude --init          # Runs 'init' matcher
claude --init-only     # Runs 'init' matcher, then exits
claude --maintenance   # Runs 'maintenance' matcher

Setup hooks can also write to CLAUDE_ENV_FILE for persisting environment variables.

PreCompact: Before Context Loss

PreCompact fires just before compaction, whether the user triggered it with /compact or it kicked off automatically as the window filled up.

Backup Transcripts

#!/usr/bin/env python3
import json
import sys
import shutil
from pathlib import Path
from datetime import datetime
 
input_data = json.load(sys.stdin)
transcript_path = input_data.get('transcript_path', '')
trigger = input_data.get('trigger', 'unknown')
 
if transcript_path and Path(transcript_path).exists():
    backup_dir = Path('.claude/backups')
    backup_dir.mkdir(parents=True, exist_ok=True)
 
    timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    backup_name = f"transcript_{trigger}_{timestamp}.jsonl"
    shutil.copy2(transcript_path, backup_dir / backup_name)
 
    # Keep only last 10 backups
    backups = sorted(backup_dir.glob('transcript_*.jsonl'))
    for old_backup in backups[:-10]:
        old_backup.unlink()
 
sys.exit(0)

PreCompact Matchers

{
  "hooks": {
    "PreCompact": [
      {
        "matcher": "auto",
        "hooks": [{ "type": "command", "command": "echo 'Auto-compacting...'" }]
      },
      {
        "matcher": "manual",
        "hooks": [{ "type": "command", "command": "echo 'Manual /compact'" }]
      }
    ]
  }
}
  • auto - Context window filled, automatic compaction
  • manual - User ran /compact

Create Recovery Markers

Pair PreCompact with SessionStart to rebuild context after a compaction event. A working pattern: a shared backup-core module, a statusline monitor that fires threshold-based triggers, and a PreCompact handler, all coordinating through one state file so nothing gets dropped between sessions. See the Context Recovery Hook guide for the full walkthrough.

SessionEnd: Cleanup

SessionEnd runs when a session is on its way out. It can't block the shutdown, but it can do cleanup.

Log Session Stats

#!/usr/bin/env python3
import json
import sys
from pathlib import Path
from datetime import datetime
 
input_data = json.load(sys.stdin)
session_id = input_data.get('session_id', 'unknown')
reason = input_data.get('reason', 'unknown')
 
log_dir = Path('.claude/logs')
log_dir.mkdir(parents=True, exist_ok=True)
 
log_entry = {
    "session_id": session_id,
    "ended_at": datetime.now().isoformat(),
    "reason": reason
}
 
with open(log_dir / 'session-history.jsonl', 'a') as f:
    f.write(json.dumps(log_entry) + '\n')
 
sys.exit(0)

SessionEnd Reasons

The reason field tells you why the session ended:

  • clear - User ran /clear
  • logout - User logged out
  • prompt_input_exit - User exited while prompt was visible
  • other - Other exit reasons

Complete Lifecycle Example

A full lifecycle configuration wired end-to-end:

{
  "hooks": {
    "Setup": [
      {
        "matcher": "init",
        "hooks": [
          {
            "type": "command",
            "command": "npm install && echo 'Dependencies installed'"
          }
        ]
      }
    ],
 
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo '## Context' && git status --short && echo '## Tasks' && cat .claude/tasks/session-current.md 2>/dev/null | head -20"
          }
        ]
      }
    ],
 
    "PreCompact": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "cp \"$CLAUDE_TRANSCRIPT_PATH\" .claude/backups/last-transcript.jsonl 2>/dev/null || true"
          }
        ]
      }
    ],
 
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo \"Session ended: $(date)\" >> .claude/logs/sessions.log"
          }
        ]
      }
    ]
  }
}

Input Payloads

SessionStart Input

{
  "session_id": "abc123",
  "hook_event_name": "SessionStart",
  "source": "startup",
  "model": "claude-sonnet-4-20250514",
  "cwd": "/path/to/project"
}

Setup Input

{
  "session_id": "abc123",
  "hook_event_name": "Setup",
  "trigger": "init",
  "cwd": "/path/to/project"
}

PreCompact Input

{
  "session_id": "abc123",
  "hook_event_name": "PreCompact",
  "transcript_path": "~/.claude/projects/.../transcript.jsonl",
  "trigger": "auto",
  "custom_instructions": ""
}

SessionEnd Input

{
  "session_id": "abc123",
  "hook_event_name": "SessionEnd",
  "reason": "clear",
  "cwd": "/path/to/project"
}

Best Practices

  1. Keep SessionStart fast - It runs on every session. Push heavy work into Setup.

  2. Use Setup for one-time work - Dependency installs, migrations, first-time project bootstrap.

  3. Back up before compaction - PreCompact is your last chance to grab the transcript.

  4. Log session ends - SessionEnd is handy for analytics and debugging.

  5. Match carefully - Different behaviour for startup vs resume vs compact avoids surprises.

Next Steps

  • Set up the main Hooks Guide for all 12 hooks
  • Configure Context Recovery for compaction survival
  • Use Stop Hooks for task enforcement
  • Explore Skill Activation for automatic skill loading

Continue in Hooks

  • Slash Commands Are Now Skills: Migrating Commands to Skills
    Claude Code slash commands and skills now do the same thing. Here is how to migrate .claude/commands to .claude/skills and why it pays off.
  • Claude Code Setup Hooks
    Braid scripts, agents, and docs into Claude Code setup hooks. One command runs a deterministic script, hands output to a diagnosing agent, logs living docs.
  • Context Backup Hooks for Claude Code
    A StatusLine-driven Claude Code context backup hook. Writes structured snapshots every 10K tokens so auto-compaction never eats errors, signatures, decisions.
  • Cross-Platform Hooks for Claude Code
    Cross-platform Claude Code hooks: skip .cmd, .sh, and .ps1 wrappers and invoke node directly so one .mjs file runs on macOS, Linux, and Windows across the team.
  • Hooks Guide
    Claude Code hooks from first principles: exit codes, JSON output, async commands, HTTP endpoints, PreToolUse and PostToolUse matchers, production patterns.
  • MCP Tool Hooks in Claude Code
    How to call MCP server tools directly from Claude Code hooks using type: mcp_tool — schema, substitution syntax, use cases, and production patterns.

More from Toolkit

  • CLAUDE.md, Skills, Subagents, Hooks: When to Use Which
    Claude Code skills vs subagents vs hooks vs CLAUDE.md: a plain mental model for picking the right primitive, with token costs and examples.
  • Claude Code Subagents: The 3 to 5 Agent Sweet Spot
    Claude code subagents work best at 3-5 concurrent agents. Here is why that ceiling exists, how to set them up, and what to use past it.
  • CLAUDE.md Best Practices: The File That Makes Claude Code Reliable
    CLAUDE.md best practices: keep it under 200 lines, write it by hand, and use hooks when you need real enforcement, not advice.
  • How to Fix Claude Code Running Out of Context
    Claude Code running out of context is a session design problem. Fix it with /compact, lean CLAUDE.md, skills, and subagents, not a bigger window.

Stop configuring. Start building.

SaaS builder templates with AI orchestration.

See what we build for companies →

On this page

The Four Session Lifecycle Hooks
SessionStart: Load Context Every Time
Basic Context Injection
With JSON Output
SessionStart Matchers
Persist Environment Variables
Setup: One-Time Operations
When to Use Setup vs SessionStart
Setup Configuration
PreCompact: Before Context Loss
Backup Transcripts
PreCompact Matchers
Create Recovery Markers
SessionEnd: Cleanup
Log Session Stats
SessionEnd Reasons
Complete Lifecycle Example
Input Payloads
SessionStart Input
Setup Input
PreCompact Input
SessionEnd Input
Best Practices
Next Steps

Stop configuring. Start building.

SaaS builder templates with AI orchestration.

See what we build for companies →