HookStack
Back to catalogue
ValidationPostToolUse· Write|Edit

Ruff format after write

Python lands PEP 8-clean on every save

After every Python file write or edit, runs ruff format via uv to apply consistent style without any agent intervention.

What does the Ruff format after write hook do?

Ruff format 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. Python lands PEP 8-clean on every save.

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

  • PEP 8 compliance without thinking about it
  • Consistent formatting across a team
  • Reduced review noise on style changes

Tags

#validation#ruff#format#python#quality

settings.json fragment

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

Script · .claude/hooks/ruff-format.mjs

#!/usr/bin/env node
// @hookstack post-write-ruff-format
import { execSync } from "node:child_process";
// @hookstack post-write-ruff-format
// Formate le fichier Python avec ruff après écriture (PostToolUse Write|Edit)
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";

function defaultExec(cmd) {
	execSync(cmd, { stdio: "ignore", timeout: 15_000 });
}

export function run(input, { exec = defaultExec } = {}) {
	const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? "";
	if (!filePath.endsWith(".py")) return null;

	try {
		exec(`uv run ruff format "${filePath}"`);
		return null;
	} catch {
		// uv/ruff absent — non bloquant
		return null;
	}
}

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

Learn more

Related hooks