We’ve all been there: you push a change, wait five minutes for the CI/CD pipeline to trigger, only to find out you forgot a semicolon or broke a basic unit test. It’s a waste of time and compute resources. In my experience, the most effective way to solve this is by shifting your quality gates left. Using git hooks for automated testing allows you to catch these regressions on your local machine before a single line of code leaves your workstation.

Git hooks are scripts that Git executes before or after events such as commit, push, and receive. While they are powerful, if you configure them poorly, your team will either bypass them using --no-verify or grow to hate them because they take ten minutes to run. Here are 10 tips to implement them the right way.

1. Stick to the ‘Pre-Commit’ Hook for Linting

The pre-commit hook is the most critical for automated testing. I recommend using it exclusively for “fast” checks: linting, formatting, and type-checking. If a check takes longer than 5 seconds, it doesn’t belong here. For example, running Prettier or ESLint ensures that your code adheres to the conventional commits specification guide and style rules before it’s even staged.

2. Use Husky for Team Synchronization

By default, the .git/hooks directory isn’t tracked by version control. This means you can’t simply commit your hooks and expect your teammates to have them. I’ve found that Husky is the industry standard for JavaScript/TypeScript projects to solve this. It allows you to define hooks in your package.json, ensuring every developer on the project is running the same automated tests locally.

3. Only Test Staged Files (The ‘Staged Only’ Rule)

Running your entire test suite on every commit is a productivity killer. Instead, use tools like lint-staged. This ensures that if you’ve changed 2 files in a project of 2,000, only those 2 files are validated. This keeps the feedback loop tight and prevents developers from bypassing the hooks.

# Example lint-staged configuration in package.json
"lint-staged": {
  "*.{js,ts}": ["eslint --fix", "jest --findRelatedTests"],
  "*.json": ["prettier --write"]
}

4. Implement ‘Pre-Push’ for Heavy Integration Tests

While pre-commit is for linting, the pre-push hook is where your heavier automated tests should live. I use this hook to run a subset of integration tests or a smoke test suite. It’s a slightly slower gate, but it’s far better to wait 60 seconds before a push than to wait 10 minutes for a CI failure.

5. Fail Fast and Provide Clear Instructions

There is nothing more frustrating than a hook that fails with a generic “Error: 1”. When you write your scripts for git hooks for automated testing, ensure the output tells the developer exactly how to fix the issue. Instead of just failing, print a message like: "❌ Linting failed. Run 'npm run lint:fix' to resolve automatically."

6. Create a ‘Bypass’ Protocol

Sometimes, you need to commit a work-in-progress (WIP) branch just to save your state. While --no-verify exists, I prefer encouraging a workflow where WIP commits are pushed to a personal feature branch that doesn’t trigger the main CI pipeline. This maintains the integrity of the hooks while allowing flexibility.

7. Validate Commit Message Format

Automated testing isn’t just about code; it’s about metadata. I use the commit-msg hook to enforce a specific format. This is essential when scaling git for monorepos best practices, as it allows for automated changelog generation and easier debugging of the history.

8. Integrate Type Checking (TSC)

For TypeScript users, running tsc --noEmit in a pre-commit hook is a lifesaver. It catches type mismatches that ESLint might miss. To keep it fast, I’ve experimented with using tsc-files to check only the files being committed, though be careful as this can occasionally miss cross-file type dependencies.

9. Avoid Network Dependencies

Your local hooks should never rely on an external API or network call. If your tests require a database, use a local Docker container or a mock. If a hook hangs because the company VPN is down, your developers will simply disable the hooks entirely.

10. Keep Hooks Modular

Don’t write one giant shell script. Instead, create a folder like .hooks/ containing separate scripts for lint.sh, test.sh, and typecheck.sh. This makes them easier to maintain and allows you to run them independently during manual development.

Terminal output showing a successful sequence of pre-commit hooks including linting and testing
Terminal output showing a successful sequence of pre-commit hooks including linting and testing

As shown in the image below, a properly configured hook setup creates a visual safety net that transforms the terminal from a place of “hope it works” to a place of “I know it works.”

Common Mistakes I’ve Seen

Measuring Success

How do you know if your git hooks for automated testing are working? I track two metrics:

  1. CI Failure Rate: You should see a significant drop in “trivial” CI failures (linting, formatting).
  2. Cycle Time: The time from the first commit to a successful merge should decrease because the back-and-forth with the CI is minimized.

Ready to automate your workflow? Start by adding a simple pre-commit linting hook today and feel the immediate difference in your development speed.