What is the difference between pre-commit, pre-push, and CI checks?
Pre-commit, pre-push, and continuous integration (CI) checks differ in when they run and which checks fit there. A pre-commit check runs before Git records a commit and suits fast checks, e.g. a linter. A pre-push check runs before commits leave the machine and suits unit tests. A CI check runs on a shared server and suits the full suite.
Earlier checks report sooner, while the change is still small. Later checks report later but are harder to skip. Git runs the two local checks as hooks, which each developer installs and can bypass. A CI/CD pipeline runs the third for each change the team shares, in a clean environment. Teams often combine all three, with the fastest checks first and the checks they must trust last.
What is a pre-commit check?
A pre-commit check is a check that runs when a developer or an agent runs git commit, before Git records the commit. Git starts it through a pre-commit hook, a script in the repository's .git/hooks folder by default. The Git hooks documentation states that a nonzero exit code from the script aborts the commit, and that git commit --no-verify bypasses it.
A plain hook script runs in the working folder, so it also reads unstaged edits. The pre-commit framework, an open-source hook manager, passes each hook only the staged files and sets unstaged edits aside first. Its SKIP variable skips named hooks, e.g. SKIP=lint, and HUSKY=0 turns off Husky's hooks.
The hook runs at each commit, so it has the smallest time budget of the three places. It suits checks that finish in seconds, e.g. a formatter.
What is a pre-push check?
A pre-push check is a check that runs when a developer or an agent runs git push. A pre-push hook is a script that Git runs before it sends commits to a remote repository. If the hook exits with a nonzero code, Git sends nothing. The --no-verify flag of git push skips the hook.
Developers push less often than they commit, so a pre-push check can take longer, e.g. to run the unit tests. Commits then stay fast. A hook manager, e.g. Husky, stores the script in the repository as .husky/pre-push. A plain pre-push script runs in the working folder, so its tests also read edits that no commit includes.
What is a CI check?
A CI check is a check that a shared server runs after commits reach the remote repository, usually on each push or pull request. A CI service, e.g. GitHub Actions, starts from a fresh copy of the repository, so it checks only what was committed and pushed.
CI has the largest time budget and earns the most trust. After the build, a smoke test can confirm that the app starts before slower end-to-end tests run. A code host can make the check blocking, so a pull request cannot merge until it passes, e.g. as a required status check in a GitHub branch protection rule. Only people allowed to bypass that rule can then skip it. The cost is time, because the result arrives after the change has left the developer's machine.
When should a team use each place?
Here is an illustrative example. Acme Co. sells furniture online. Its team puts each check at the earliest place where it runs fast and gives the same result on any machine. A developer at Acme asks a coding agent to "Let customers edit their delivery address during checkout." Acme's .pre-commit-config.yaml runs a linter before each commit and the unit tests before each push:
repos:
- repo: local
hooks:
- id: lint
name: lint
entry: npx eslint
language: system
types: [javascript]
stages: [pre-commit]
- id: unit-tests
name: unit tests
entry: npm test
language: system
pass_filenames: false
stages: [pre-push]
Running pre-commit install -t pre-commit -t pre-push sets up both hooks. The change then moves through the three places:
- The agent edits the checkout code and runs
git commit. The linter reports an unused variable, and Git records nothing. - The agent deletes the variable, runs
git add, and commits again. The linter passes. - The agent runs
git push. The hook runsnpm test, and the unit tests pass. - CI builds the app from a fresh copy and runs an end-to-end test of checkout.
- The test fails. The address saved, but the cart emptied.
- The agent fixes the cart code and pushes again, and CI passes.
The linter caught the cheap problem in seconds. Only CI started the whole app, so only CI caught the empty cart. This example is simplified. A real project would also scan for secrets before each commit.
What changes when a coding agent writes the code?
A coding agent can commit many times per task, so a slow pre-commit hook adds its wait to each commit. The agent can also run git commit --no-verify, so a check the team relies on must run in CI too.
An agent hook adds a fourth place, inside the agent's loop, where a check can run before the agent commits. A Stop hook runs when the agent tries to end its turn, so it can run unit tests that are too slow for a pre-commit hook.
A background coding agent works in a remote sandbox and returns a branch or pull request. Git does not copy hooks when it clones a repository, so hooks run there only if the sandbox's setup installs them.
Anthropic reported that its CI job volume grew 25-fold in six months as Claude came to write about 80% of its code. At that volume, a fast check that fails in an agent hook or a Git hook stops a broken change before it adds a CI run.
Can pre-commit, pre-push, and CI checks be used together?
Pre-commit, pre-push, and CI checks can be used together on the same change. The local hooks catch cheap mistakes early, and CI runs the integration and end-to-end tests that need a clean machine. Using all three still has limits:
- Local hooks are optional. Each developer has to install them, and anyone can skip them, so CI can rerun them, e.g. with a
pre-commit run --all-filesstep. - A local pass can differ from CI. Uncommitted files and tool versions can differ between a laptop and CI.
- Flaky checks wear down trust. A flaky test in a hook fails commits at random, so developers start to skip the hook.
- Repeated slow checks cost time. Running the full suite at push time and again in CI doubles the wait.
A green result in all three places shows only that the chosen checks passed, not that the whole app works.
How do pre-commit, pre-push, and CI checks compare?
The three places differ on these attributes:
| Attribute | Pre-commit | Pre-push | CI |
|---|---|---|---|
| Runs at | git commit, before the commit is recorded | git push, before commits are sent | After commits reach the remote |
| Runs on | The developer's machine | The developer's machine | A shared server in a clean environment |
| Input | Staged files with a hook manager, else the working folder | The commits about to be pushed | A fresh copy of the pushed code |
| Time budget | Seconds | Up to a few minutes | Minutes or longer |
| Typical checks | Formatter and linter | Unit tests and type checks | Full suite and end-to-end tests |
| Can be skipped | Yes, with --no-verify or a skip variable | Yes, with --no-verify or a skip variable | Only by people allowed to bypass a required check |
How does RunStory help with running checks?
Local checks and CI run what a team has set up, and the tests that start the whole app usually wait for CI. RunStory runs your software in a separate environment, tries relevant workflows, and checks the results while you keep working. It sends reproducible failures back to your coding agent. It is in private alpha for CLIs and web apps, and your team keeps the final release decision.
FAQs
Should you use a pre-commit or pre-push hook?
A pre-commit hook suits checks that finish in seconds, e.g. a formatter, because it runs at every commit. A pre-push hook suits slower checks, e.g. unit tests. Teams often use both and keep the full suite in CI.
Is it good practice to run unit tests in version-control hooks?
Running unit tests in a Git hook is good practice when the tests are fast and reliable, and a pre-push hook fits them better. Slow or flaky tests in a hook lead developers to skip it, so CI should still run the full suite.
Should CI rerun what a pre-commit hook already ran?
CI should rerun the checks that a pre-commit hook ran, because a developer or an agent can skip a local hook or never install it. A CI step can run the same hooks, and fast checks cost little to repeat.
Which checks are too slow for a pre-commit hook?
Checks that take longer than a few seconds are usually too slow for a pre-commit hook, because the hook runs at each commit. Unit tests fit a pre-push hook, and the full suite and end-to-end tests belong in CI.