Hook event 02 of 33

Setup

Fires for requested one-time setup or maintenance runs, separate from normal session startup.

Exit code veto
Exit code cannot block. Every exit code lets execution continue and every JSON output field is discarded.
Matchers
init, maintenance
if filter
Does not apply
Docs checked
2026-08-29

Nothing fires on a normal start

You put a first-run hook under Setup and start claude the usual way. Nothing happens, and there is no error to chase. Setup does not run on a plain start. It fires only when the command line asks for it: claude --init-only, claude -p --init, or claude -p --maintenance. The matcher takes init or maintenance, matching the flag. Beside the fields every hook receives, the payload carries one of its own, trigger, holding the same value.

Every return value is thrown away

Even a deny decision printed as JSON will not stop the run. Exit 0, exit 1, exit 2, and every other code all let the run continue. The hooks reference goes further than that: the event discards its JSON output fields. systemMessage, continue, and hookSpecificOutput.additionalContext are all dropped. A Setup hook that prints a blocking decision is printing something Claude Code never reads.

Under -p, stdout, stderr, and the exit code appear as hook_response events, and only when the run uses --output-format stream-json --verbose. The reference does not say where they go on any other surface.

The channel that does work

What survives is a side effect. A Setup hook can write environment variables to the file named by CLAUDE_ENV_FILE, and those persist into the session that follows. If the run needs to leave something behind for later commands, that file is where it goes.

The mistake most people make first

You write a hook that installs dependencies on first run, put it under Setup, and start claude. Nothing happens. A plain start is not one of this event’s triggers, so the hook was never called, and there is no error because nothing was configured wrong.

Run claude --init-only and it fires. Then the second half: if the hook prints {"systemMessage": "dependencies installed"} expecting a line on screen, that message is discarded with the rest of the JSON. Write the confirmation to a file, or to CLAUDE_ENV_FILE if a later command needs to know.