Build This Now
Build This Now
Keyboard ShortcutsStatus Line Guide
Hooks GuideCross-Platform Hooks for Claude CodeClaude Code Setup HooksStop HooksSelf-Validating Claude Code AgentsClaude Code Session HooksContext Backup Hooks for Claude CodeSkill Activation HookClaude Code Permission Hook
Get Build This Now
speedy_devvkoen_salo
Blog/Toolkit/Hooks/Claude Code Session Hooks

Claude Code Session Hooks

Four lifecycle hooks for Claude Code. Run init steps on demand, inject project context at every start, back up transcripts, and log cleanup on exit.

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

More in this guide

  • Keyboard Shortcuts
    Configure custom keyboard shortcuts in Claude Code.
  • Status Line Guide
    Set up a custom Claude Code status line showing model name, git branch, cost, and context usage.
  • AI SEO and GEO Optimization
    A rundown of Generative Engine Optimization: how to get content cited inside ChatGPT, Claude, and Perplexity responses instead of just ranked on Google.
  • Claude Code vs Cursor in 2026
    A side-by-side look at Claude Code and Cursor in 2026: agent models, context windows, pricing tiers, and how each tool fits different developer workflows.
  • Claude Code VSCode Extension
    Anthropic's VS Code extension puts Claude Code inside the editor sidebar as a Spark-icon panel, with inline diffs, plan mode, subagents, and MCP support.

Stop configuring. Start building.

SaaS builder templates with AI orchestration.

Get Build This Now

Self-Validating Claude Code Agents

Wire PostToolUse hooks, Stop hooks, and read-only reviewer agents into agent definitions so bad output never reaches you.

Context Backup Hooks for Claude Code

A StatusLine-driven backup system that writes structured snapshots every 10K tokens so auto-compaction cannot eat your session detail.

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.

Get Build This Now