githook.sh docs

creating a hook

add an executable script to .githook/ named after the git hook you want:

echo '#!/bin/sh
npm test' > .githook/pre-commit
chmod +x .githook/pre-commit

the script must be executable (chmod +x) and start with a shebang (#!/bin/sh).

supported hooks: pre-commit, commit-msg, pre-push, prepare-commit-msg, post-commit, post-merge, pre-rebase, post-checkout, post-rewrite, pre-auto-gc, applypatch-msg, pre-applypatch, post-applypatch.

common hooks

pre-commit — runs before commit is created. use for linting, formatting, tests:

#!/bin/sh
npm run lint && npm test

commit-msg — validate commit message format. $1 is the temp file containing the message:

#!/bin/sh
if ! grep -qE "^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .+" "$1"; then
  echo "error: commit message must follow conventional commits format"
  echo "example: feat(auth): add login endpoint"
  exit 1
fi

pre-push — runs before push. use for full test suite or build verification:

#!/bin/sh
npm run build && npm run test:all

post-merge — runs after merge/pull. use to reinstall deps if lockfile changed:

#!/bin/sh
changed_files=$(git diff-tree -r --name-only ORIG_HEAD HEAD)
if echo "$changed_files" | grep -q "package-lock.json"; then
  echo "package-lock.json changed, running npm install..."
  npm install
fi

testing hooks

run your hook directly without making a commit:

./.githook/pre-commit

to test that hooks block commits properly, add exit 1:

#!/bin/sh
echo "this will block the commit"
exit 1

a non-zero exit code aborts the git operation. exit 0 (or no exit) allows it to proceed.

skipping hooks

for a single command, use git's built-in --no-verify flag:

git commit --no-verify -m "wip"
git push --no-verify

to disable all hooks for multiple commands:

GITHOOK=0 git commit -m "skip"
GITHOOK=0 git push

or export it for your session:

export GITHOOK=0
git commit -m "first"
git commit -m "second"
unset GITHOOK

ci/cd and docker

hooks are for local development—disable them in CI/CD and containers with GITHOOK=0:

# github actions
env:
  GITHOOK: 0

# gitlab ci
variables:
  GITHOOK: "0"

# docker
ENV GITHOOK=0

package.json integration

auto-install hooks when developers run npm install:

npm / pnpm / bun:

npm pkg set scripts.prepare="./.githook.sh install"

yarn:

npm pkg set scripts.postinstall="./.githook.sh install"

yarn uses postinstall instead of prepare.

makefile integration

add a target for installing hooks:

.PHONY: prepare
prepare:
	./.githook.sh install

run after cloning:

make prepare

migrating from husky

hooks are automatically migrated during setup if .husky/ exists. to migrate manually:

./.githook.sh migrate husky

this copies hook scripts from .husky/ to .githook/. after verifying:

rm -rf .husky
npm uninstall husky

using other languages

hooks can be written in any language. use a shell wrapper:

#!/bin/sh
node .githook/pre-commit.js

or use a shebang directly (the file must still be executable):

#!/usr/bin/env python3
import subprocess
import sys
result = subprocess.run(["pytest", "-x"])
sys.exit(result.returncode)
#!/usr/bin/env ruby
system("bundle exec rubocop") || exit(1)

node version managers

gui clients and ides may not load your shell profile, causing "command not found" errors. create ~/.config/githook/init.sh to fix this globally:

mkdir -p ~/.config/githook
cat > ~/.config/githook/init.sh << 'EOF'
export NVM_DIR="$HOME/.nvm"
 -s "$NVM_DIR/nvm.sh"  && . "$NVM_DIR/nvm.sh"
EOF

this is sourced before every hook runs. for fnm use eval "$(fnm env)", for volta add $VOLTA_HOME/bin to PATH.

debugging

enable debug output:

GITHOOK_DEBUG=1 git commit -m "test"

common issues:

"command not found" — see nvm section

hook not running — check it's executable: ls -la .githook/

wrong directory — hooks run from repo root

uninstalling

remove git configuration (keeps your hook scripts):

./.githook.sh uninstall

to fully remove:

rm -rf .githook .githook.sh

how it works

githook.sh sets core.hooksPath to .githook/_ which contains wrapper scripts. when git runs a hook, the wrapper sources ~/.config/githook/init.sh, checks GITHOOK=0, then runs your script from .githook/.

quick setup (curl | sh) runs install if already setup.

each developer runs ./.githook.sh install once after cloning because git config is local.