Qvantor logo QvantorInfrastructure, explained plainly
Dev Tools

Git Pre-Commit Hooks That Small Teams Actually Keep Using

A teammate pushes a commit with a stray console.log , a 40 MB video file and an AWS key in a config file. The reviewer catches the log line, misses the key, and the video sits in the repository history forever.

A terminal window showing a refused Git commit with a short list of failed checks

A teammate pushes a commit with a stray console.log, a 40 MB video file and an AWS key in a config file. The reviewer catches the log line, misses the key, and the video sits in the repository history forever. Git pre-commit hooks exist to stop exactly this kind of afternoon, and on a small team they are one of the cheapest safety nets you can set up.

I have rolled out hooks on four teams between three and twelve people. Two of those rollouts stuck. The other two were quietly bypassed within a month. The difference had very little to do with which tool we picked and a lot to do with how slow, how strict and how shared the hooks were. Here is what I would do again, and what I would skip.

What Git Pre-Commit Hooks Actually Do

A Git hook is a script that Git runs at a fixed moment. The pre-commit hook runs after you type git commit and before the commit is written. If the script exits with a non-zero status, the commit is refused and the developer sees the output.

That is the whole mechanism. Everything else, from formatters to secret scanners, is just a program the hook calls. The hook lives in .git/hooks/pre-commit, which matters for one reason: the .git folder is not versioned. A hook you write there exists only on your machine. This single detail explains most failed rollouts.

The other hooks worth knowing

  • commit-msg checks the message itself, useful if you enforce a ticket number.
  • pre-push runs before code leaves the laptop, a better home for anything that takes more than a few seconds.
  • post-checkout can remind people to reinstall dependencies after switching branches.

Sharing Hooks So Everyone Runs the Same Checks

Because hooks are not committed, you need a way to distribute them. There are three common routes, and the right one depends mostly on what language your repository already speaks.

ApproachFits bestSetup cost
core.hooksPath pointing at a versioned folderAny repo, no extra toolingOne command per clone
The pre-commit framework (Python)Polyglot repos, many lintersA YAML file and one install
Husky with lint-staged (Node)JavaScript and TypeScript projectsRuns on npm install

The simplest option is underrated. Put scripts in a .githooks folder, commit it, and ask each developer to run git config core.hooksPath .githooks once. Add that line to your setup script so nobody has to remember. If your onboarding already struggles with machines that drift apart, read our piece on how a local setup slowly drifts away from production, because hooks suffer from the same problem.

The Five Checks That Earn Their Place

A pre-commit hook should catch mistakes that are cheap to detect and expensive to fix later. On every team I have worked with, these five paid for themselves within weeks:

  1. Secret scanning. Tools like gitleaks or detect-secrets look for patterns such as private keys and tokens. A leaked key in history means rotation, which is painful, as we covered in why rotating API keys trips up small teams.
  2. Large file block. Refuse anything over, say, 5 MB. One bad commit can add hundreds of megabytes to every future clone.
  3. Formatting on staged files. Prettier, Black or gofmt, applied only to what is staged, ends the whitespace debates in review.
  4. Merge conflict markers. A one-line grep for leftover conflict markers saves an embarrassing broken build.
  5. Trailing whitespace and line endings. Boring, but mixed CRLF and LF files cause noisy diffs on teams that mix Windows and macOS.

Why Running the Full Test Suite on Commit Is a Mistake

The most common advice I hear is to run every test and the full linter on each commit, so broken code can never enter the repository. It sounds responsible. In practice it is the fastest way to teach a team to type the no-verify flag on git commit.

Developers commit often, sometimes every few minutes, and often with half-finished work on a feature branch. If a commit takes 90 seconds, people stop committing small, which makes history worse, or they bypass the hook, which makes the hook pointless. My rule is blunt: the pre-commit hook must finish in under five seconds on a typical change.

Tests belong in two other places. Fast unit tests can go in a pre-push hook, where a 30 second wait is tolerable because it happens a few times a day. The full suite belongs in continuous integration, which runs on clean machines and cannot be skipped by a tired developer at 6 pm. Hooks are a convenience layer; CI is the gate.

Keeping Hooks Fast on Large Repositories

