Local Hook Configuration with Husky Jump to heading
Husky is the standard local orchestration layer within the broader Git Automation & CI/CD Hook Engineering discipline β it intercepts Git lifecycle events on every developer machine before a commit or push can reach the remote, establishing a fast-feedback validation gate that operates entirely independent of server-side pipelines.
Prerequisites Jump to heading
Before starting, confirm the following are in place:
Step 1 β Install Husky and Scaffold the Hook Directory Jump to heading
Initialize Husky with a single command. It creates .husky/ with a sample pre-commit script and registers the prepare lifecycle hook so every future npm install automatically installs hooks on fresh clones.
# Run from the repository root β requires package.json to exist
npx husky init
# Stage the generated files so teammates get the hooks on clone
git add .husky/ package.json Verify: Open package.json β it should now include "prepare": "husky" in the scripts block. Running git config core.hooksPath should return .husky.
git config core.hooksPath
# Expected output: .husky Husky v9 sets core.hooksPath automatically during husky init. This redirects Git away from the default .git/hooks/ directory, which is never committed, and toward .husky/, which is version-controlled and shared across the team.
Step 2 β Author POSIX-Compliant Hook Scripts Jump to heading
Create the three baseline hooks that cover commit-time and push-time validation. Each script must use #!/usr/bin/env sh as the shebang β this resolves sh through PATH rather than hardcoding a system path, ensuring the script runs identically on macOS, Linux, and Windows (Git Bash / WSL2).
#!/usr/bin/env sh
# .husky/pre-commit
# Runs before the commit object is created.
# Delegates staged-file analysis to lint-staged (see package.json "lint-staged" key).
npx lint-staged #!/usr/bin/env sh
# .husky/commit-msg
# Validates the commit message against your project's convention (e.g. Conventional Commits).
# $1 is the path to the temporary file containing the commit message.
npx --no -- commitlint --edit "$1" #!/usr/bin/env sh
# .husky/pre-push
# Runs before the push is transmitted to the remote.
# Keep this under 15 seconds β slow hooks reduce adoption.
npm test -- --passWithNoTests Verify: Make a test commit after authoring the hooks and confirm each hook fires:
# Force hook execution without changing files
git stash
git commit --allow-empty -m "test: verify hooks fire"
# Expect: pre-commit and commit-msg output appears; commit succeeds or fails based on your config
git stash pop SAFETY WARNING: Never hardcode machine-specific paths such as
/usr/local/bin/nodeor/opt/homebrew/bin/npxin hook scripts. These paths vary by OS and Node version manager (nvm, asdf, fnm). Always use#!/usr/bin/env shand rely onPATHfor tool resolution, or your hooks will silently break on teammatesβ machines.
Step 3 β Pin the Husky Version Jump to heading
Lock Husky to a major version in devDependencies so behavior does not drift when teammates run npm install at different times:
{
"devDependencies": {
"husky": "^9.0.0",
"lint-staged": "^15.0.0",
"@commitlint/cli": "^19.0.0",
"@commitlint/config-conventional": "^19.0.0"
}
} Verify: Run npm ls husky β the resolved version should match your pinned range.
npm ls husky
# Expected: [email protected] (exact patch will vary) Step 4 β Configure lint-staged for Staged File Delegation Jump to heading
Hook scripts should remain thin wrappers. Embed linting logic in the lint-staged configuration inside package.json rather than in the hook script itself. This keeps hooks composable and easy to audit:
{
"lint-staged": {
"*.{js,ts,jsx,tsx}": ["eslint --fix", "prettier --write"],
"*.{css,scss}": ["prettier --write"],
"*.md": ["prettier --write --prose-wrap always"]
}
} The pre-commit hook calls npx lint-staged, which reads this configuration, filters to only the files currently staged, runs the commands, and re-stages any auto-fixed changes β all without touching unstaged work. Full filtering and auto-fix routing patterns are covered in Lint-Staged & Formatting Automation.
SAFETY WARNING: Never make network calls (HTTP requests, package registry fetches, remote API calls) inside local hook scripts. Network latency is unpredictable and offline development environments will cause every commit to hang or fail for unrelated reasons. Keep hooks strictly local.
Step 5 β Disable Hooks in CI Environments Jump to heading
Husky v9 detects the CI environment variable and skips husky install automatically when it is set. Most hosted CI platforms (GitHub Actions, GitLab CI, CircleCI) set CI=true by default, so no extra configuration is needed for them.
For Docker builds or CI runners that do not set CI, explicitly pass HUSKY=0:
# In your CI runner's install step or Dockerfile
HUSKY=0 npm ci This prevents Husky from attempting to configure core.hooksPath in an environment where the .git/ directory may be shallow, absent, or irrelevant.
Verify: Inspect your CI run logs after npm ci β you should see no output from Husky.
Integration with Adjacent Tools Jump to heading
Boundary with lint-staged Jump to heading
Husky owns the hook lifecycle β it decides when scripts run by routing core.hooksPath to .husky/. Lint-staged owns the what β it filters staged files and dispatches linter/formatter commands against that precise set. Keep this boundary clean: the pre-commit hook calls npx lint-staged and nothing else. Linter configuration, glob patterns, and auto-fix commands all live in the lint-staged block of package.json.
Boundary with CI/CD pipeline triggers Jump to heading
Local Husky hooks are a developer UX gate, not a security perimeter. They run only on machines where Husky is installed and can be bypassed with --no-verify. Your CI/CD Pipeline Trigger Mapping must independently re-execute all validations (linting, tests, secret scanning) in the pipeline β local hooks and remote pipelines are complementary layers, not redundant ones. This separation is what prevents the βit passed locallyβ failure class in production deployments.
Boundary with pre-push validation rules Jump to heading
Huskyβs pre-push hook is the local counterpart to Pre-Push Validation Rules defined in your CI system. Use the local hook for fast feedback (unit tests, quick secret scans under 15 seconds). Reserve integration tests, end-to-end suites, and protected-branch enforcement for the remote pipeline, where they run with guaranteed, consistent infrastructure.
Troubleshooting Jump to heading
| Symptom | Likely cause | Fix |
|---|---|---|
hooks not running after clone | prepare script missing from package.json | Add "prepare": "husky" to scripts; run npm install to trigger it |
core.hooksPath not set | husky init was not run, or ran outside the repo root | Run npx husky init from the directory containing .git/ |
Permission denied when hook fires | Hook file missing execute bit | Run chmod +x .husky/pre-commit (and other hook files); commit the change |
env: sh: No such file or directory on Windows | Hook shebang references a path absent in Git Bash | Replace any hardcoded path shebang with #!/usr/bin/env sh |
Husky running in CI and failing | CI env variable not set; HUSKY not explicitly disabled | Add HUSKY=0 before npm ci in your CI runner configuration |
lint-staged not found | npx lint-staged fails because lint-staged is not installed | Add lint-staged to devDependencies and run npm install |
Frequently Asked Questions Jump to heading
Does Husky v9 work on Windows? Jump to heading
Yes. Hook scripts that use #!/usr/bin/env sh run correctly under Git Bash and WSL2. Avoid PowerShell syntax inside .husky/ files β Git invokes hooks through its bundled shell, not through PowerShell. If contributors use WSL2, ensure Node is installed inside the WSL2 distribution, not only on the Windows host, because hook scripts execute in the WSL2 shell environment.
How do I stop Husky from running in CI pipelines? Jump to heading
Husky v9 skips installation automatically when the CI environment variable is set to any truthy value. For runners that do not set it, prefix your install command with HUSKY=0 npm ci. This is the recommended approach for Docker builds, Nix environments, and any runner with a read-only Git worktree.
What is the difference between pre-commit and pre-push hooks? Jump to heading
pre-commit fires before the commit object is written to the object store β ideal for sub-two-second checks like formatting and fast linting that give instant feedback. pre-push fires before the push handshake with the remote server β suitable for test suites and secret scanning where a few extra seconds is acceptable in exchange for catching broader regressions. Never move slow checks into pre-commit; slow commit hooks erode adoption faster than almost any other factor.
Can I bypass a Husky hook for an emergency fix? Jump to heading
Yes: git commit --no-verify skips all local hook execution. This is intentional β local hooks are a quality UX aid, not an enforcement mechanism. Document every bypass in your contributing guide, require a follow-up ticket to address any skipped validations, and ensure your CI/CD pipeline catches what the local hook would have caught.
How do I migrate from manually managed .git/hooks/ scripts or Husky v4? Jump to heading
Start by auditing what exists with find .git/hooks -type f -executable -print, document each scriptβs exit code semantics and dependencies, then run npx husky init and recreate the logic in .husky/. The step-by-step process, including v4 configuration mapping, is covered in Migrating from Legacy Git Hooks to Husky v9.
Related Jump to heading
- Migrating from Legacy Git Hooks to Husky v9 β step-by-step guide for teams moving off
.git/hooks/scripts or an older Husky major version, including v4 configuration mapping. - Lint-Staged & Formatting Automation β how to configure staged-file filtering, auto-fix pipelines, and glob patterns to keep your
pre-commithook fast and precise. - Pre-Push Validation Rules β patterns for scoping test suites, secret scanners, and branch policy checks to the
pre-pushhook without slowing everyday development. - Preventing Broken Builds with Pre-Push Hooks β concrete recipes for connecting local pre-push hooks to build verification steps before network transmission.
- CI/CD Pipeline Trigger Mapping β how to coordinate the boundary between local Husky hooks and remote pipeline triggers so validations are neither duplicated nor missed.