Skip to main content
Kodelyth ECC
guide

Claude Code Hooks — A Practical Guide to All 8 Events

What each Claude Code hook event fires on, what the payload looks like, and the failure modes that bite in production: exit codes, fail-open defaults, timeouts, and why a slow hook is a security problem. With 44 working examples from Kodelyth ECC.

Hooks are the only way to make an AI coding assistant enforce something rather than merely intend it. A rule in a prompt is a suggestion the model may drop under context pressure. A hook is a process that runs whether the model likes it or not.

This guide covers the eight events Kodelyth ECC hooks into, what each one is good for, and — the part most documentation skips — the failure modes that bite once hooks are doing real work.

The eight events

ECC ships 44 hook entries across these eight events. The distribution is a reasonable map of where hooks actually earn their keep.

EventECC entriesFiresGood for
PreToolUse12Before a tool runsBlocking dangerous operations, validating arguments
PostToolUse13After a tool returnsFormatting, linting, type-checking, scanning output
PostToolUseFailure1After a tool errorsCapturing failure context while it is still fresh
UserPromptSubmit2When you send a messageInjecting context, scanning input
SessionStart4Session beginsLoading memory, project rules, prior lessons
Stop10Assistant finishes a turnVerification gates, capturing what was learned
SessionEnd1Session closesFinal writes, cost accounting
PreCompact1Before context compactionPreserving state that compaction would discard

The two biggest buckets are PreToolUse and PostToolUse, which is what you would expect: most of what you want to enforce happens around a tool call.

The payload

A hook receives a JSON object on stdin. The shape varies by event, but the fields you will reach for most are:

{
  "tool_name": "Edit",
  "tool_input":    { "file_path": "src/app.ts", "old_string": "…" },
  "tool_response": { "content": [{ "type": "text", "text": "…" }] }
}

Read stdin to completion before parsing. A hook that reads one chunk and assumes it has the whole payload works fine in testing and truncates on a large tool response.

Exit codes are the whole interface

This is where most hook bugs live.

  • Exit 0 — allow. Anything on stdout is passed along.
  • Exit 2 — block. For PreToolUse this stops the tool from running.
  • Any other non-zero — treated as the hook erroring, not as a decision.

The practical consequence: a hook that crashes does not block anything. If your hook's job is to prevent something, an unhandled exception in it is a silent failure of the protection, not a loud one.

Fail-open is a decision, not a default

Most hooks should fail open. A linter that throws should not wedge the session. ECC's own prompt-injection guard runs in warn mode by default and always exits 0, so a bug in it degrades detection rather than blocking work.

But be deliberate about it, because fail-open has a sharp edge:

If a hook is the thing enforcing a rule, every path where it fails to reach a verdict is a path where the rule is not enforced.

That includes paths you did not write. Which brings us to the failure mode that is easiest to miss.

A slow hook is a security problem

Hooks run on the critical path. A hook that takes two seconds adds two seconds to every tool call. That alone is reason enough to keep them fast — but there is a worse version.

ECC's prompt-injection guard scans untrusted text — file contents, fetched pages, command output — against a set of regexes. One of those patterns was written as:

\s+ (group)? \s* (group)? \s*

Three whitespace quantifiers with optional groups between them. When both optional groups match empty they sit adjacent, so a run of N spaces can be partitioned between them in O(N²) ways — and every partition is retried when the rest of the pattern fails. Measured on "ignore" + N spaces + "x":

Ntime
2,0001.5s
4,00012.5s
8,000did not finish

The guard caps input at 20,000 characters, and the blow-up starts at 2,000 — so the cap did not help.

Now combine that with fail-open. In warn mode the session stalls until the harness kills the hook, and no warning is ever emitted. In blocking mode it is worse: a hook that never returns never exits 2, so the thing it existed to block goes through. The denial of service doubles as a bypass of the control.

The fix was to bind each optional segment's whitespace inside its own group, so a whitespace run has exactly one valid partition:

(?:\s+X)?    instead of    \s+(X)?\s*

4.8ms at the full cap afterwards, and linear beyond it.

Two lessons generalise:

  1. Regexes in hooks are attack surface when they run against text an attacker can influence. Test them with adversarial input, not just realistic input.
  2. Time your hooks against hostile input, not typical input. A catastrophic regex is synchronous and CPU-bound, so a test-framework timeout cannot interrupt it — your test suite will hang rather than fail. Run the probe in a child process with a hard timeout so a regression reports itself.

Keep them fast

A reasonable budget is under 100ms for anything on PreToolUse or PostToolUse. Practical ways to stay there:

  • Do the cheap rejection first. A string check before a regex, a regex before a parse, a parse before anything touching disk.
  • Cap input length, and pick the cap by measuring, not by feel. A cap above the point where your slowest path degrades is decoration.
  • Skip work you cannot use. A formatter hook that fires on every tool should return immediately for tools that do not touch files.
  • Prefer a timeout you control over one the harness imposes. If the harness kills your hook, you get no chance to report why.

Where hooks beat prompting

Hooks are worth the complexity when the rule must hold even if the model forgets, and the check is mechanical. Formatting, secret scanning, type checks, test gates, and anything whose answer is a clean yes or no.

Prompting is better when the judgement is genuinely contextual. A hook cannot tell you whether an abstraction is premature.

See it working

Every hook described here ships in ECC and runs on install, across Claude Code, Windsurf, Cursor, Codex, Antigravity and the rest.

npm i -g kodelyth-ecc && kodelyth-ecc

The hook definitions live in hooks/hooks.json, and the scripts they call are plain Node with no dependencies — readable, and worth reading before you trust anything that runs on every tool call.

Last updated: 2026-10-05T00:00:00.000Z · v2.24.5