22 July 2026
Automating linting in real life
Manual code reviews for style issues are a burden. Here's the three-layer pipeline I built to catch formatting and lint violations before they ever reach a teammate.
Code linting is essential for every team that owns a product repository. Manual code reviews for style issues waste valuable engineering time that should be spent on logic, architecture, and business requirements.
Automating linting well means consistent code style, faster reviews, immediate feedback, and historical enforcement. A well-configured pipeline catches formatting issues, potential bugs, and style violations before code ever reaches a teammate.
What the global lint gave us, and what it didn't
In production we reuse a global, team-wide lint configuration maintained by the DevOps team. This is valuable for aligning rules across a large engineering organisation. Everyone's ESLint runs from the same base, which means style debates don't fragment along team lines.
But it only goes so far. The global config runs ESLint on files and reports errors and warnings as annotations on the PR. It surfaces problems, but it still requires a developer to read the annotation, understand the violation, fix it manually, and push again. Every one of those steps is friction that compounds across every PR, every contributor, every sprint.
So I brought in three layers on top to make the linting process as automatic as possible.
A pipeline that catches issues before they compound
Claude Code PostToolUse hook, catch it at the point of generation
A PostToolUse hook in .claude/settings.json runs eslint --fix automatically on every .ts, .tsx, .js, .jsx, .cjs, and .mjs file immediately after Claude writes or edits it, before the developer even sees the file.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "eslint --fix \"${file}\""
}
]
}
]
}
}
This is local to each developer's Claude Code session via the project settings file. The principle here is catching errors at the earliest possible moment, the point of generation, rather than at review or CI. By the time a developer reads the file, it already conforms to the rules. There is nothing to fix and nothing to be distracted by.
Lint-staged pre-commit hook, enforce it for every contributor
A lint-staged pre-commit hook in package.json runs eslint --fix --max-warnings=0 alongside the Prettier step on staged files before every commit.
{
"lint-staged": {
"*.{ts,tsx,js,jsx,cjs,mjs}": [
"prettier --write",
"eslint --fix --max-warnings=0"
]
}
}
The --max-warnings=0 flag is the key detail here. It means warnings block the commit, consistent with the CI lint gate. Without this, warnings accumulate silently, developers assume someone else will deal with them, and the codebase drifts. Matching the pre-commit gate to the CI gate means there are no surprises: if it commits, it will pass CI.
Critically, this layer enforces the rules for all contributors regardless of editor setup. Not everyone has the Claude Code hook. Not everyone has ESLint configured in their editor. The pre-commit hook is the safety net that makes the standard universal.
CI auto-fix job, catch what slips through and commit it back
A dedicated eslint-auto-fix job in .github/workflows/npm-docker-build-image.yaml runs eslint --fix on PR-changed files only, commits any auto-fixed changes back to the PR branch, then re-runs ESLint to verify. If unfixable errors remain after that second pass, the build fails and a developer resolves them.
eslint-auto-fix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
fetch-depth: 0
- name: Get changed files
id: changed
run: |
echo "files=$(git diff --name-only --diff-filter=ACMRT \
origin/${{ github.base_ref }}...HEAD \
| grep -E '\.(ts|tsx|js|jsx|cjs|mjs)$' \
| tr '\n' ' ')" >> $GITHUB_OUTPUT
- name: Run ESLint auto-fix
run: npx eslint --fix ${{ steps.changed.outputs.files }}
- name: Commit fixes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "chore: eslint auto-fix"
- name: Verify — fail if unfixable errors remain
run: npx eslint --max-warnings=0 ${{ steps.changed.outputs.files }}
Scoping to changed files using a triple-dot merge-base diff is the critical design choice here. Running ESLint across the entire codebase on every PR would cause failures on pre-existing errors in files nobody touched, punishing contributors for problems they didn't introduce and creating a chilling effect on otherwise safe changes. Scoping to the diff means the bar is clear: the code you changed must pass. Everything else is a separate concern.
Each layer catches what the previous one misses
| Layer | When it runs | Who it covers | What it does |
|---|---|---|---|
| Claude Code hook | At file generation | Claude Code users | Auto-fixes before the developer sees the file |
| lint-staged | At every commit | All contributors | Blocks commits with warnings, auto-fixes staged files |
| CI auto-fix job | On every PR | All contributors | Auto-fixes and commits back, fails on unfixable errors |
No single layer is sufficient on its own. The Claude Code hook is the fastest feedback loop but only covers one tool. Lint-staged is universal but can be bypassed with --no-verify. The CI job is the authoritative gate but runs last. Together they create a pipeline with no gaps, violations caught at generation don't reach the commit, violations that reach the commit don't reach CI, and violations that reach CI are auto-fixed before a human ever sees them.
Closing
Final thoughts
The goal of automating linting isn't to be strict, it's to make the right thing the effortless thing. When lint violations are caught and fixed automatically, developers stop thinking about style entirely. Reviews become conversations about logic and architecture, which is where human attention actually belongs.
The three-layer structure reflects a practical reality: you cannot rely on a single enforcement point when contributors have different editor setups, different tools, and different habits. Each layer targets a different moment and a different audience. The overlap is intentional, defence in depth for code quality works the same way it does for security.
Start with lint-staged if you're building this from scratch. It's one config block, it's universal, and it immediately changes the culture around what "ready to commit" means. Add the CI job next for the authoritative gate. The Claude Code hook is the polish, the layer that makes the feedback loop so tight that most developers will rarely see a lint error at all.