Skip to content

What is the difference between pre-commit, pre-push, and CI checks?

Pre-commit, pre-push, and CI checks run at different moments, before a commit is saved, before it leaves the machine, or on a server after it arrives.

Last updated , 8 min read

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.

Where each check runs Edit code git commit git push Pull request Agent hook agent's loop Pre-commit check staged files Pre-push check commits about to leave CI check fresh checkout Developer's machine Shared server
The two Git hooks run on a machine the developer controls, so the developer can skip them. The CI check runs on a server the team controls.

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:

  1. The agent edits the checkout code and runs git commit. The linter reports an unused variable, and Git records nothing.
  2. The agent deletes the variable, runs git add, and commits again. The linter passes.
  3. The agent runs git push. The hook runs npm test, and the unit tests pass.
  4. CI builds the app from a fresh copy and runs an end-to-end test of checkout.
  5. The test fails. The address saved, but the cart emptied.
  6. 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-files step.
  • 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:

AttributePre-commitPre-pushCI
Runs atgit commit, before the commit is recordedgit push, before commits are sentAfter commits reach the remote
Runs onThe developer's machineThe developer's machineA shared server in a clean environment
InputStaged files with a hook manager, else the working folderThe commits about to be pushedA fresh copy of the pushed code
Time budgetSecondsUp to a few minutesMinutes or longer
Typical checksFormatter and linterUnit tests and type checksFull suite and end-to-end tests
Can be skippedYes, with --no-verify or a skip variableYes, with --no-verify or a skip variableOnly 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.

Join the RunStory alpha →

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.