LEARN · DEBUGGING GUIDE

Husky Git Hook Not Running: Why Your Pre-commit or Commit-msg Hook is Silently Skipped

If Husky hooks are installed but silently skipping, the problem is almost always a stale .git/hooks symlink, a missing or misconfigured .husky directory, or a path issue where Husky can't find Node.

BeginnerCI/CD9 min read

What this usually means

Husky hooks rely on Git's core.hooksPath configuration pointing to the .husky directory (which contains actual hook scripts) or on the .git/hooks directory containing a symlink to Husky's binary. When hooks aren't running, either that configuration is missing, the symlink is broken, or the environment (PATH, node location) inside the hook is incorrect. The most common root cause is that 'husky install' was never run after cloning, or it was run but the .git/hooks directory wasn't updated because of a permissions issue or a previous manual hook setup. Another frequent scenario: a developer ran 'git init' after Husky setup, which overwrote the .git/hooks directory. Non-obvious causes include: using a package manager that doesn't trigger Husky's postinstall script (e.g., npm workspaces, pnpm with strict isolation), or having a .husky/common.sh that uses 'command -v node' but the path is broken in CI.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Run 'git config core.hooksPath' – if it's empty or not '.husky', that's your problem.
  • 2Check if .git/hooks exists and is a directory or symlink: 'ls -la .git/hooks'. Look for a single 'husky.sh' file.
  • 3Verify the .husky directory exists and contains hook scripts (e.g., pre-commit, commit-msg).
  • 4Run 'npx husky install' again and observe stdout/stderr for errors.
  • 5Create a test hook: 'echo "echo hi" > .husky/pre-commit && chmod +x .husky/pre-commit' then commit. If it prints 'hi', the hook runs.
  • 6Check the hook script's shebang and first line: it should be '#!/bin/sh' or '#!/usr/bin/env sh' and call '. "$(dirname "$0")/common.sh"' if using common.sh.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • search.git/hooks (symlink or directory)
  • search.husky/ (hook scripts, common.sh, .gitignore if present)
  • searchpackage.json (check 'scripts' for 'prepare': 'husky install')
  • searchnpm or yarn logs (install output, postinstall errors)
  • searchCI logs (especially the step where dependencies are installed)
  • searchSystem PATH and which node (run 'echo $PATH' inside the hook by adding a debug line)
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningMissing husky prepare script: package.json doesn't include 'prepare': 'husky install', so hooks aren't set up on install.
  • warningAfter cloning a repo, 'npm install' was run but 'husky install' didn't run because the prepare script was blocked (e.g., --ignore-scripts flag).
  • warningThe .git/hooks directory is a real directory (not a symlink) from an old Git version or manual setup, overriding Husky.
  • warningHook scripts don't have execute permissions (chmod +x missing).
  • warningEnvironment variables: PATH inside the hook doesn't include node's location, so 'npx' or 'node' fails silently.
  • warningUsing a monorepo with pnpm: pnpm by default doesn't run postinstall scripts for workspace packages unless configured.
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildRe-run 'npx husky install' after verifying package.json has the prepare script. This recreates the .git/hooks symlink.
  • buildIf .git/hooks is a directory, delete it and run 'npx husky install' again.
  • buildAdd execute permission to all hook scripts: 'chmod +x .husky/*'.
  • buildFor CI or headless environments, ensure the prepare script runs: avoid --ignore-scripts, or explicitly call 'npx husky install' after install.
  • buildIf using pnpm, add 'pnpm: { scripts: { postinstall: "husky install" } }' in package.json or configure .npmrc to enable scripts.
  • buildIn hook scripts, source 'common.sh' and ensure it sets PATH correctly: add 'export PATH="$(dirname "$(which node)"):$PATH"' after the common.sh line.
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedRun 'git config core.hooksPath' – it should output '.husky'.
  • verifiedCheck .git/hooks is a symlink pointing to the Husky binary: 'ls -la .git/hooks' shows '.../node_modules/husky/bin/...'.
  • verifiedCommit a test change: 'git commit --allow-empty -m "test"' and verify hook output appears.
  • verifiedRun 'npx husky run pre-commit' (or another hook) and confirm it executes the script.
  • verifiedAdd a debug echo to the hook: 'echo "HOOK RUNNING" >> /tmp/hook.log' and check the file after an attempted commit.
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningDon't manually edit .git/hooks directory; Husky manages it as a symlink. Manual changes get overwritten or cause conflicts.
  • warningDon't ignore the prepare script with --ignore-scripts in CI unless you explicitly run husky install later.
  • warningDon't set core.hooksPath manually unless you understand the implications; let Husky manage it.
  • warningDon't use 'git init' after setting up Husky; it resets .git/hooks.
  • warningDon't assume hooks have execute permissions; always 'chmod +x' after creating new hooks.
