#1511·lefthook

ai: Claude Code Stop/PreToolUse hooks need exit code 2 to block, but `lefthook run` always exits 1

Author: taliesynwCreated Aug 29, 2026Updated Aug 29, 2026

Summary

The ai: integration (2.1.10, #1448) generates .claude/settings.json entries that run lefthook run <hook>. Claude Code only treats a hook as blocking when the command exits with code 2: for Stop, exit 2 blocks the stop and feeds stderr back to the agent so it keeps working; for PreToolUse, exit 2 blocks the tool call. Any other non-zero exit is "non-blocking": stderr is shown to the user and the agent stops / the tool call proceeds.

lefthook run exits 1 whenever a job fails, even when the job itself exits 2. So a Stop: validate hook that fails lint just prints to the user after the agent has already finished; the agent never sees it.

Reproduction

yaml
# lefthook.yml
ai:
  claude:
    Stop: validate
validate:
  jobs:
    - run: exit 2
$ lefthook install
$ lefthook run validate; echo $?
...
exit status 2
...
1

Same with run: exit 1 → lefthook exits 1. Claude Code docs on exit codes: https://code.claude.com/docs/en/hooks#hook-output

Suggestion

When invoked from a generated agent hook (or via a flag, e.g. lefthook run --agent validate that the ai: generator emits), exit 2 on failure — or propagate the highest job exit code. Also worth noting in the docs that Claude passes stop_hook_active: true on stdin when the agent is already continuing from a Stop hook; users who lint on Stop will want to exit 0 in that case to avoid a loop.

lefthook 2.1.10, macOS (Homebrew).