Claude Code Hooks: Lint After Every Edit
Make Claude Code see lint failures immediately
I want lint feedback while Claude Code is changing files, not ten minutes later when I finally run the checks. A PostToolUse hook gives us that loop: Claude edits a file, ESLint checks it, and failures go back into Claude’s context.
We’ll build this in a disposable JavaScript project. The hook checks the file touched by Edit or Write, leaves formatting alone, and reports failures without pretending the edit never happened.
This works independently of your model routing. If you already run Claude Code through Fluxo, switching between models such as Claude Sonnet 5.5 and Claude Haiku 4.5 does not require changing this hook.
Create a small ESLint project
Use Node.js 24, npm, and an installed Claude Code CLI. Run these commands in your terminal:
mkdir claude-lint-hook-demo && cd claude-lint-hook-demo && npm init -y && npm install --save-dev eslint @eslint/js
printf '%s\n' 'import js from "@eslint/js";' 'export default [js.configs.recommended];' > eslint.config.mjs
printf '%s\n' 'export const answer = 42;' > demo.js
npx eslint demo.js — The last command should exit successfully. Establish that baseline first; otherwise, you’re debugging ESLint configuration and hook wiring simultaneously. In an existing repository, keep its configuration and install dependencies using its normal package manager.
Write the PostToolUse lint runner
Claude Code sends command hooks a JSON payload on standard input. For Edit and Write, tool_input.file_path identifies the edited file. Our runner reads that path and invokes the locally installed ESLint executable through npx.
mkdir -p .claude/hooks — printf '%s\n' 'const fs = require("node:fs");' 'const path = require("node:path");' 'const { spawnSync } = require("node:child_process");' 'const input = JSON.parse(fs.readFileSync(0, "utf8"));' 'const file = input.tool_input?.file_path;' 'if (!file || !/\.(?:js|mjs|cjs)$/.test(file)) process.exit(0);' 'const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();' 'const target = path.resolve(root, file);' 'const relative = path.relative(root, target);' 'if (relative.startsWith("..") || path.isAbsolute(relative)) process.exit(0);' 'const result = spawnSync("npx", ["--no-install", "eslint", target], { cwd: root, encoding: "utf8" });' 'if (result.status !== 0) {' ' process.stderr.write("ESLint failed after edit:\n" + (result.stdout || "") + (result.stderr || "") + (result.error?.message || ""));' ' process.exit(2);' '}' > .claude/hooks/lint-after-edit.cjs
Arguments go directly to the child process rather than through shell interpolation. That matters when filenames contain spaces. The extension filter deliberately excludes TypeScript and JSX because this example has not configured their parsers.
Exit code 2 is important: for PostToolUse, Claude Code receives the standard-error output as feedback. The edit has already happened; this hook cannot roll it back. We also avoid --fix so the checker does not silently introduce another set of edits.
Register Edit and Write in project settings
The following command creates settings for this disposable project. In a real repository, merge the PostToolUse entry into your existing settings instead of overwriting them.
printf '%s\n' '{"hooks":{"PostToolUse":[{"matcher":"Edit|Write","hooks":[{"type":"command","command":"node \"$CLAUDE_PROJECT_DIR/.claude/hooks/lint-after-edit.cjs\"","timeout":30}]}]}}' > .claude/settings.json
Start a fresh session after saving the configuration:
claude
Ask Claude: “Use the Edit tool to add const unused = 1; to demo.js, then report what the hook says.” ESLint should flag the unused variable. Claude may immediately repair it; inspect the tool transcript to verify the failure appeared.
Check the feedback loop before trusting it
- Confirm a clean edit produces no lint failure feedback.
- Confirm an unused variable produces an actionable ESLint diagnostic.
- Remember that shell-based edits bypass this Edit and Write matcher.
- Keep full-project lint checks in CI for broader coverage.
These hooks run locally even when inference goes through Fluxo. Fluxo provides one key and one base URL, https://api.fluxorelay.com, for Claude, GPT, Gemini, DeepSeek, Kimi, GLM, Qwen, Grok, and more. It works with Claude Code, Codex, Gemini CLI, and any OpenAI SDK, with billing per real token, including cache reads and writes, and no monthly fee.
If that fits your terminal workflow, create a Fluxo account and try the loop. Users also earn a referral share when people they invite pay.