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:

~/.config/git/config
[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.

~/.config/git/hooks/pre-commit
#!/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 0

The 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.

~/.config/git/hooks/post-receive
#!/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
fi

But 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:

I now define the nocheckin-hook in my user-config like this:

~/.config/git/config
[hook "nocheckin"]
	event = pre-commit
	command = ~/.config/git/hooks/nocheckin

hooksPath 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" },
    },
},