Skip to content

How to configure Claude Code to run your pre-commit hooks

Run the project’s pre-commit configuration before Claude Code finishes a change. Calling pre-commit itself keeps the agent’s checks aligned with .pre-commit-config.yaml.

Prerequisites

  • Claude Code and uv installed
  • A Git repository with .pre-commit-config.yaml and an existing passing check baseline
  • A .claude/hooks/ directory

Prepare the hook environments

Run the configured checks once before starting the session:

uvx pre-commit run --all-files

Each hook prints Passed, Failed, or Skipped. Address failures before enabling the agent hook. The first run may install isolated hook environments; later runs reuse the cache. Runtime depends on the configured tools and project size.

Check changed and new files at turn end

Save this script as .claude/hooks/precommit-stop.py:

.claude/hooks/precommit-stop.py
import json
import os
from pathlib import Path
import subprocess
import sys

payload = json.load(sys.stdin)
if payload.get("stop_hook_active"):
    sys.exit(0)
cwd = payload.get("cwd") or os.environ.get("CLAUDE_PROJECT_DIR") or "."
root = Path(subprocess.check_output(
    ["git", "rev-parse", "--show-toplevel"], cwd=cwd, text=True,
).strip())

def git_paths(args):
    output = subprocess.check_output(["git", *args], cwd=root)
    return {os.fsdecode(path) for path in output.split(b"\0") if path}

has_head = subprocess.run(
    ["git", "rev-parse", "--verify", "HEAD"], cwd=root,
    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
).returncode == 0
if has_head:
    files = git_paths(["diff", "--name-only", "--diff-filter=ACMR", "-z", "HEAD"])
else:
    files = git_paths(["ls-files", "--cached", "-z"])
files |= git_paths(["ls-files", "--others", "--exclude-standard", "-z"])
files = sorted(path for path in files if (root / path).is_file())
if not files:
    sys.exit(0)
result = subprocess.run(
    ["uvx", "pre-commit", "run", "--files", *files],
    cwd=root, capture_output=True, text=True,
)
if result.returncode:
    print(result.stdout + result.stderr, file=sys.stderr)
    print("Review fixes, rerun checks, or report the remaining blocker.", file=sys.stderr)
    sys.exit(2)

The script finds the Git root from the hook payload’s active directory, so checks follow Claude into a worktree or a repository subdirectory. It includes staged and unstaged tracked changes, plus untracked files that Git does not ignore. NUL-delimited paths and an argument list preserve filenames with spaces. Deleted files are excluded, and repositories without a first commit use their staged and untracked files.

Register the Stop hook

Merge this event into .claude/settings.json:

.claude/settings.json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "uv",
            "args": [
              "run",
              "--no-project",
              "python",
              "${CLAUDE_PROJECT_DIR}/.claude/hooks/precommit-stop.py"
            ],
            "timeout": 120
          }
        ]
      }
    ]
  }
}

Exec form starts uv directly without depending on Bash or jq, including on Windows. --no-project launches the standard-library handler without installing the current project or creating its environment and lockfile; uvx pre-commit manages the check separately.

A failed check returns its report to Claude on stderr with exit code 2. The stop_hook_active guard allows the agent to stop after one corrective attempt; it must report any failure that remains.

Hooks with pass_filenames: false or always_run: true may check more than the listed files. Keep slow network or integration checks in CI if they make each turn impractical.

Verify the check and keep CI required

Open /hooks to confirm registration. Ask Claude to change a Python file and inspect the hook report. Confirm that a new untracked file is checked too. Run the reported uvx pre-commit run --files ... command yourself if the outcome is unclear.

A formatter may change a file and still exit 0 for an unstaged file because pre-commit compares Git diffs to detect modifications. Inspect the actual file, then rerun checks. Do not use an exit message alone as proof that the change passed without modification.

Keep uvx pre-commit run --all-files in required CI checks. The local Stop hook provides feedback; server-side checks protect the merge even when local hooks are skipped.

Last updated on