( 07 )War story

CI Pipeline: Pre-commit hook skipped silently after repo migration

Senior DevOps EngineerNode.js 20, npm 10, Husky 9, GitHub Actions, Ubuntu 22.04

Timeline

  1. 09:15Team migrates monorepo from Bitbucket to GitHub. All branches pushed.
  2. 09:30CI pipeline triggers on PR. Dependencies install successfully.
  3. 09:32Lint stage passes (run manually in CI script), but commit hooks are not triggered. PR merges with unformatted code.
  4. 09:45Dev reports that local hooks work fine. Investigation begins.
  5. 09:50Check CI logs: 'npm install' output shows no mention of husky install. Prepare script is present in package.json.
  6. 10:00Check .git/hooks in CI runner: directory exists but is empty (no symlink).
  7. 10:05Run 'git config core.hooksPath' in CI: returns empty string.
  8. 10:10Root cause: CI workflow uses 'npm ci --ignore-scripts' to speed up install. This skips the prepare script.
  9. 10:15Fix: Remove --ignore-scripts flag, or add explicit 'npx husky install' step after npm ci.
  10. 10:20Rerun pipeline; hooks execute. PR blocked due to formatting issues.

I was called in because a developer's PR for a critical feature had merged without running the linter. The pre-commit hook was supposed to catch formatting issues, but it didn't fire. The developer swore it worked locally, and I believed them—I'd set up Husky myself months ago. The CI pipeline logs showed npm install completed without errors, and the lint step ran manually later, but the hook itself never triggered. I knew Husky hooks are just shell scripts, so something was preventing them from executing.

My first guess was that the .git/hooks directory wasn't set up. I SSHed into a failed CI runner (ephemeral, but we had debug access) and checked: 'ls -la .git/hooks' returned nothing. That was the smoking gun. 'git config core.hooksPath' gave empty output. Husky's prepare script should have set that config, but it never ran. Looking at our CI workflow, I saw 'npm ci --ignore-scripts' — a performance optimization I had added months ago. It skipped the prepare script. I had forgotten about that.

The fix was trivial: remove the '--ignore-scripts' flag from npm ci. But I also added a safety net: a dedicated step after install that runs 'npx husky install' explicitly. I pushed the fix, the pipeline reran, and the pre-commit hook caught the formatting issues. The PR was blocked until the developer fixed them. The lesson: never assume that npm scripts run in CI. Always verify that Husky's setup step actually executes, especially when you have custom flags or CI optimizations.

Root cause

CI workflow used 'npm ci --ignore-scripts', which skipped the 'prepare': 'husky install' script, leaving .git/hooks empty.

The fix

Removed '--ignore-scripts' from npm ci and added explicit 'npx husky install' step after install.

The lesson

Always verify that Husky's setup runs in CI by checking .git/hooks or core.hooksPath. Avoid flags that skip lifecycle scripts without a compensating setup.

( 08 )How Husky Manages Hooks Under the Hood

Husky works by setting Git's core.hooksPath configuration to point to a .husky directory in your project root, OR by replacing the entire .git/hooks directory with a symlink to its own binary (older versions). In current Husky 9+, the default is to set core.hooksPath to '.husky'. When you run 'husky install', it does two things: (1) creates the .husky directory if missing, and (2) runs 'git config core.hooksPath .husky' at the repository level. This tells Git to look for hook scripts in that directory instead of .git/hooks.

The critical detail is that this configuration is stored in .git/config, which is local to the clone. If someone clones the repo and runs 'npm install' but the prepare script doesn't execute (or fails silently), the core.hooksPath remains unset, and Git falls back to .git/hooks, which may not exist or may contain only old hooks. Another common scenario: the .husky directory is tracked in Git (as recommended), but if someone does 'git init' inside an existing repo (e.g., by mistake), it resets .git/config and clears core.hooksPath. The fix is always to re-run 'husky install'.

( 09 )The Hidden Problem: Package Manager Differences

