Using Git Pre-Commit Hooks
Published on 20 Oct 2025 · Filed in Explanation · 645 words (estimated 4 minutes to read)A while ago I wrote an article about linting Markdown files with markdownlint. In that article, I presented the use case of linting the Markdown source files for this site. While manually running linting checks is fine—there are times and situations when this is appropriate and necessary—this is the sort of task that is ideally suited for a Git pre-commit hook. In this post, I’ll discuss Git pre-commit hooks in the context of using them to run linting checks.
Before moving on, a disclaimer: I am not an expert on Git hooks. This post shares my limited experience and provides an example based on what I use for this site. I have no doubt that my current implementation will improve over time as my knowledge and experience grow.
What is a Git Hook?
As this page explains, a hook is a program “you can place in a hooks directory to trigger actions at certain points in git’s execution.” Generally, a hook is a script of some sort. Git supports different hooks that get invoked in response to specific actions in Git; in this particular instance, I’m focusing on the pre-commit hook. This hook gets invoked by git-commit (i.e., the user running a git commit command) and allows users to perform a series of checks or tests before actually making a commit. If the pre-commit script exits with a non-zero status, then Git aborts the commit.
Using a Pre-Commit Hook to Lint Markdown
For my use case, I wanted to run Markdownlint to lint the Markdown files (as described in the previous article on linting Markdown) before the commit. To do that, I came up with the following script:
#!/usr/bin/env bash
MDLINT="/usr/local/bin/markdownlint"
for file in $(git diff --cached --name-only --diff-filter=ACM | grep "\.md"); do
if ! $MDLINT "$file"; then
echo "Lint check failed on file '$file'."
echo "Run markdownlint to identify the errors and try again."
exit 1
fi
done
To make this script active as a pre-commit hook, you must name it pre-commit, you must place it into Git’s hook directory (which defaults to .git/hooks in the current repository), and you must make it executable.
This script is readable enough for most folks to understand what it is doing, but there are a couple of things that might be helpful to point out:
- The
git diff --cached --name-onlycommand will return a list of the files staged for commit usinggit add. This list of files is what will Markdownlint will check. - The
--diff-filter=ACMparameter tellsgit diffto return only Added, Copied, and Modified files. This preventsgit difffrom returning the filename of a deleted file, which would then generate an error from Markdownlint. - Using
grephere further restricts the list of files to include only Markdown files. - The
exit 1command is critical; without returning a non-zero exit code, Git wouldn’t know to abort the commit.
With the script in the right place and marked as executable, it’s automatically invoked by Git when I try commit any Markdown files. If the check succeeds, the commit proceeds as normal (my editor opens for me to edit the commit message); if the check fails, then I see an error message and the commit aborts. Exactly what I needed!
Additional Resources
I found the following articles useful when I was learning about pre-commit hooks and how to use them:
How to use git pre-commit hooks, the hard way and the easy way
Git Pre-Commit Hook: A Practical Guide (with Examples)
I hope you’ve found this post helpful in some fashion. As I previously mentioned, I don’t pretend to be an expert on this topic. If you spot an error or mistake in this post, then please reach out and let me know. You can reach me on Twitter, on the Fediverse, or in a variety of Slack communities. Positive feedback is also welcome—thanks for reading!