# What is a pre-commit hook?

A pre-commit hook is a script Git runs before it records a commit, used to lint, format, or test the staged changes and stop the commit if a check fails.

Last updated September 29, 2026, 8 min read

## Learning objectives

After reading this article you will be able to:

-   Define a pre-commit hook
-   Explain how hook managers install and run checks
-   Identify what belongs in a pre-commit hook

## Related content

-   [What is CI/CD?](https://specstory.com/learning/ci-cd/ci-cd)
-   [What is the difference between pre-commit, pre-push, and CI checks?](https://specstory.com/learning/ci-cd/pre-commit-vs-pre-push-vs-ci)
-   [What are agent hooks (Stop hooks)?](https://specstory.com/learning/ci-cd/agent-hooks)

## What is a pre-commit hook?

A pre-commit hook is a script that Git runs at the start of `git commit`, before Git records the [commit](https://specstory.com/learning/glossary#commit). It checks the staged changes, which are the edits selected with `git add`, and a common check is [linting](https://specstory.com/learning/code-review/linting). A nonzero exit code stops the commit. The name also refers to pre-commit, an open-source hook manager.

Git's [hooks documentation](https://git-scm.com/docs/githooks) says that the pre-commit hook runs before Git gets the commit message, and that Git ignores a hook file that is not executable. The same page says that `git commit --no-verify` bypasses the hook.

A pre-commit hook is one of several Git [hooks](https://specstory.com/learning/ci-cd/git-hooks), which are scripts that run at set points in Git's workflow. Its checks usually finish in seconds, because the hook runs at every commit. A continuous integration and delivery ([CI/CD](https://specstory.com/learning/ci-cd/ci-cd)) pipeline can run the same checks again on a server.

## How does a pre-commit hook work?

Git runs a pre-commit hook in four steps:

1.  A developer stages changes with `git add` and runs `git commit`.
2.  Git looks for an executable file named `pre-commit` in the hooks folder, which is `.git/hooks` unless `core.hooksPath` names another folder.
3.  Git runs that file from the working tree's root, and the file checks the staged changes.
4.  Git reads the exit code. Code 0 lets the commit continue, and any other code stops it.

Diagram: How Git runs a pre-commit hook

When the hook runs, Git records the commit only if the hook exits with code 0. The bypass flag records the commit without running the hook.

A hook can be 2 lines of shell. This one exits with a nonzero code when a staged line ends in whitespace:

```bash
#!/bin/sh
exec git diff --cached --check
```

Git does not copy hooks when someone clones a repository. A hook manager adds a committed config file and an install step that each clone needs. These open-source managers are common:

-   **pre-commit.** The pre-commit tool reads `.pre-commit-config.yaml`, and `pre-commit install` writes a script to `.git/hooks/pre-commit`. Each hook runs in its own environment at a pinned version.
-   **prek.** The prek tool is a single Rust binary that reads pre-commit's config file and hooks and needs no Python.
-   **Husky.** The Husky npm package sets `core.hooksPath` so Git runs hook scripts committed in a `.husky` folder.
-   **Lefthook.** The Lefthook tool is a Go binary that reads `lefthook.yml` and can run commands in parallel.

The [pre-commit documentation](https://pre-commit.com/) says the tool stashes unstaged edits while hooks run, so the checks read only the staged contents. Without a commit, `pre-commit run` checks the staged files, and `pre-commit run --all-files` checks every tracked file. Git's `git hook run pre-commit` runs the installed script.

## What is an example of a pre-commit hook?

Here is an illustrative example. Acme Co. sells furniture online. A developer at Acme is working on the request "Let customers edit their delivery address during checkout." The web store's repository has this `.pre-commit-config.yaml`:

```yaml
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: detect-private-key
```

The developer runs `pre-commit install --install-hooks` once, which also sets up each hook's environment. After editing `checkout/address.js`, the developer runs `git add .`, which also stages a local key file by mistake. The next `git commit` starts the hook, which prints this output:

```text
$ git commit -m "Let customers edit the delivery address"
trim trailing whitespace.................................................Failed
- hook id: trailing-whitespace
- exit code: 1
- files were modified by this hook

Fixing checkout/address.js

fix end of files.........................................................Passed
detect private key.......................................................Failed
- hook id: detect-private-key
- exit code: 1

Private key found: config/payments.pem
```

Git records no commit. The whitespace hook already fixed the file, so the developer stages the fix, unstages the key file with `git restore --staged config/payments.pem`, adds it to `.gitignore`, and commits again. All three hooks pass, and Git records the commit. Because the first commit failed, the key never entered the repository's history. The key check is a basic form of [secret scanning](https://specstory.com/learning/glossary#secret-scanning), and a CI check would run only after the key reached the server.

The hook read the text of the staged files and never started the store. The change also had a bug that only a running store shows. The address saved, but the cart emptied. This example is simplified. A real repository would also pin a linter for its language, e.g. ESLint for JavaScript.

## What changes when a coding agent writes the code?

A [coding agent](https://specstory.com/learning/ai-coding/coding-agent) commits by running `git commit` in a shell, so the pre-commit hook runs inside the agent's session. The hook's output returns to the agent as the result of that command, and the agent can edit the files and try again. A hook that fixes files, e.g. `trailing-whitespace`, still fails the commit. The agent has to stage the fixed files before it commits again, or the hook fails a second time.

The loop has two weak points. Git skips the hook when the command includes `--no-verify` or its short form `-n`. An agent can also weaken the check instead of fixing the code, e.g. by adding the failing file to an `exclude` pattern in `.pre-commit-config.yaml`. When a hook runs tests, editing a failing test instead of the code is [test tampering](https://specstory.com/learning/verification/test-tampering).

A team can run the same hooks again in CI, where neither flag has any effect, and review each change to `.pre-commit-config.yaml` in the diff. [Agent hooks](https://specstory.com/learning/ci-cd/agent-hooks) add checks at fixed points in the agent's own loop, e.g. when the agent tries to end its turn.

## What are the limits of pre-commit hooks?

A pre-commit hook gives fast feedback, but it has four limits:

-   **It is optional.** A developer who never runs the install step has no hook, and a code host's web editor never runs one.
-   **It checks only the staged files.** A change can break a file it did not touch, and the hook does not read that file.
-   **It reads code without running it.** Most hook checks are [static analysis](https://specstory.com/learning/code-review/static-analysis), so they miss failures that only [dynamic analysis](https://specstory.com/learning/code-review/static-vs-dynamic-analysis) finds, e.g. the emptied cart.
-   **It adds a wait to every commit.** People skip a slow hook.

Checks that belong in the hook finish in seconds and read one file at a time:

-   **Formatting.** A formatter rewrites each staged file's layout.
-   **Linting.** A linter reports code that breaks a rule.
-   **Secrets.** A secret check stops a key before it enters the history.
-   **File checks.** A file check rejects merge conflict markers and oversized files.

Slower checks, e.g. a full run of the [unit tests](https://specstory.com/learning/testing/unit-testing), usually run in CI or as [local CI checks](https://specstory.com/learning/ci-cd/local-ci-checks) before a push. A passing hook is not the same as working software.

## How is a pre-commit hook different from a CI check?

A pre-commit hook runs on the developer's machine before the commit exists, and a CI check runs on a server after a push. The hook reads the staged files in one working copy. The CI check usually starts from a fresh copy of the pushed code, so it can build the project and run slower tests. A code host can require a CI check to pass before a merge, but it cannot require a local hook.

Teams often run the same hooks in both places, e.g. with `pre-commit run --all-files --show-diff-on-failure` as a CI step, which prints the changes a failing hook made. A pre-push hook sits between the two, and the [pre-commit, pre-push, and CI](https://specstory.com/learning/ci-cd/pre-commit-vs-pre-push-vs-ci) comparison shows which checks fit each place.

## FAQs

### How does pre-commit work?

The pre-commit tool works from a config file in the repository that lists hooks and pins the version of each hook repository. Its install command writes one script into the Git hooks folder. On each commit, the tool stashes unstaged edits, runs the listed hooks on the staged files, and stops the commit if any hook fails.

### Do pre-commit hooks work with any Git repository?

Pre-commit hooks work in any Git repository where commits are made with Git on a local machine. Each clone needs its own install, because Git does not copy hooks when a repository is cloned. Commits made elsewhere, e.g. in a code host's web editor, never run the local hook.

### How do you run pre-commit hooks manually?

Pre-commit hooks can run manually without a commit. With the pre-commit tool, pre-commit run checks the staged files, and the same command with the flag for all files checks every file that Git tracks. Git can also run the installed script with git hook run pre-commit.

### How do you use pre-commit in CI?

Teams use pre-commit in CI by adding a pipeline step that runs every hook against all files. That step catches commits that skipped the local hook or came from a clone without it. An option that shows the diff on failure prints what a fixing hook changed, so the log shows what to correct.

---

Source: [What is a pre-commit hook? | Using pre-commit | SpecStory](https://specstory.com/learning/ci-cd/pre-commit-hook)
