HookStack
Back to catalogue
ValidationPostToolUse· Write|Edit

Biome format + lint after write

Lint errors fixed in the same loop, not in CI

Formats the file with biome check --write and returns the remaining lint errors to the agent so it fixes them immediately within the same loop.

What does the Biome format + lint after write hook do?

Biome format + lint after write is a Claude Code PostToolUse hook matching Write|Edit. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. Lint errors fixed in the same loop, not in CI.

As a PostToolUse hook it runs after the action, reacting to what just happened rather than blocking it. Because it is a deterministic Node.js script, it executes on every matching event without relying on the model to remember — the guarantee that makes agentic workflows safe to automate.

Use cases

  • Continuous quality
  • Auto-fix loop
  • Lint standards

Tags

#validation#biome#lint#format#quality

settings.json fragment

{
  "hooks": {
    "PostToolUse": [
      {
        "hooks": [
          {
            "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/post-write-biome.mjs",
            "type": "command"
          }
        ],
        "matcher": "Write|Edit"
      }
    ]
  }
}

Script · .claude/hooks/post-write-biome.mjs

#!/usr/bin/env node
// @hookstack post-write-biome
// Formate et vérifie le fichier avec Biome après écriture (PostToolUse Write|Edit)
import { execSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { hookSpawnEnv } from "./lib/spawn-env.mjs";

function defaultExec(cmd) {
	return execSync(cmd, {
		stdio: "pipe",
		timeout: 15_000,
		env: hookSpawnEnv(process.env.CLAUDE_PROJECT_DIR ?? process.cwd()),
	});
}

export function run(input, { exec = defaultExec } = {}) {
	const filePath = input.tool_input?.file_path ?? "";
	if (!filePath || !/\.([cm]?[jt]sx?|jsonc?)$/.test(filePath)) return null;

	try {
		// `biome check --write --error-on-warnings` : formate (--write) ET remonte
		// les erreurs restantes — fusion de l'ancien post-write-autoformat (format)
		// et post-write-biome (lint). `biome check` (et non `biome lint`) pour matcher
		// le CI qui lance `biome check .` (lint + format + organize imports).
		exec(`npx --no-install biome check --write --error-on-warnings "${filePath}"`);
		return null;
	} catch (err) {
		const output = err.stdout?.toString() ?? "";
		return output ? { message: `Biome: ${output.trim()}\n` } : null;
	}
}

/* v8 ignore next 5 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
	const input = JSON.parse(readFileSync(0, "utf8"));
	const result = run(input);
	if (result?.message) process.stderr.write(result.message);
}

Learn more

Related hooks