empty bar

Hooked on git hooks

Intro

Git hooks are awesome. I use them all day every day. However, they are criminally underused. I, myself, had been using git for years before I discovered this feature. Therefore, I’ve decided to spread awareness.

Git hooks, as the name suggests, are a way to run executables (usually shell scripts) upon certain git actions like commit or push. They are stored as executable files in .git/hooks/ directory with names indicating when to run them, for example .git/hooks/post-commit will be run after a commit is made.

Here are a couple of examples of how I use them in my daily workflow.


Always, always, always unit test before push

Starting simple — .git/hooks/pre-push. This is run when git push is invoked and if it returns a non-zero value, then the whole push is aborted. I like to put here things that CI would do to check my code, for example linting and unit tests. This way:

  1. I don’t need to remember to run the checks myself.
  2. If push succeeds then the CI is green.

No need to check GitHub or anything, especially since my laptop is usually way faster than GitHub Action runners.

#!/bin/sh
# .git/hooks/pre-push
make tests

XD-123 | put ticket number in commit messages

This is a big one for me. By convention, teams I’ve been on put JIRA ticket numbers into commit messages so it’s easier to track which change solves what. So, if I’m working on ticket XD-123, I should word my commit messages like this XD-123 | actual commit message. It’s not a big deal to write the ticket number manually, but it’s tedious and it’s easy to make a typo. This git hook solves it. I just git commit -m "actual commit message" and XD-123 | gets prepended automatically based on branch name.

#!/bin/sh
# .git/hooks/commit-msg
set -eu
git branch --show-current | sed 's/^\([^-]*-[^-]*\).*/\1 | /' | sed -i "1s/^/$(cat -)/" "$1"

This cursed pipe chain edits a file with commit message (the one under $1) in-place. All I need is a properly named branch like XD-123-feature-b.


Check for committed credentials

Committing secrets to repo is a serious blunder. If you’re extra paranoid, you can automatically fail commits that look like they contain credentials using tools like trufflehog.

#!/bin/sh
# .git/hooks/pre-commit
TRUFFLEHOG_PRE_COMMIT=1 trufflehog git file://.

Be warned that adding secrets to staging area also copies them to your .git/ directory. With this hook you won’t make a commit containing them but they’ll still be there as an orphaned blob object.


Messy pre-commit

Many projects include a pre-commit script named after git’s pre-commit hook. Some of them return a non-zero code to abort commits if they violate project conventions. Those are the simple kind — you can safely put them in your .git/hooks/ directory.

Others perform modifications on files like reordering imports. Automatically format code before a commit — simple idea but gets really tricky to implement with git hooks depending on your project’s conventions.

The issue is, running the script will NOT change files that are already staged — a situation that looks like this:

Changes to be committed:
  (use "git restore --staged [file]..." to unstage)
	modified:   staged-and-modified

Changes not staged for commit:
  (use "git add [file]..." to update what will be committed)
  (use "git restore [file]..." to discard changes in working directory)
	modified:   staged-and-modified

You’ll have to re-add them to staging area using something like git add --update which will add ALL tracked, modified files. Good if that’s what you want, bad if you want to commit only a subset of changes in this particular commit.

So with such hook, if you don’t want to commit everything you can pass --no-verify flag to git commit to disable git hooks on this particular invocation BUT then commit-msg hook also won’t run and you’ll have to add XD-123 | manually.

At some point, it might be simpler to just run the pre-commit script yourself.

#!/bin/sh
# .git/hooks/pre-commit
set -eu
make lint-fix
git add --update  # WARN: will mess up your commit if not careful

Agentic / vibe coding

Git doesn’t care who or what invoked git commit. Hooks will be run the same way whether triggered from command-line, lazygit, IntelliJ or a harness like opencode, claudecode, etc.

This is a good way to programmatically force LLMs to follow your git conventions, much better than hoping that AGENTS.md will be remembered at all times. Of course, LLMs know how to use --no-verify too, so if they really “want” to slopify your git history, they will.

For auto-formatting in particular, I recommend using the harness itself, opencode has this capability, I’m sure others have too. It’s less confusing for LLMs to format files immediately after edits than wait until a commit.

Speaking of AI, subscribe to my RSS feed / socials to learn about git worktrees — another awesome git feature that’s really useful for coding with LLMs.


AGENTS.md in every worktree

If you already know git worktrees, then this is a really useful one. I have my own, opinionated AGENTS.md that I don’t commit to repo as it describes my local setup and workflow.

This script creates a symlink to the main AGENTS.md file after every git worktree add.

#!/bin/sh
# .git/hooks/post-checkout
set -eu
GIT_ROOT=$(git rev-parse --show-toplevel)
GIT_COMMON=$(git rev-parse --git-common-dir)
AGENTS_SOURCE="$(dirname "$GIT_COMMON")/AGENTS.md"

[ -e "$GIT_ROOT/AGENTS.md" ] || ln -s "$AGENTS_SOURCE" "$GIT_ROOT/AGENTS.md"

Further reading

This has been only a brief introduction to git hooks. There’re many, many more hooks you can use. Listing them all here would be pointless since you already have all the descriptions on your computer — just run man githooks.


Published: 2026-06-27