What does lint-staged run your checks against, and why use it in a Git pre-commit hook?
answer
- Proportional to the change, not the repo
- The commit comes from the index
- One glob-to-command map, staged paths only
- git diff --cached --name-only
basics
~20 slint-staged runs the commands you configure only against the files currently staged in Git's index, not the whole project. That keeps a pre-commit hook proportional to the change, so it finishes fast and only reports problems in code you touched.
solid answer
~40 sA pre-commit hook has a very small time budget — a few hundred milliseconds before people start reaching for a bypass. Linting or formatting an entire repository on every commit blows that budget and, worse, reports pre-existing problems in files the author never touched. lint-staged narrows the work: it asks Git which paths are staged, matches them against globs in its config, and runs the configured command with just those paths as arguments. If a command rewrites a file — a formatter, say — lint-staged re-stages it so the fix is part of the commit. The Git-side equivalent of its file list is `git diff --cached --name-only --diff-filter=ACMR`, where `--cached` means "compare the index against HEAD" and the filter drops deletions so you never lint a path that no longer exists.
code
console · 3 lines$ git add src/user.ts
$ git diff --cached --name-only --diff-filter=ACMR
src/user.tsgo deeper
Recall the one-line answer: it runs your configured commands only on the files you staged, so the commit hook stays fast. Be able to name a typical use such as formatting changed files.
Explain the mechanics: the file list comes from the index via a staged diff, deletions are filtered out, and modified files are re-added so fixes land in the commit. Know why the index, not the working tree, is the right source.
Show judgment about what belongs in the sub-second budget versus what needs the whole tree. Be ready to discuss partially staged files and why a check that reads from disk can validate the wrong content.
Frame it as developer-experience economics: a hook that exceeds its budget gets bypassed, so scoping the work is what preserves the hook's value at all. Then be clear that the real guarantee still has to live where it cannot be skipped.
## The problem it solves A `pre-commit` hook runs on every single commit, in the foreground, while the developer waits. Anything that takes more than a second or two trains people to bypass the hook, at which point the hook is worse than useless: it costs time and provides no guarantee. So the design goal for anything you attach to `pre-commit` is *make the work proportional to the change*. Running a linter or formatter over the whole project is not proportional. A three-line commit in one file triggers a full-repository pass, and on a large codebase that is tens of seconds. It also surfaces failures in files the author never touched, which forces them to either fix unrelated code or skip the check. ## What lint-staged actually does lint-staged is a small runner with one job: take the set of files Git has staged, filter it by glob patterns, and invoke the configured commands on that subset. Its configuration is a map from glob to command, for example "every staged `.ts` file goes to the formatter and the type-aware linter". Files that match nothing are ignored. Commands receive the matching paths as arguments, so the tool itself does no discovery. A second behaviour matters just as much: when a configured command *modifies* a file — a formatter writing a reformatted version — lint-staged re-adds that file to the index so the fix ends up in the commit being created, rather than being left as a dirty working-tree change the developer discovers later. ## The Git commands underneath Nothing here is magic; the file list comes from the index. The manual equivalent is: `git diff --cached --name-only --diff-filter=ACMR` - `--cached` (equivalently `--staged`) diffs the index against `HEAD`, i.e. exactly what the next commit will change. - `--name-only` prints paths instead of patch text. - `--diff-filter=ACMR` keeps Added, Copied, Modified and Renamed paths and excludes Deleted ones — feeding a deleted path to a linter is a guaranteed error. If you ever hand-roll this in a shell hook, remember that paths with spaces need `-z` and null-delimited reading, and that the hook's working directory is the top level of the working tree. ## Why the index, not the working tree This is the subtle part. The commit is built from the **index**, not from the files on disk. A developer can stage part of a file with `git add -p` and leave other edits unstaged. A naive check reads the file from disk, so it validates content that is not what is being committed. lint-staged addresses this by isolating the unstaged portion while the commands run, then restoring it afterwards; that is also why interrupting it mid-run is something you want to avoid. ## Fitting into a manager lint-staged does not install hooks itself. It is invoked *by* a hook that some hook manager put in place — the manager wires up the `pre-commit` entry point, lint-staged decides what runs on which files. The two are commonly discussed together, but they answer different questions: "how does every clone get this hook" versus "how does this hook stay fast". ## Practical rules - Keep the staged-file commands to formatting and cheap, file-local lint rules. Whole-project analysis — type checking across the module graph, the test suite, dependency audits — does not fit the budget and belongs in CI. - Prefer checks that either pass or auto-fix. A check that requires the developer to open a report is a bad fit for a blocking hook. - Remember the guarantee is weak: the hook can be skipped, and a fresh clone that never ran the manager's install has no hook at all. CI still has to re-run whatever actually matters.
- Why does lint-staged re-stage files that its commands modified?Because a formatter that rewrites a file on disk would otherwise leave the fix unstaged: the commit would still contain the unformatted version, and the developer would be left with a mysteriously dirty working tree. Re-adding the modified paths to the index makes the fix part of the commit being created, which is the whole point of running the formatter at commit time.
- Which checks are a bad fit for a staged-files runner?Anything whose correctness depends on the whole project rather than one file: cross-module type checking, dead-code and unused-export analysis, dependency or license audits, and the test suite. Restricting them to staged paths gives an answer that is either wrong or meaningless. Those belong in CI, where the full tree is checked on the pushed commits.
- How would you get the list of staged files in a plain shell hook, without any manager?`git diff --cached --name-only --diff-filter=ACMR` prints the paths the next commit will add or modify, excluding deletions so you never pass a missing file to a tool. Add `-z` and read null-delimited entries if paths may contain spaces or non-ASCII characters. The hook already runs from the top of the working tree, so the paths are usable as-is.
saying these in an interview costs you the question
- Says it lints the whole repository, just faster
- Confuses staged files with all modified working-tree files
- Thinks lint-staged installs the Git hook itself
- Claims it makes CI checks unnecessary
- Suggests running the full test suite through it