Speed comes from doing less work, not from faster machines. Three habits keep hooks quick even on repositories with thousands of files.

Only check staged files

Ask git diff with its cached and name-only options for the list of added, copied or modified files, and pass only those to your tools. lint-staged and the pre-commit framework do this for you. Running a linter across the whole tree on every commit is where most slow hooks come from.

Pin tool versions

If one developer has Prettier 2 and another has Prettier 3, the hook will reformat the same file back and forth. The pre-commit framework pins versions in its config file, and Node projects can pin in package.json. Either way, the version should live in the repository.

Cache aggressively

The first run of the pre-commit framework downloads environments and can take a minute. After that it is cached. Warn new joiners so they do not decide the hook is slow on day one.

Rolling Out Hooks Without a Revolt

The two rollouts that failed for me had the same shape: someone added ten strict checks on a Friday, and on Monday every commit failed on files nobody had touched in years. Here is the order that worked instead.

  • Week one, add formatting and the large file block only, and run the formatter once across the whole repository in a single commit so later diffs stay clean.
  • Week two, add secret scanning in warning mode, review what it flags, and build an allowlist for false positives such as test fixtures.
  • Week three, switch secret scanning to blocking and add the conflict marker check.
  • Write a short section in the README explaining each check and how to fix a failure in one command.

Accept that the no-verify flag will exist. You cannot stop it locally, and you should not try. What you can do is make CI run the same checks, so a bypass only delays the feedback rather than removing it.

Frequently Asked Questions

Can a pre-commit hook be enforced on every developer?

No. Hooks run on the developer's machine and can always be skipped. Treat them as a fast early warning and repeat the important checks in CI, where they cannot be bypassed.

Do hooks work the same on Windows?

Mostly. Git for Windows runs hooks through its bundled shell, so plain shell scripts work. Watch line endings: a hook saved with CRLF endings can fail with a confusing "not found" error. Set the hooks folder to LF in .gitattributes.

What should happen when the formatter changes a file?

Most tools fix the file and fail the commit, so you can review the change and commit again. Some teams prefer auto-staging the fix. I would avoid that at first, because silent changes confuse people who are new to the setup.

Start With Two Checks This Week

If you do one thing, add a versioned hooks folder with a large file block and a secret scanner, keep it under five seconds, and mirror both in CI. That covers the two mistakes that are hardest to undo. Once the team trusts it, add formatting. More ideas for small team tooling live in our dev tools section.

MQ
Marisa Quan

Marisa hosts game servers for her friends and writes about latency, uptime and the people who keep servers alive.

More posts by Marisa

More in Dev Tools

01doorganico 084goad 3hot8 4465456 after5internprogram ahandfulofblessings ahmadpc ahomcybersecurity allodialresources annodex antiquesandperiodcollectibles aotc asianworldkashmir beedlectrics bloggertemplatefree bluefeatheragency bodyweightsolution campaign08blog carfixadvisor catstreetcollective cbstvsports cnfanspurchasing committeeof500yearsofdignityandresistance covertopsdjs cs-kaiyuangame cuestacomunicaciontotal cxoutlet dancebetgame deskgearhub destinyvillastay detectmagazine di33 diamondsteelbuildings donmo dyanadropship emarsysfinance emimattl escapechan eskitarih evautoplay everythingweddings-orlando farmzone feedviewerapp foxstudiosscratch gamevui123 gleasongrade greetingsday hacklabasu haskell iw-kaiyun jagoanproperti jilidskripsi joltbusinessfinance juegodepokergratis juegospucca kaeru-festival latestmehndidesigns lestanzediottaviano litemagazines localcommunityhero lustrsazan mariashebets menswheels newwebstechs1 nikkit ootheday oxigenvpn plurfitness prefetial probudgetcarpetcleaning psychicmrsmartin pyaten rejoignezlogis rexzbot riselayouts ruchin rwxqsgd5 sakaltime shibiaowang shop-bot skrtips strcoding tetapatrium topselller uneplanteartificielle vipertechcr webtech1223411 wo-kygaming yamaha-mekar yeknava yoybuysheets zenunwa yamaha-mekar jilidskripsi ankertoto ankertoto ankertoto ankertoto