Hook event 01 of 33

SessionStart

Runs when Claude Code starts a new session or resumes, clears, compacts, or forks an existing one.

Exit code veto
Exit code cannot block
Matchers
startup, resume, clear, compact, fork
if filter
Does not apply
Docs checked
2026-08-29

Five ways a session starts, and one of them is compaction

You want Claude to see your project conventions on turn one. You wire a hook under SessionStart and it works. Then compaction fires mid-session and the same hook runs again. A session starts fresh, or it resumes an earlier one, or it is cleared, compacted, or forked. SessionStart fires for all five, and the matcher names which: startup, resume, clear, compact, or fork. The hooks reference says the if filter does not apply here. Set it anyway and the handler never runs, rather than running less often.

The value to look at twice is compact. Compaction is Claude Code replacing the conversation so far with a summary when the context window, the amount of conversation the model can hold at once, fills up. That counts as a start. So a hook written for the beginning of a conversation runs again in the middle of one.

What arrives, and what only arrives on resume and fork

Every call carries source, the same value the matcher saw, and may carry model, agent_type, and session_title. On a resume or a fork in Claude Code v2.1.251 or later, four more fields appear: seconds_since_last_response, context_tokens, prompt_cache_likely_expired, and estimated_cache_write_usd. A hook that reads those on a plain startup finds nothing there.

Plain text on stdout is context here

On most events, text a hook prints to stdout goes to the debug log unless it is JSON. SessionStart is different. Exit 0 with plain text on stdout puts that text in front of Claude as startup context. That is the whole mechanism for a hook that injects your conventions, and it needs no JSON at all.

The JSON form exists for the other things a hook can set. Under hookSpecificOutput with hookEventName: "SessionStart", the reference lists additionalContext, initialUserMessage, sessionTitle, watchPaths, and reloadSkills. There is no field that blocks.

No exit code stops a session

Exit 1 lets the session start and injects nothing from plain output, though valid JSON still applies. Exit 2 lets the session start too. Its stderr shows to you as a hook-error notice, Claude never sees it, and the session proceeds. Nothing a SessionStart hook returns can prevent a session from starting. The return value is about what Claude sees on turn one, never about whether turn one happens.

Module 09 lists this event in its inventory and covers the timeouts that apply to it.

The mistake most people make first

You write a hook that prints your project conventions and wire it under SessionStart with no matcher. It works on startup. An hour into a long session the context window fills, Claude Code compacts, and compact is one of the five sources. The hook runs again and prints the conventions again. The context now holds two copies, then three after the next compaction.

Nothing warns you. The hook is doing what you wired it to do, and there is no error to see. The sign is the same block appearing more than once in what Claude has been given.

Match startup alone, or read source from the payload and exit 0 with no output when it is compact.