Stockio

Husky and lint-staged: Fast Pre-Commit Checks

Set up Husky and lint-staged to check only staged files, handle partial commits safely, and keep local feedback fast without treating hooks as a security boundary.

By Ravi Desai

DevOps & Security Engineer ·

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 init

Keep the generated prepare script so normal dependency installation can install hooks. Replace the content of .husky/pre-commit with this command:

npx lint-staged

Add 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 --cached

The 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.
Featured sponsor

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

Keep reading

Related articles

More in Dev Tools & AI Coding
Dev Tools & AI Coding

8 Best AI Code Review Tools

Compare eight AI-assisted code review tools for pull requests, security checks, and repository workflows, with practical cautions about false alarms, context, and human responsibility.

Includes: JSON formatterSofia MarchettiSofia Marchetti
Dev Tools & AI Coding

15 Best VS Code Extensions for Web Development

Find useful VS Code extensions for formatting, debugging, writing and remote work. This practical list explains what each add-on does, who benefits, and when built-in features are enough.

Includes: JSON formatterSofia MarchettiSofia Marchetti
Dev Tools & AI Coding

Turborepo Monorepo With pnpm Workspaces

Set up pnpm workspace boundaries and Turborepo task caching with explicit package dependencies, reliable outputs, and a structure that stays understandable as teams grow.

Includes: JSON formatterSofia MarchettiSofia Marchetti
Dev Tools & AI Coding

Chrome DevTools Performance Panel: From Trace to Fix

Record focused Chrome Performance traces, identify expensive main-thread work, and verify targeted fixes with repeatable scenarios instead of guessing from a flame chart.

Includes: JSON formatterRavi DesaiRavi Desai
Dev Tools & AI Coding

direnv and .env Files: Managing Local Environment Variables

Make local project configuration predictable with .env templates, explicit loading rules, direnv approval, and checks that catch missing values before startup.

Includes: JSON formatterRavi DesaiRavi Desai
Dev Tools & AI Coding

AI Tailwind CSS: Get Clean Styles From AI Assistants

AI assistants can produce a convincing interface quickly, but the stylesheet often reveals how little was specified. Arbitrary spacing, duplicated utilities and brittle positioning accumulate when every prompt.

Includes: JSON formatterRavi DesaiRavi Desai