Fix Git hooks that never run
My pre-commit hook (or Husky) isn't running — why does Git ignore it?
If you’re seeing this error
hint: The '.git/hooks/pre-commit' hook was ignored because it's not set as executable.husky - pre-commit hook exited with code 127 (error)You’re in the right place — the fix is below.
Short answer
Nine times out of ten the hook file is not executable, is misnamed, or sits in a folder Git isn't reading. Run git config core.hooksPath to see where Git actually looks, check the file is named exactly pre-commit with no extension, then chmod +x it. A stray global core.hooksPath silently overrides every repository on the machine — including Husky's.
git config core.hooksPath # where is Git looking?
ls -l .husky/pre-commit # is it there, and executable?
chmod +x .husky/pre-commit
The three checks that explain almost every silent hook
Does this match your situation?
- git commit succeeds instantly and no lint, test or format step runs.
- 'hook was ignored because it's not set as executable'
- Hooks work for you but not for a teammate who just cloned the repository.
- Husky worked yesterday and stopped after you reinstalled node_modules.
Step-by-step fix
Find out where Git is actually looking
Git runs hooks from exactly one directory, and core.hooksPath overrides the default .git/hooks. A value set globally — often left behind by an old tool — applies to every repository on the machine, and is the single most common reason Husky appears installed but does nothing.
step 1git config core.hooksPath git config --global core.hooksPath git config --show-origin --get core.hooksPathEmpty output means Git is using .git/hooks. If the global value points somewhere unexpected: git config --global --unset core.hooksPath
Check the filename — Git matches it exactly
Git looks for a file called pre-commit. Not pre-commit.sh, not precommit, and not pre-commit.sample. The .sample files Git ships with are deliberately inert; renaming one is how you switch it on.
step 2ls -l .git/hooks/ mv .git/hooks/pre-commit.sample .git/hooks/pre-commitMake it executable — and record that in the repository
Git skips a hook without the executable bit and, since 2.36, prints a hint saying so. chmod fixes it on your machine; git update-index --chmod=+x stores the bit in the repository so every clone and every new branch inherits it. That second command is what stops the fix regressing for your team.
step 3chmod +x .husky/pre-commit git update-index --chmod=+x .husky/pre-commit git commit -m "chore: mark pre-commit hook executable"Show the Windows / PowerShell version
Windows / PowerShell# chmod does not exist on Windows — set the bit in Git's index directly git update-index --chmod=+x .husky/pre-commitCheck the shebang and the line endings
Exit code 127 means 'command not found' — usually a missing shebang, CRLF line endings on a shell script, or a tool that only exists inside node_modules being invoked without npx. A hook that dies this way looks identical to one that never ran.
step 4head -1 .husky/pre-commit # expect #!/usr/bin/env sh file .husky/pre-commit # 'CRLF line terminators' is a bug here sed -i 's/\r$//' .husky/pre-commit # strip CRLFAdd .husky/** text eol=lf to .gitattributes so Windows checkouts stop breaking the shebang line.
Reinstall the hook manager
Husky writes its hooks during npm install via the prepare script. If someone cloned the repository and installed with scripts disabled, or .husky was gitignored, nothing was ever written.
step 5npm pkg set scripts.prepare="husky" npm run prepare # pre-commit (Python): pre-commit installConfirm it fires — and know the escape hatch
Commit something trivial and watch for output. Once it works, remember --no-verify exists for the genuine emergency rather than as a habit: it skips pre-commit and commit-msg entirely.
step 6git commit --allow-empty -m "test: hook check" git commit --no-verify -m "emergency: skip hooks"
Why this works
Hooks are ordinary executables Git runs by path at fixed moments. Git makes no attempt to interpret them, report on them, or tell you they exist — if the file is missing, misnamed, non-executable or in a directory Git is not reading, it simply carries on. That silence is deliberate, because hooks are never transmitted by clone or fetch: .git/hooks sits outside the object database, so a repository can never ship code that runs automatically on someone else's machine. Everything Husky, lint-staged and pre-commit do is a workaround for that one security decision — they keep hooks in a tracked folder and point core.hooksPath at it, which is exactly why they break the moment that setting is overridden somewhere else.
If that didn’t work
- Run GIT_TRACE=1 git commit -m test and read which hook path Git tries to execute.
- Check for a core.hooksPath in the repository's own .git/config — it beats the global value.
- In a worktree, confirm core.hooksPath was not set with --worktree, where the main checkout will not see it.
- Confirm your Git is 2.9 or newer; core.hooksPath does not exist before that.
How to stop it happening again
- Keep hooks in a tracked directory and set core.hooksPath from a setup script, so new clones get them.
- Set the executable bit with git update-index --chmod=+x so it survives cloning.
- Add .husky/** text eol=lf to .gitattributes so Windows never breaks the shebang.
- Enforce the same checks in CI — a hook is a convenience, not a guarantee, because anyone can pass --no-verify.
Commands used in this guide
git hooksRun your own scripts automatically at points in the Git lifecycle.
git configRead and write Git settings for one repo, your user, or the whole machine.
git update-indexManipulate the index directly - including telling Git to ignore changes to a tracked file.
git commitRecord everything currently staged as a permanent snapshot.
.gitattributesPer-path rules for line endings, diffing, merging and export.
Still stuck?
Search the full command reference and every other rescue guide — there are 76 of them, covering everything from detached HEAD to force-push disasters.
Browse all guides →Frequently asked questions
Why did Git say my hook was ignored because it's not set as executable?
Git 2.36 added that hint for exactly this case: the file exists but lacks the executable bit, so Git refuses to run it. Fix it with chmod +x, then git update-index --chmod=+x so the bit is stored in the repository and every future clone inherits it.
Are Git hooks copied when someone clones my repository?
No. .git/hooks lives outside the object database and is never transferred, because a repository that shipped auto-executing code would be a serious security hole. That is precisely why Husky and pre-commit exist — they keep hooks in a tracked folder and repoint core.hooksPath at it.
How do I skip a Git hook once?
git commit --no-verify skips pre-commit and commit-msg; git push --no-verify skips pre-push. Reserve it for genuine emergencies, and make sure CI runs the same checks so a skipped hook cannot land broken code.
Why does Husky stop working after npm ci?
Husky installs its hooks from the prepare lifecycle script, which is skipped when npm runs with --ignore-scripts or in some CI images. Run npm run prepare after installing, and make sure .husky is committed rather than gitignored.
Where does Git look for hooks?
.git/hooks by default, or whatever core.hooksPath points at. Check with git config --show-origin --get core.hooksPath — a value inherited from your global ~/.gitconfig silently overrides every repository on the machine, which is the classic cause of hooks that 'used to work'.
Related rescue guides
git status says every file changed but I didn't touch them — how do I fix line endings?
Windows writes CRLF and Unix writes LF, and without a policy Git records whichever your editor produced. Commit a .gitattributes with `* text=auto`, then renormalise the repository once — this fixes it for everyone, unlike the per-machine core.autocrlf setting.
Read the fix →Always fixablegit commit fails with 'error: gpg failed to sign the data' — how do I fix commit signing?
Test the signing outside Git first: echo test | gpg --clearsign. 'Inappropriate ioctl for device' means the passphrase prompt has nowhere to draw — add export GPG_TTY=$(tty) to your shell profile. 'No secret key' means user.signingkey does not match a key you hold. If you just want to commit right now, git commit --no-gpg-sign gets you through.
Read the fix →Always fixableI cloned a repo and the submodule folders are empty — how do I fix it?
Cloning records submodules but doesn't populate them. Run git submodule update --init --recursive, or clone with --recurse-submodules next time so it happens automatically.
Read the fix →Always fixablegit commit opened a text editor I can't get out of — how do I save and exit?
You are in Vim. Press Esc, type :wq and press Enter to save and finish the commit, or :q! to abandon it. For a log or diff that has taken over the terminal you are in the pager instead — press q. Set git config --global core.editor "code --wait" (or nano) so it never happens again.
Read the fix →