EMZETT.
Login

Claude Code Hooks

In short: Shell commands (or prompts/agents) that Claude Code runs automatically at specific points in the workflow – e.g. before a tool runs, after Claude has finished answering, or when a session starts or ends.

In more detail: Hooks are configured in settings.json, per event (e.g. Stop, PreToolUse, SessionEnd) and optionally with a “matcher” (e.g. only for certain tools). Important: Stop fires after every answer turn, not just once at the end of the whole conversation – SessionEnd is what’s actually meant for “once per session”.

Our context: As a test, we built a Stop hook that dumped the complete JSONL transcript 1:1 into an Obsidian note after every answer – but then discarded it again, because you wanted structured notes on the content instead of raw text. Now I write glossary entries like this one live during the conversation instead.

In Depth

Configuration and matchers

Hooks are configured per event in settings.json (or settings.local.json for local, uncommitted overrides). A typical definition looks like this:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "echo 'Bash is being run' >> log.txt" }]
      }
    ]
  }
}

The matcher filters for which tools/events the hook fires at all — without a matcher it runs on every event of that category. Several hooks can be registered for the same event; they then run one after another.

Available event types

Besides PreToolUse/PostToolUse (before and after every tool execution) and Stop (after every answer turn) there are, among others, SessionStart/SessionEnd (once at the start/end of a session), UserPromptSubmit (when the user sends a message, before Claude processes it) and PreCompact (before the context is compacted). Each event type receives different structured data via stdin (e.g. for PreToolUse the name of the tool and its input parameters as JSON) — a hook script can read this data and decide on that basis whether and how to react.

Blocking tool calls

A PreToolUse hook can even block a tool execution (by responding with a specific exit code or a specific output), which can be used for your own security rules, for example — say, to forbid certain Bash commands entirely, regardless of Claude’s own judgement. This is an important difference from mere instructions in CLAUDE.md: a hook is a hard rule enforced by the harness itself that can’t be bypassed through clever prompt wording, whereas text instructions are in principle always just behavioural guidelines that the model follows.

Shell commands vs. prompts/agents

Besides plain shell commands, hooks can also trigger more complex prompts or dedicated agents, not just one-line scripts. This allows, for example, a Stop hook that automatically sets a review agent loose on the most recently changed files after every answer, or a SessionEnd hook that generates a summary of the session — considerably more powerful than a plain shell script, but also with correspondingly higher resource usage, since another LLM call is involved.

Important differences between similar-sounding events

Stop fires after every answer turn, not just once at the end of the whole conversation — an easily overlooked pitfall if what you actually want is “once per session”. SessionEnd is meant for that goal; it really only fires when the entire session ends. Anyone who confuses the two risks either a hook that runs needlessly often on every single answer turn, or one that doesn’t react at the actual end as intended.

See also: JSONL