On this page
A pre-commit hook should catch an obvious mistake before it becomes someone else's review task. It should not turn every small commit into a complete release rehearsal. Husky connects Git events to project commands; lint-staged narrows suitable commands to files already selected for the commit. Together, they make a useful feedback loop when their responsibilities stay small.
Decide what belongs before a commit
Start with fast, deterministic checks: formatting, lint rules that do not require a running service, and inexpensive syntax checks. Keep browser suites, integration environments, and production builds outside the ordinary pre-commit path. A reasonable starting budget is a few seconds on a typical change, measured on the slowest supported development machine rather than the maintainer's workstation.
Before editing package configuration, use the JSON formatter to inspect a redacted package.json fragment and catch trailing commas or malformed nesting. Do not paste private registry credentials into an online tool. Copy only the scripts and configuration objects needed for the task, then validate the actual file locally as well.
Agree on the policy first: which files are formatted, whether warnings fail commits, and where the same checks run independently. Local hooks are convenience features. They are not enforcement mechanisms, because contributors can bypass them or never install them.
Install a small, explicit setup
The following example assumes an npm project, a supported Node version, and ESLint and Prettier already configured. Use the repository's existing package manager rather than introducing a second lockfile. Follow the current Husky documentation if your repository layout or installed major version differs.
Run the initialization from the package associated with the Git repository. Inspect the generated hook: initialization may create a sample test command that should be replaced rather than left alongside an expensive test suite.
npm install --save-dev husky lint-staged
npx husky initKeep the generated prepare script so normal dependency installation can install hooks. Replace the content of .husky/pre-commit with this command:
npx lint-stagedAdd this top-level field to package.json, merging it with existing configuration rather than replacing the file:
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,css,md}": "prettier --write"
}
}The array gives JavaScript and TypeScript files a defined execution order: lint fixes first, formatting second. Avoid overlapping glob entries that write to the same file concurrently. Read lint-staged's documentation before adding custom functions or special shell behavior; task strings are not a substitute for understanding command argument handling.
Understand the index before trusting automation
Git commits the index, not every current working-tree change. That distinction matters when a developer stages one hunk of a file and leaves another hunk for later. A formatter sees files, while the intended commit may contain only part of one file.
By default, lint-staged creates a backup stash and hides unstaged changes in partially staged files while tasks run. It then applies successful task modifications to the index and restores hidden changes. Treat that as useful protection, not permission to stop looking at the diff. Conflicts, failing tasks, and unusual repository states still require attention.
Use staged-diff inspection as the final checkpoint:
git status --short
git diff --cached
npx lint-staged --debug
git diff --cachedThe debug invocation runs tasks; it is not a read-only preview. Use it on a controlled change. If recovery is needed, inspect Git status and available stashes before applying or deleting anything. Blindly popping a stash can add another conflict on top of the original failure.
Do not append git add to lint-staged tasks. Modern lint-staged stages task modifications itself. Also avoid blanket staging commands in hooks, which can pull unrelated edits into a commit. The Git hooks reference explains the event model and why each hook must return a meaningful exit status.
Test the unhappy paths deliberately
Create a disposable branch and test four cases: a clean file, a fixable formatting error, an unfixable lint error, and a partially staged file. Confirm that the first two commit successfully and the lint failure blocks the commit without swallowing unrelated work. Inspect both staged and unstaged diffs afterward.
Test a filename containing spaces. Let lint-staged pass filenames to commands rather than constructing a shell string from untrusted paths. If a custom script is necessary, accept an argument array and avoid shell interpolation. A filename is data, not executable syntax.
Next, test from the editor's Git integration as well as a terminal. Environment differences explain many apparent hook failures: a GUI may inherit a different PATH or fail to load a shell's Node version manager. Document the supported setup instead of hard-coding one developer's home directory into a tracked hook.
For monorepos, verify configuration discovery with files from two packages in the same commit. A nearby lint-staged configuration can change which commands apply. Keep ownership obvious, and avoid assuming that every command starts in the package directory you expect. The broader development guides are useful background, but repository-specific instructions must remain in the repository.
Keep bypasses and shared checks honest
A local escape hatch is sometimes necessary for an emergency or a broken toolchain. Documented bypasses are safer than secret workarounds: explain when they are acceptable, which checks must run afterward, and how reviewers can see the exception. Never use a bypass to hide a known failing correctness check.
Run authoritative lint and formatting checks on shared infrastructure over the appropriate project scope, not merely the files a local hook happened to inspect. Whole-project type checking belongs there too unless the repository has a carefully measured local alternative. Passing staged filenames directly to a compiler can change project semantics rather than simply make compilation faster.
Track hook friction during normal work. If a check repeatedly takes too long, reduce its scope or move it to a later checkpoint rather than training contributors to skip everything. Reliable feedback beats an impressive list of commands nobody waits for. Keep the hook policy beside onboarding instructions and revisit it when adding packages or changing runtimes. Other developer-tool workflows should support the same principle: clear ownership, predictable commands, and recoverable failures.
Key takeaways
- Use small pre-commit checks for formatting and inexpensive lint feedback.
- Keep glob patterns non-overlapping when tasks modify files.
- Verify partial staging, failed tasks, and editor-launched commits before rollout.
- Review the staged diff after automated fixes, not just before them.
- Treat hooks as convenience; shared checks remain the authority.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor