Put fast, low noise checks in pre-commit, slower ones in pre-push or on demand, and anything you must be able to prove in CI as a required status check. Three questions decide it: how long does it take, can it be skipped, and who needs to see the result.
The three questions
How long does it take? A hook that adds ten seconds to every commit trains people to bypass it. The githooks documentation confirms the bypass is one flag away for pre-commit.
What happens if someone skips it? If you would want to know, it needs a server side copy.
Who needs the evidence? A reviewer or auditor can see a CI result on a pull request. They cannot see what ran on a laptop.
A starting split
| Check | Pre-commit | Pre-push | CI required |
|---|---|---|---|
| Formatting and whitespace | Yes | No | Optional |
| Private key and obvious secret patterns | Yes | No | Yes |
| Large file blocker | Yes | No | Yes |
| Unit tests | No | Optional | Yes |
| Dependency and SBOM scan | No | No | Yes |
| Static analysis on changed files | Optional | Optional | Yes |
The pre-commit framework supports this with stages. A hook can be limited to [pre-commit, pre-push], and stages: [manual] makes it run only on demand. The command to install the pre-push type is in the block below.
pre-commit install --hook-type pre-push
Why duplicate at all
It looks wasteful to run the secret check twice. The reason is the cost of finding the problem late. A secret caught at commit time never enters history. One caught only in CI is already in a commit on a branch that may have been pushed.
GitHub's push protection documentation draws the same line: blocking before the push is preventive, while scanning afterward means the secret "must be remediated after exposure".
A config that serves both
default_stages: [pre-commit]
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: detect-private-key
- id: check-added-large-files
Then in CI run the same hooks over every file. One file, two enforcement points.
pre-commit run --all-files
Signs your split is wrong
- Developers routinely bypass hooks: the hooks are too slow or too noisy.
- CI failures are usually things a hook could have caught: move them earlier.
- Nobody can show an auditor when a control ran: move it to CI.
For more on the bypass problem see why local hooks need a CI backstop.
Safeguard's policies and gates can hold a pull request on dependency findings, which fits the required CI column. A free scan is available at gold.safeguard.sh/scan.
A worked example
Take a secret check. Locally, a hook scans staged files for key blocks in about a second, so the developer fixes it before committing. In CI, the same check runs over the whole pull request and is marked required. If a developer skipped the hook, CI still stops the merge.
Now take a full dependency scan. It may need network access and a minute or more. Running it on every commit would be miserable, so it lives in CI only, and developers can run it by hand when they want an early look.
Quick check
Should security scanning run in pre-commit?
Only the fast, low noise parts such as key detection. Full dependency or static analysis scans usually belong in CI where they are required and visible.
Sources
Last reviewed: 19 October 2026.