Updating My nocheckin Hook
To the new and improved hooks introduced in Git 2.54.
A while ago I had the idea of writing a rant on Git and its implementation of
hooks but it seems like they updated the hook system before I took the time to
do it. In this article I will instead describe my old nocheckin-hook and how
I updated it to the new and improved system.
The nocheckin Hook
I have watched a lot of programming streams by Jonathan Blow and a
trick that he uses is nocheckin-comments. When he tries to commit a
nocheckin his source control (SVN) blocks the commit and prevents him from
committing until the nocheckin has been removed. It functions as a short-term
reminder that can be put next to e.g. old code that should be revised, code
that should be documented, temporary overrides, debug stuff, etc., so that you
remember to actually finalize that extra thing that you thought of before
committing. It is easy to forget but not if you leave a nocheckin there.
int complicated_function(int a, int b, int c, int d) {
// nocheckin: explain function
if (a*c - b*d == 0) {
return 5;
}
[...]
}I don’t use SVN though so I adapted this workflow to Git.
The Old Method
Obviously, I want the nocheckin-hook to apply to all repositories I work in
and it should work without having to set it up for each repository. Therefore,
in my Git user-config I set:
[core]
hooksPath = ~/.config/git/hooks/This makes it so that every single repository uses the hooks in hooksPath
instead of the repository-hooks in <repository>/.git/hooks. In hooksPath
I put a pre-commit-hook which checks if the staged contents include the
string nocheckin. If it finds the string in the added lines the commit is
blocked by returning a non-zero value.
#!/bin/sh
#
# This pre-commit hook aborts the commit if the commit contains the string
# 'nocheckin'.
#
#
# NOTE(Neogit): When committing using Neogit it commits using the -c
# color.ui=always option. This inserts ANSI color codes at the beginning of
# each line in the diff command which causes the grep "^+" to not match
# anything => commits containing 'nocheckin' are not blocked.
#
# Workaround: Add -c color.ui=never in the diff command to remove the color
# codes.
#
N=$(git -c color.ui=never diff --staged | grep "^+" | grep -i "nocheckin")
if [ $? -eq 0 ]; then
echo "ERROR: Attempted to commit a 'nocheckin'. Commit aborted."
echo ""
# Print the found occurrences of nocheckin.
# TODO: print filename:linenumber for each line.
echo "$N"
exit 1
fi
exit 0The large comment explains a workaround I had to implement so that the hook would work with both Git CLI and Neogit.
But setting hooksPath has some issues. This is what the rant was going to be
about. It is quite a large change since by setting it all hooks in hooksPath
become global and apply to all of your repositories. Furthermore, all of your
prior repository-hooks are effectively disabled. This might break a lot stuff.
I don’t use a lot of hooks though so this solution was good enough for me.
As an aside, I have tried to run repository-hooks by executing them directly from the user-hooks and it does seem to work(?) even though it feels like a fragile hack. You need one of these scripts for each hook that you care about.
#!/bin/sh
# In the user-hook ~/.config/git/hooks/post-receive run the
# repository-hook post-receive if it exists.
# Check $PWD!
# $PWD is either the repository directory or the .git-directory
# inside the repository (depending on the hook and repository).
if [ -f "./hooks/post-receive" ]; then
./hooks/post-receive
fiBut we don’t have to bother since there is a better method.
The Git 2.54 Method
The new method, introduced in Git 2.54 (released in April 2026), allows hooks to be defined in the config-files. There are multiple config-files available to choose between:
/etc/gitconfigfor system-wide hooks,~/.config/git/configfor user-hooks,<repository>/.git/configfor repository-hooks.
I now define the nocheckin-hook in my user-config like this:
[hook "nocheckin"]
event = pre-commit
command = ~/.config/git/hooks/nocheckinhooksPath is no longer set and the nocheckin-script is the
pre-commit-script from earlier. I decided to keep the script in the old
hooksPath directory as it is managed, along with the entire git-config
directory, by Stow. If you expect to run the script outside of Git then
~/.local/bin or something is probably a better location for it.
References
Bonus: todo-comments.nvim
I use todo-comments.nvim and I have configured it to highlight nocheckins:
keywords = {
-- Add a 'nocheckin' keyword.
NOCHECKIN = {
icon = " ",
color = "error",
alt = { "nocheckin" },
},
},