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.
Want the framework behind these builds?
Get the Claude Code system we use to plan, build, test, and ship production software.
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:
| Hook | When It Fires | Can Block? | Use Case |
|---|---|---|---|
| Setup | With --init or --maintenance | NO | One-time setup, migrations |
| SessionStart | Every session start/resume | NO | Load context, set env vars |
| PreCompact | Before context compaction | NO | Backup transcripts |
| SessionEnd | Session terminates | NO | Cleanup, 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 sessionresume- From--resume,--continue, or/resumeclear- After/clearcompact- 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 0Anything 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
| Operation | Use Setup | Use SessionStart |
|---|---|---|
| Install dependencies | Yes | No |
| Run database migrations | Yes | No |
| Load git status | No | Yes |
| Set environment variables | Yes | Yes |
| Inject project context | No | Yes |
| Cleanup temp files | Yes (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' matcherSetup 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 compactionmanual- 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/clearlogout- User logged outprompt_input_exit- User exited while prompt was visibleother- 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
-
Keep SessionStart fast - It runs on every session. Push heavy work into Setup.
-
Use Setup for one-time work - Dependency installs, migrations, first-time project bootstrap.
-
Back up before compaction - PreCompact is your last chance to grab the transcript.
-
Log session ends - SessionEnd is handy for analytics and debugging.
-
Match carefully - Different behaviour for
startupvsresumevscompactavoids 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
Want the framework behind these builds?
Get the Claude Code system we use to plan, build, test, and ship production software.

