Setting Up lint-staged for Code Checks Before Commit
lint-staged runs commands only on files added to git staging area. The difference is fundamental: ESLint on a whole 500-file project takes 20–30 seconds, ESLint on only 3 changed files — less than a second. This makes pre-commit checks nearly imperceptible.
Installation
npm install --save-dev lint-staged
Usually installed alongside Husky. .husky/pre-commit should contain:
npx lint-staged
Configuration
lint-staged.config.mjs:
export default {
// TypeScript and JavaScript
'**/*.{ts,tsx,js,jsx}': [
'eslint --fix --max-warnings 0',
'prettier --write',
],
// CSS and SCSS
'**/*.{css,scss}': [
'stylelint --fix',
'prettier --write',
],
// JSON, Markdown, YAML — formatting only
'**/*.{json,md,yml,yaml}': [
'prettier --write',
],
};
Order of commands matters: linter first (may change code), then formatter (brings to uniform style).
Configuration in package.json
Alternatively, you can declare it directly in package.json:
{
"lint-staged": {
"**/*.{ts,tsx,js,jsx}": [
"eslint --fix",
"prettier --write"
],
"**/*.{css,scss}": [
"stylelint --fix",
"prettier --write"
]
}
}
TypeScript typecheck on Staged Files
Problem: tsc --noEmit doesn't accept a file list — it checks the whole project. Work around this with a wrapper script:
// scripts/typecheck-staged.mjs
import { execSync } from 'node:child_process';
try {
execSync('tsc --noEmit --incremental', { stdio: 'inherit' });
} catch {
process.exit(1);
}
// lint-staged.config.mjs
export default {
'**/*.{ts,tsx}': [
'eslint --fix',
'prettier --write',
() => 'node scripts/typecheck-staged.mjs', // function — ignores file list
],
};
() => 'command' — when a command is returned as a function, lint-staged doesn't pass it the file list as arguments.
Parallel Execution
By default, lint-staged runs commands for different glob patterns in parallel. If this causes issues (e.g., two processes writing to one file):
export default {
concurrent: false, // sequential execution
'**/*.{ts,tsx,js,jsx}': ['eslint --fix'],
};
Additional Checks
Check file sizes (accidentally don't commit large binaries):
npm install --save-dev bundlesize
export default {
'**/*.{js,css}': [
'bundlesize',
],
// Prevent committing files > 500KB
'**/*': (files) => {
const { statSync } = require('node:fs');
const large = files.filter(f => {
try { return statSync(f).size > 512 * 1024; }
catch { return false; }
});
if (large.length) {
console.error('Large files:', large);
process.exit(1);
}
},
};
Check for secrets via git-secrets or detect-secrets:
export default {
'**/*': 'detect-secrets scan',
'**/*.{ts,tsx,js,jsx}': ['eslint --fix', 'prettier --write'],
};
Debugging
# See what will be run without actually running it
npx lint-staged --debug --dry-run
Output shows: which files matched which patterns, which commands will be executed.
Timeline
Setting up lint-staged with ESLint and Prettier: 30–60 minutes. Adding typecheck and custom checks: another 1–2 hours.







