Your team spent hours writing a pre-commit hook that runs eslint and a secret scanner before every commit. The hook is committed to the repo in a hooks/ directory and documented in the README. But new developers who clone the repo report that no hooks run — they can commit files that break linting without any warning. The issue is fundamental to how Git works: .git/ is never cloned or version-controlled. Any hooks in .git/hooks/ exist only on the machine that put them there. Sharing hooks requires a deliberate installation step. This guide covers every method, from a one-line git config change to a full husky or lefthook setup.
Common causes
Ordered by hit rate, highest first.
1. Hooks are in a tracked directory but .git/hooks/ is never populated
The repo has a hooks/ or .githooks/ directory with pre-commit and pre-push scripts committed. Developers assume cloning the repo means the hooks are active. They are not — Git only looks in .git/hooks/, not in a tracked directory.
How to spot it: ls .git/hooks/ shows only .sample files. The tracked hooks are in hooks/ but are not symlinked or copied.
2. core.hooksPath is not set in the repo or global config
Even if the team uses git config core.hooksPath hooks, this setting is local to each developer’s clone and is not propagated by git clone. Each new clone starts with the default core.hooksPath value (.git/hooks).
How to spot it: git config core.hooksPath returns empty on a fresh clone.
3. Hook file is not executable
The hook file was committed without the executable bit. git clone preserves file permissions, so the hook lands in .git/hooks/pre-commit as a non-executable file. Git silently skips non-executable hooks without printing an error.
How to spot it: ls -la .git/hooks/pre-commit — if permissions show -rw-r--r-- (no x), the hook is not runnable.
4. Hook manager (husky, lefthook) was not installed after clone
The repo uses husky v8 / v9, which stores hooks in .husky/. The hooks only become active after npm install (or npm run prepare) because husky registers .git/config entries during npm install. Developers who did not run install have no active hooks.
How to spot it: .husky/ directory exists and package.json has a "prepare": "husky install" script. The developer only ran git clone without subsequent npm install.
5. Hooks skipped by --no-verify in a CI script or alias
A CI pipeline or a developer’s shell alias appends --no-verify to git commit, bypassing all hooks. Hooks run fine locally for developers who do not use the alias but are completely absent in CI.
How to spot it: grep -r "no-verify\|--no-verify" .github/ Makefile scripts/ — any occurrence of --no-verify is skipping hooks.
6. Windows line endings in the hook script break the shebang
The hook file was written on Windows and committed with CRLF line endings. On macOS and Linux, the kernel cannot parse #!/bin/sh\r (with a carriage return) as a shebang and silently refuses to execute the file.
How to spot it: file .git/hooks/pre-commit returns “with CRLF line terminators.” head -c 12 .git/hooks/pre-commit | xxd shows 0d 0a (CRLF) after the shebang.
Shortest path to fix
Step 1: Set core.hooksPath to the tracked hooks directory
The fastest fix that works across all developers with one commit:
# Add to .gitconfig at the repo root (overrides global config for this repo only)
git config --local core.hooksPath hooks
Or commit a bootstrap script that developers run once:
# scripts/install-hooks.sh
#!/bin/sh
git config core.hooksPath hooks
chmod +x hooks/*
echo "Hooks installed."
Step 2: Make hook files executable and fix line endings
chmod +x hooks/pre-commit hooks/pre-push
# Fix CRLF if on macOS/Linux
sed -i 's/\r$//' hooks/pre-commit hooks/pre-push
git add hooks/
git commit -m "fix: make hook scripts executable and UNIX line endings"
Step 3: Install husky correctly (if using husky)
npm install # triggers the "prepare" script which runs "husky install"
# Verify:
ls .husky/ # should contain pre-commit, pre-push etc.
cat .git/config | grep hooksPath # should show .husky
If you want to enforce this on clone without npm install, add a post-checkout hook manually (bootstraps itself):
# hooks/post-checkout
#!/bin/sh
git config core.hooksPath hooks
Step 4: Verify hooks are running
# Stage a file and attempt a commit
touch test-hook.txt && git add test-hook.txt
git commit -m "test hook activation"
The pre-commit hook output should appear in the terminal.
Step 5: Remove —no-verify from CI scripts
grep -r "no-verify" .github/ Makefile scripts/
# Edit each occurrence and remove --no-verify
# CI should run hooks, or alternatively, run the hook checks as separate CI steps
Prevention
- Commit hooks to a tracked directory (e.g.,
hooks/or.githooks/) and document the one-time setup command inREADME.mdand the project’sMakefilebootstrap target. - Use a hook manager (lefthook, husky v9, or pre-commit) that integrates with your package manager’s install lifecycle so
npm install/pip installautomatically activates hooks. - Add a CI check that runs the same linting and secret-scanning commands as the pre-commit hook, so skipping the hook with
--no-verifystill fails the build. - Commit
.gitattributeswithhooks/* text eol=lfto enforce LF line endings on hook scripts across all platforms. - Add
chmod +x hooks/*to the CI setup step so hooks are executable after CI checkout. - Document in
CONTRIBUTING.md: “After cloning, runmake setup(or equivalent) to activate Git hooks.” - Audit periodically:
git log --oneline -20 --no-walk --tags | while read sha msg; do git show --no-patch --format="%H %ae" $sha; doneto check whether recent commits bypassed hooks.
FAQ
Q: Is there a way to share hooks without requiring developers to run a setup step?
A: Not natively — Git will not execute hooks from a tracked directory automatically. The closest workaround is a post-checkout hook committed in .git/hooks/post-checkout via a repo template (git config --global init.templateDir ~/.git-templates), which is copied into new clones made after the template is configured.
Q: Can I use core.hooksPath to point to a system-wide shared hooks directory?
A: Yes. Set git config --global core.hooksPath /usr/local/share/git-hooks and place shared organizational hooks there. Project-specific hooks can be in .githooks/ with core.hooksPath = .githooks overriding the global setting per repo.
Q: Our hooks run on Mac but not in the Docker-based CI environment. Why?
A: The Docker image likely clones the repo but does not run the installation step. Add git config core.hooksPath hooks && chmod +x hooks/* to the Dockerfile or CI setup script.
Q: How do I run a pre-commit hook only for specific file types?
A: Inside the hook script, filter on file extension: git diff --cached --name-only | grep '\.ts$' — then run the linter only if the output is non-empty.