Discussion of the Benefits and Drawbacks of the Git Pre-Commit Hook
The Git pre-commit hook is a commonly used tool for automated quality assurance in the contribution process to software projects. It is run by default when a user creates a commit and prevents the commit from being created if it fails, i.e. exits with a non-zero exit code. Setting up a pre-commit hook is as simple as putting a script named pre-commit
into the .git/hooks
folder. The pre-commit hook can be a powerful tool, since it is the last chance to prevent sensitive information from being tracked by version control, which can be difficult to clean up if it is noticed too late.
This text does not concern itself with the pre-receive hook or other Git hooks, for which different benefits and drawbacks might be true. It is only focused on the pre-commit hook, since it is the most widely used hook and the one I was recently confronted with.
Using the Pre-Commit Hook
Since the hooks folder is not part of Git’s version control, setting up the pre-commit hook is not trivial to do for new contributors to a project and requires some tools to set up easily. There are various tools for this - Pre-Commit is a standalone tool, Prek is a work-in-progress Rust re-engineering of Pre-Commit, and Husky requires Node.js and npm and supports other Git hooks as well.
Pre-Commit includes dependency management of hooks, which are written and packaged for it. Common quality assurance tools like Gitleaks, ESLint or Nixfmt include a manifest with a configuration for Pre-Commit, which makes them installable with it.
Husky relies on users using npm to install it and any other dependencies, and its hooks are predominantly written in JavaScript/TypeScript. It is more of an ecosystem-internal solution than a tool for software projects in general.
Without external tools, a common and simple way to set up hooks and to track the hooks in Git’s version control is to create symlinks from the hooks folder to the repository with something like ln -s $(pwd)/scripts/pre-commit .git/hooks/pre-commit
. The symlink has to be created only once after cloning the repository, which works well for long time contributors, but is an easily overlooked step during onboarding.
Benefits, Drawsbacks and Everything In-Between
Preventing Leaked Secrets
Secret strings should never be committed into version control and the pre-commit hook is the last point in time to prevent that. Once committed, secrets can be hard to remove and an accidental push can add the secrets to the - in some cases public - remote repository. Experienced Git users can find their way around this, but preventing this from happening in the first place is still more secure and helps prevent mistakes from less experienced users. Gitleaks and other tools can quickly scan changed files for common patterns of secret strings like API tokens, SSH keys, GPG keys and others. Including Gitleaks in a pre-commit hook is unintrusive, since it is quite fast, and gives immediate feedback, thus being a huge security benefit without much friction.
Formatting, Linting, Testing
Other changes are not necessarily problematic when pushed to a remote repository, but will most likely be rejected during code review, namely formatting issues, linter issues and failing tests. Formatting issues can be prevented by auto-formatting in the pre-commit hook, which requires zero user intervention. Committing files without reviewing them, even automatically changing them upon commit, still feels wrong to me, but in the case of formatters there is no discussion about the result anyway and if something goes wrong it will be caught in code review afterwards. Linter issues and failing tests would usually be caught by QA pipelines before merging a pull request, so checking for these locally strictly isn’t necessary either, but catching them early tightens the feedback loop.
These tools can sometimes, depending on project size, take a while to run, which increases friction in the workflow. If they impede the workflow, their use should be reconsidered.
Reducing Mental Load
Most projects have scripts to format, lint and test before pushing. Oftentimes developers - and I’m definitely not an exception here - forget to run those, only for CI pipelines to fail two minutes later and prevent a merge. Having a pre-commit hook run these scripts automatically reduces mental load and increases the probability that issues are caught before code review.
Not Every Commit is Made to Satisfy QA
Most developers use Git to track incremental changes while working. We don’t commit only after a few days of work, but multiple times per day. Good developers clean up their commit history afterwards, to improve commit messages and to make commits individually semantically complete. But before that, commits are often made in incomplete states, including linter errors and most of all failing tests. Preventing commits due to failing tests can be a serious break in workflows and can and will make developers develop the habit of disabling pre-commit hooks, even setting aliases to do so.
Necessary Setup
Pre-commit hooks have to be installed for Git to know about them and to run them. This requires a setup step after cloning a repository, which is one step more in every onboarding process. This is not necessarily a problem, but requires attention and documentation. The setup can be simplified by integrating it with other setup steps, e.g. by using devenv to set up development environments. It can even be automated entirely by using something like direnv.
Slow Hooks are Annoying
If your workflow is to make many small commits, long running hooks are seriously annoying and can also lead to the scenario described above, where developers disable pre-commit hooks out of habit.
Conclusion
The biggest benefit, entirely preventing secret leaks, is definitely worth the setup of pre-commit hooks. However, the hooks need to be set up in a way that makes people actually use them, which means keeping them fast and reducing the friction in development workflows. What this means, exactly, depends on your project and your team. Formatters are fast, even most linters are and can be configured to fix issues without manual intervention and to ignore the rest, which means that no commits are ever blocked and some QA is ensured automatically. Running tests in hooks is probably a bad idea, since it can prevent work-in-progress commits and be a real nuisance.
My Workflow and Tooling Recommendation
I’ve recently started using devenv to set up my development environments. Devenv integrates Pre-Commit and makes it very easy to install and manage pre-commit hooks. This makes onboarding very easy, removes Pre-Commit’s package management and unifies package management using Nix, and even integrates pre-commit hooks in a central devenv test
command.
So far, this makes for a pretty good workflow for me. Having the pre-commit hook automatically fix formatting issues and some linter issues feels pretty good. I have not yet tested all of this in actual large projects and I suspect that friction will be much worse with more code, but intend to do test it and then re-evaluate.
Alternatives
I’ve seen projects include a make reviewable
command or script that runs the full QA pipeline and tells the user whether the current state of the repository is fit for review. This is much more manual, but does not interfere with the normal development workflow and is more explicit. However, users need to be told about it and they need to actively use it. Many code reviews in these projects begin with a feedback loop of “please run this script” - “oh, i didn’t know/forgot this exists”. More automation makes this smoother.