
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.
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:
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 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.
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.
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 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.
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" 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