A few months ago I got a message from a random engineer on the company slack; he
told me that he reviewed one of my pull request, and I might want to go through
my code again, since I left some “inappropriate” debug logs there. I immediately
checked what he could’ve mean, and I found the println()
with a content that
was unmistakably created out of sheer rage and desperation caused by hours of
an unsuccessful bug hunting session. Unless you have a temper of a zen-master,
I bet you know what kind of bug I’m talking about.
I prefer to prevent “accidents” like this one - after all, this is what agile is about, right? So I started to look for a solution - then ended up at git hooks pretty fast. However, with git hooks we have basically two options:
- run the same
pre-commit
hook for every repository and skip the repo-defined hook (follow the red arrow)
or
- copy my
pre-commit
hook to every repository I work, worked and ever will work with (go as the green arrow). Neither is a good choice. 😕

What I want instead is to run both, basically to go through the dashed arrow. And there is a fairly simple way to achieve this.
Configuring core.hooksPath
A local hook is something that lives in the repository; it is getting executed on everyone’s machine who works on the repo.
A global hook lives on the machine instead - if I have global hooks then they will will be triggered for every repository I work on my machine, no matter what the repository is.
The issue is that git has only a single config entry for setting the hook path,
and this is a setting for the machine. We can set an absolute path here
(like /home/foo/bar
) or a path relative to $GIT_DIR
(like the default
value, $GIT_DIR/hooks
). It is intentional that we have a single directory;
git does not want to take up the responsibility to deal with execution orders
of multiple triggers. Fair.
What we could do instead is to write a global hook that executes the local hook as well 💡 and since the work directory of a running hook is the root of the repository, it is easy to call the local hook from the global one. 🎉
Execute global hooks THEN local hooks
Lets set the core.hooksPath
to an absolute path then
git config --global core.hooksPath $HOME/.git/hooks
…And now we can have a pre-commit
file in that directory:
#!/bin/bash
# execute global functions here...
# ... then call the local hook
if [ -f .git/hooks/pre-commit ]; then
.git/hooks/./pre-commit
fi
Since executing the local hook is the last step, the global hook will exit with the same code as the local hook did.
Swear-word filtering logic
We can abort the commit process with exiting with a non-0 code from the hook.
We will use git diff --cached
to get all the changes since the last commit
and grep -if FILE_WITH_EXCEPTIONS
to collect all occurrences popping up in the
input:
CUSS_WORDS="$HOME/.git/hooks/no-no.txt"
if git diff --cached | grep -if $CUSS_WORDS >/dev/null; then
echo "cuss words detected! abort commit"
exit 1
fi
This same solution could be used to filter out any kind of dangerous strings; you can even add your API keys, just as a second line of defense for not committing and pushing them onto a public repository.
Conclusion
In this post we saw how to
- create a simple git hook
- execute multiple hooks with from different paths
- prevent unwanted strings commited and pushed upstream
You can find the whole pre-commit
hook in this gist.