Skip to content

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

Description

@taliesynw

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

# 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions