The lint-staged npm package runs linters, formatters, and other commands against only the files you have staged in git, which keeps every commit checked and fast without scanning the whole repository each time. It is almost always paired with a git-hook manager like Husky so that the checks fire automatically on git commit. The security angle people miss is that lint-staged is fundamentally a command runner: it takes commands from a config file and executes them against your files. That is exactly what makes it useful and exactly why its configuration deserves the same review as any code that runs on your machine.
If you have used npm lint-staged before, you know the payoff. A pre-commit hook that lints the entire codebase is slow enough that people disable it. One that lints only staged files runs in seconds and stays enabled.
What lint-staged does
When you commit, lint-staged asks git which files are staged, matches them against glob patterns in your config, and runs the associated commands against just those files. If a command exits non-zero, the commit is aborted. Crucially, it stashes unstaged changes during the run so your linters only see what is actually being committed, then restores them afterward.
The current major line is v17, which requires a modern LTS Node runtime (Node 22 or newer in recent releases), so confirm your toolchain is current before installing.
npm install --save-dev lint-staged
A safe, minimal configuration
Configuration lives in package.json under a lint-staged key, or in a dedicated .lintstagedrc file. Keys are glob patterns; values are the commands to run on the matched staged files.
{
"lint-staged": {
"*.{js,ts,jsx,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md,yml}": ["prettier --write"]
}
}
Two behaviors matter here. First, lint-staged appends the list of matched file paths to each command, so eslint --fix becomes eslint --fix file1.ts file2.ts. Second, when a command modifies files (like --fix or --write), lint-staged automatically re-stages those changes so the fixes are included in the commit. That is the whole workflow: check, auto-fix where safe, and commit only clean code.
Wiring it to a git hook
lint-staged does not install a git hook itself; it is meant to be called by one. Husky is the common companion. After installing both, the pre-commit hook is a one-liner:
# .husky/pre-commit
npx lint-staged
Now every commit runs your staged-file checks. Developers who want to bypass in a genuine emergency can use git commit --no-verify, which is worth knowing exists but worth discouraging as a habit, since it silently skips every check.
The security considerations
Because lint-staged executes arbitrary commands from a config file, treat that config as executable code, not settings.
A few concrete points:
- Review changes to the lint-staged config in pull requests. A malicious or careless change could add a command that runs during every teammate's commit. The config is a place to look during review, not skim past.
- Do not treat a local pre-commit hook as a security control. Hooks are trivially bypassed with
--no-verifyand only run on machines that installed them. Anything that must be enforced, secret scanning, security linting, license checks, belongs in CI where it cannot be skipped. lint-staged is a developer convenience, not a gate. - Mind the dependency footprint. lint-staged and Husky are dev dependencies pulled from npm like everything else, and dev dependencies have been targeted in supply-chain attacks precisely because they run on developer machines and in CI. Pin versions, review updates, and let an SCA tool flag advisories in your dev-dependency tree so a compromised tooling package does not slip in during a routine bump.
The reassuring part is that lint-staged itself is a mature, widely used, well-maintained package. The risk is not the tool; it is treating its config as trivial and treating its hooks as enforcement.
Common configuration mistakes
The pattern I see fail most often is running type-checks through lint-staged. Because lint-staged passes specific file paths, running tsc on individual files ignores the project's type graph and produces misleading results. Type-checking wants the whole project, so run it in a separate script or in CI, not per staged file.
The second is expecting the hook to run in CI. It does not; git hooks are local. Your CI must run the same linters independently against the full diff or the whole repo. lint-staged speeds up the local loop; CI provides the actual guarantee. If you are evaluating how local checks and pipeline gates fit together, our Academy covers building that layered setup.
FAQ
What does the lint-staged npm package do?
It runs linters, formatters, or other commands against only your git-staged files rather than the whole repository, making pre-commit checks fast. It is typically triggered by a git-hook manager like Husky and aborts the commit if any command fails.
Is lint-staged a security tool?
No. It is a developer-convenience command runner. A local pre-commit hook can be bypassed with git commit --no-verify and only runs on machines that installed it, so any real security check (secret scanning, license enforcement) must run in CI where it cannot be skipped.
Why should I not run tsc through lint-staged?
Because lint-staged passes individual staged file paths to each command, and TypeScript needs the whole project graph to type-check correctly. Running tsc on isolated files gives misleading results. Run type-checking as a separate whole-project script or in CI.
Are there supply-chain risks with lint-staged?
The package itself is mature and well maintained, but it and its companions are npm dev dependencies that run on developer machines and in CI, which makes the dev-dependency tree a target. Pin versions, review updates, and scan dependencies for advisories.