nano-staged: a dependency-free runner for commands on changed files
Tiny tool to run commands for modified, staged, and committed files in a GIT repository.
At a glance
- What is it?
- nano-staged runs your formatters and linters against the files a git operation actually touched, using glob patterns from package.json or a dedicated config file. It ships no dependencies, but it only covers a narrow slice of what a fuller task runner does.
- Who is it for?
- nano-staged is a good fit for a JavaScript or TypeScript repository that wants prettier, eslint or stylelint to run over staged files in a pre-commit hook without pulling in a dependency tree. It is the wrong fit if you need a full task runner with watch mode, task dependencies and a plugin ecosystem, since the documented surface is glob patterns, a small API object and a handful of flags.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 179 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The unit of work is the file list a git operation produced
The README describes nano-staged as a tiny tool to run commands for modified, staged and committed files in a git repository, and states that it helps speed up the running of tests, linters and scripts. The problem it addresses is narrow and real: running a formatter across an entire repository on every commit is slow, and running nothing means formatting drifts into the codebase.
The approach is to let git decide which files matter. The staged file list, the unstaged file list or the files changed between two refs are the input, and your configured commands receive that list. The tool supports multiple file states, with the feature list naming staged, unstaged and last-commit changes among them, and the api object exposes the run type as `staged`, `unstaged` or `diff`.
This is worth understanding as a filter, not a scheduler. nano-staged decides which files to hand to a command, and the command itself is an ordinary executable such as prettier or eslint that you already have installed.
The size and speed claims come with a dated benchmark
The README advertises a small footprint and no dependencies, and includes a benchmark comparison that is worth reading with its context rather than its conclusion. The running time comparison for ten files is:
$ node bench/running-time/index.js
- lint-staged 1.145 ms
+ nano-staged 0.652 msand the size comparison is:
$ node bench/size/index.js
Data from packagephobia.com
- lint-staged 147 kB
+ nano-staged 52 kBThe README states that these results were generated on a MBP Late 2013 with a 2.3 GHz Intel Core i7 by running `npm run bench` in the library folder, and links to the benchmark script itself. Two things follow. The measurement machine is a decade-old laptop, so the absolute numbers say little about a modern machine, and the size figure comes from packagephobia rather than from a local measurement. The dependency-free claim is the sturdier part of the argument, since a package with no runtime dependencies cannot inherit a transitive tree. The repository confirms the shape of the project: a lib directory, a test directory, a bench directory and a pnpm lockfile, with the published files limited to lib, the changelog, the licence and the README.
Ten config filenames, and only the first one in the directory counts
Configuration can live in a `nano-staged` section of package.json, in a `.nano-staged.json`, `nano-staged.json` or `.nanostagedrc` file, or in a JavaScript config file. The JavaScript variants are the flexible ones: `.nano-staged.cjs` and `nano-staged.cjs` export CommonJS modules, while `.nano-staged.mjs`, `nano-staged.mjs` and `.nano-staged.js` use ES module syntax, with the `.js` form switching on the `type` field of package.json.
The part that catches people is the priority rule. If several configuration files exist in the same directory, only one is used, and the README lists the order explicitly:
1. `.nano-staged.js`
2. `nano-staged.js`
3. `.nano-staged.cjs`
4. `nano-staged.cjs`
5. `.nano-staged.mjs`
6. `nano-staged.mjs`
7. `.nano-staged.json`
8. `nano-staged.json`
9. `.nanostagedrc`
10. `package.json`So a stray `nano-staged.json` will silently lose to a `.nano-staged.js` in the same folder, and the fallback is package.json itself, which is last. There is no merging of config files and no warning described in the documentation about a shadowed file.
A config function receives filenames and the run type
The glob object form is the simple case, where each pattern maps to a command or a list of commands:
{
"*": "your-cmd",
"*.ext": ["your-cmd", "your-cmd"]
}JavaScript config files may instead export a function or an object whose values are functions. The README states that the api object exposes `api.filenames`, the working filenames, and `api.type`, the run type, which is `staged`, `unstaged` or `diff`. The example filters by extension inside the function:
export default (api) => {
const jsFiles = api.filenames.filter((file) => path.extname(file) === '.js')
return [`eslint --fix ${jsFiles.join(' ')}`, `prettier --write ${jsFiles.join(' ')}`]
}An object whose values are functions works too:
export default {
'*.js': (api) => `eslint --fix ${api.filenames.join(' ')}`,
}Note what this puts in your hands. The filename list is interpolated into a shell command string, so a path containing a space or a shell metacharacter is a quoting problem the tool does not appear to solve for you, and there is no documented escaping helper.
Command line flags decide which file set runs
The CLI is where the file states are chosen. By default the tool runs commands from the config for staged files only. The `--unstaged` or `-u` flag switches to git unstaged files, and `--diff` takes an optional pair of refs so you can run commands on files changed between the working tree and the index, between the index and a tree, between two trees, or between two indexes given as commit hashes. `--config` or `-c` points at a configuration file, with the path either absolute or relative to the directory the process runs from.
Three more flags shape the outcome. `--allow-empty` permits creating an empty commit, which matters when a hook would otherwise block a commit with nothing staged. `--quiet` shows only that work was done and that errors occurred. `--bail` stops running commands after the first failure, so the remaining commands in the list do not run.
That last flag is the one to think about. Without it, a failing formatter does not prevent later commands from running against the same files, which can leave a file half-formatted when one tool succeeds and another fails.
Wiring the pre-commit hook with simple-git-hooks or husky
The documented way to use the tool is a pre-commit hook, and the README gives two complete recipes. Install it as a dev dependency:
npm install --save-dev nano-stagedor with yarn:
yarn add nano-staged -DThen add the commands to the `nano-staged` section of package.json, for example:
"nano-staged": {
"*.{js,ts}": "prettier --write",
"*.css": ["stylelint", "eslint --fix"]
},and run it directly to check the setup:
./node_modules/.bin/nano-stagedFor the hook itself, the simple-git-hooks route adds a `simple-git-hooks` section:
"simple-git-hooks": {
"pre-commit": "./node_modules/.bin/nano-staged"
}followed by `npx simple-git-hooks` to write the hook into the repository, and optionally a postinstall script so the hook survives a fresh clone:
"scripts": {
"postinstall": "npx simple-git-hooks"
}The husky route is the same idea with different commands: `npx husky install`, then `npx husky add .husky/pre-commit "./node_modules/.bin/nano-staged"`, and a postinstall script running `npx husky install`.
Node 22 or 24, ES modules only, and no tagged releases
The package manifest constrains where the tool can run. The engines field is:
"engines": {
"node": "^22 || >= 24"
},The package is `"type": "module"` with a single bin at `./lib/bin.js`, and the published entry is `lib/index.js` with types from `lib/index.d.ts`. There is no CommonJS build of the published package, only CommonJS support for your own config files.
The development setup behind it is worth noting, because it is small and unusually strict for a project of this size. The test script runs coverage through c8 over a uvu test suite with esmock for module mocking, and the c8 configuration sets a 90 percent line threshold with `check-coverage` enabled, so the coverage gate is enforced by the test command itself.
On maintenance, the repository is not archived and the last push was on 2026-04-27, but there are no GitHub releases, so the only version marker is the manifest, which reads 1.0.2. There is a CHANGELOG.md in the repository and in the published files, which is where a change log for that version would live. The licence is MIT.
lint-staged is the comparison the project itself makes
The README credits lint-staged as the inspiration for the project, stating that some code was borrowed from it, and the benchmark section compares the two directly on size and running time. That is the only comparative evidence available, so it is the comparison to weigh.
The difference the documentation supports is one of scope. lint-staged is the larger package by the README's own figures, 147 kB against 52 kB, and nano-staged has no dependencies at all, which is the property that produces most of that difference. On the feature side, nano-staged exposes multiple file states and the api object with its run type, and the alternative here is a tool whose configuration surface is the glob map.
The two packages are not mutually exclusive in practice, since both are invoked as a pre-commit hook and both hand a file list to commands you already have installed. The decision is really about how much machinery you want between git and your linters. If your repository already has a task runner that watches files and orchestrates dependencies, the extra layer buys little. If you want a small hook that does one job, the size difference is the argument for nano-staged.
Editorial conclusion
nano-staged is a good fit for a JavaScript or TypeScript repository that wants prettier, eslint or stylelint to run over staged files in a pre-commit hook without pulling in a dependency tree. It is the wrong fit if you need a full task runner with watch mode, task dependencies and a plugin ecosystem, since the documented surface is glob patterns, a small API object and a handful of flags. Verify first that your Node version satisfies the declared engines field of ^22 or 24, check which config file wins if you have more than one in the same directory, and pin an exact version because the repository has no GitHub releases to fall back on.
Frequently asked questions
How do I install and run nano-staged?
Install it as a dev dependency with npm install --save-dev nano-staged or yarn add nano-staged -D, add a nano-staged section to your package.json with the commands per glob pattern, and run ./node_modules/.bin/nano-staged. By default it runs the configured commands for staged files only.
Which config file does nano-staged use if I have more than one?
Only one is used, chosen by a fixed priority order: .nano-staged.js, nano-staged.js, .nano-staged.cjs, nano-staged.cjs, .nano-staged.mjs, nano-staged.mjs, .nano-staged.json, nano-staged.json, .nanostagedrc, and finally the package.json section.
What can a nano-staged config function do?
A JavaScript config file may export a function that receives an api object exposing api.filenames, the working filenames, and api.type, the run type, which is staged, unstaged or diff. The function returns the command strings to run for those files.
What Node.js version does nano-staged require?
The package manifest declares the engines field as node ^22 || >= 24, and the package is published as an ES module with the bin at ./lib/bin.js and types from lib/index.d.ts.
How do I make nano-staged run automatically before each commit?
The README documents two setups: simple-git-hooks, with a pre-commit entry pointing at ./node_modules/.bin/nano-staged and npx simple-git-hooks to install it, or husky, with npx husky install and npx husky add .husky/pre-commit. Both can be wired to a postinstall script so the hook is set up after a fresh clone.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/usmanyunusov-nano-staged)Was this useful?