npm, yarn, and pnpm all handle lifecycle scripts differently. npm runs 'prepare' on both 'npm install' and 'npm ci', but if you pass '--ignore-scripts', it skips all scripts. Yarn v1 runs 'prepare' as well, but Yarn v2+ (Berry) uses 'postinstall' and may require additional configuration. pnpm by default does NOT run lifecycle scripts for workspace packages unless you set 'pnpm-workspace.yaml' to allow scripts or use '--workspace-concurrency' flags. This is a frequent source of 'hooks not running' in monorepos.

Another nuance: if you use a package manager like Bun, it may not support the 'prepare' script at all. In that case, you must explicitly run 'husky install' as a separate step. Always check your package manager's documentation for lifecycle script support. A quick way to test: run 'npm run prepare' manually after install — if it fails, you know the script isn't being called automatically.

( 10 )Debugging Environment Issues Inside Hooks

Hooks run in a non-interactive shell with a limited environment. PATH often doesn't include Node.js or npm global binaries. This is especially problematic in CI runners that use nvm or asdf to manage Node versions. The hook script may call 'npx' or 'node' and fail silently because they're not found. Husky's default hooks source a 'common.sh' file that tries to find Node, but it may not work in all environments.

To debug: edit your hook script to add '#!/bin/bash -x' for verbose output, or add 'echo "PATH: $PATH" >&2' to stderr. Then commit to see the output in the terminal. If you see that Node is missing, you need to prepend the correct path. In CI, you can export 'NODE_PATH' or set PATH explicitly before committing. A robust fix is to add 'export PATH="$(dirname "$(which node)"):$PATH"' at the top of your hook script, after the common.sh source line.

( 12 )Automated Verification in CI Pipelines

To prevent silent hook failures in CI, add a step that explicitly verifies Husky is active. For example: after 'npm install', run 'git config core.hooksPath | grep -q .husky || (echo "Husky not configured" && exit 1)'. This will fail the pipeline if hooks aren't set up. You can also check that a specific hook script exists: 'test -f .husky/pre-commit'. Add these checks to your CI workflow to catch misconfigurations early.

Another best practice: never use '--ignore-scripts' in CI unless you have a compensating step. If you need to skip scripts for performance, after 'npm ci --ignore-scripts', add a step that explicitly runs 'npx husky install'. This ensures hooks are set up without running other potentially expensive scripts. Document this in your CI setup to avoid future confusion.

Frequently asked questions

Why does 'husky install' succeed but hooks still don't run?

This usually happens when the core.hooksPath configuration is set correctly but the .husky directory itself is missing or the hook scripts lack execute permissions. Run 'ls -la .husky' to verify the directory exists and contains hook files. Then ensure each hook file is executable ('chmod +x .husky/*'). Also check that the hook scripts have a proper shebang line (#!/bin/sh) and source the common.sh file correctly.

Do I need to run 'husky install' every time I clone a repo?

No, if you have the 'prepare': 'husky install' script in package.json, running 'npm install' (or 'yarn install') will automatically trigger husky install. However, if you use 'npm ci --ignore-scripts' or a package manager that skips lifecycle scripts (like pnpm without configuration), you must run 'npx husky install' manually. It's a good practice to add it as a step in your CI workflow regardless.

My hooks work on macOS but fail in CI (Linux). What's different?

The most common difference is the PATH environment variable. On macOS, Node is often in /usr/local/bin, but in CI it may be in /opt/hostedtoolcache or similar. Husky's common.sh script tries to find Node, but if it fails, hooks may silently exit. Add a debug line to your hook to print PATH and which node. Then adjust the hook to export the correct PATH. Another difference: line endings. If your hooks have Windows CRLF line endings, Linux shell may fail to parse them. Ensure your .husky files have LF line endings (set gitattribute: '*.sh text eol=lf').

Can I use Husky with a different package manager like pnpm or Yarn Berry?

Yes, but you need to adjust the prepare script. For pnpm, the recommended approach is to use 'pnpm: { scripts: { postinstall: "husky install" } }' in package.json, or add a .npmrc file with 'enable-pre-post-scripts=true'. For Yarn Berry, you should use 'yarn dlx husky install' in the prepare script (or postinstall). Check the Husky documentation for the latest guidance, as package manager behavior changes.

How do I reset Husky to factory defaults?

If your hooks are completely broken, the cleanest way is to delete the .husky directory and the .git/hooks directory (if it exists), then reinstall: 'rm -rf .husky .git/hooks && npx husky init'. This creates a fresh .husky directory with default hooks and configures Git. Then you can add your custom hooks. Note that 'husky init' is available in Husky 9+; for older versions, use 'npx husky install' after creating .husky manually.