Project 3: Auto-Formatter Pipeline - PostToolUse Hook
Project 3: Auto-Formatter Pipeline - PostToolUse Hook
Project Overview
| Attribute | Value |
|---|---|
| Project Number | 3 of 40 |
| Category | Hooks System Mastery |
| Main Programming Language | Bun/TypeScript |
| Alternative Languages | Python, Bash, Deno |
| Difficulty Level | Level 2: Intermediate |
| Time Estimate | 1 Weekend |
| Coolness Level | Level 3: Genuinely Clever |
| Business Potential | Micro-SaaS / Pro Tool |
| Knowledge Area | Hooks / Code Quality / Developer Experience |
| Primary Tool | PostToolUse Hooks, Prettier, ESLint, Black |
| Main Reference | âClean Codeâ by Robert C. Martin |
Summary: Build a PostToolUse hook that automatically runs formatters (Prettier, Black, gofmt) on files after Claude writes or edits them, ensuring all AI-generated code matches your projectâs style guidelines.
Real World Outcome
After Claude writes any file, your hook automatically formats it:
You: Create a React component for user authentication
Claude: I'll create the component...
[Uses Write tool to create auth-form.tsx]
+--------------------------------------------------+
| AUTO-FORMATTER RESULTS |
+--------------------------------------------------+
| |
| File: src/components/auth-form.tsx |
| Formatter: Prettier |
| Status: Formatted |
| |
| Changes: |
| - Added trailing commas |
| - Fixed import order |
| - Normalized quotes to single |
| |
+--------------------------------------------------+
[Uses Write tool to create auth-form.test.tsx]
+--------------------------------------------------+
| AUTO-FORMATTER RESULTS |
+--------------------------------------------------+
| |
| File: src/components/auth-form.test.tsx |
| Formatter: Prettier |
| Status: Formatted (1 fix applied) |
| |
+--------------------------------------------------+
Every file Claude touches is automatically formatted to your projectâs standards - no manual intervention required.
What This Teaches You
- PostToolUse hook execution timing
- Detecting file types from paths
- Spawning child processes in Bun/TypeScript
- Graceful error handling for missing formatters
- Respecting project-specific formatter configurations
The Core Question Youâre Answering
âHow can I ensure that every file Claude Code modifies automatically conforms to my projectâs style guidelines?â
PostToolUse hooks are fundamentally different from PreToolUse:
PreToolUse PostToolUse
---------- -----------
BEFORE tool executes AFTER tool completes
+------------------+ +------------------+
| | | |
| Can BLOCK action | | Can POST-PROCESS |
| Exit 2 = deny | | Exit 2 = no-op |
| | | |
| Use for: | | Use for: |
| - Security | | - Formatting |
| - Validation | | - Logging |
| - Rate limiting | | - Notifications |
| | | - Syncing |
+------------------+ +------------------+
PostToolUse hooks cannot block - the tool has already executed. Instead, theyâre perfect for:
- Auto-formatting code after writes
- Logging tool usage for analytics
- Triggering notifications on completion
- Syncing changes to external systems
Concepts You Must Understand First
Stop and research these before coding:
1. PostToolUse vs PreToolUse
| Aspect | PreToolUse | PostToolUse |
|---|---|---|
| Timing | Before tool runs | After tool completes |
| Can Block | Yes (exit 2) | No |
| Has Result | No | Yes (tool_output) |
| Use Case | Security, validation | Formatting, logging |
| Exit 2 Effect | Block tool | Nothing special |
PostToolUse payload includes tool_output:
{
"hook_event_name": "PostToolUse",
"session_id": "abc123",
"tool_name": "Write",
"tool_input": {
"file_path": "/path/to/app.tsx",
"content": "..."
},
"tool_output": {
"success": true,
"bytes_written": 1024
}
}
Reference: Claude Code Docs - âHook Eventsâ
2. Formatter Ecosystem
Know which formatters handle which languages:
| Language | Formatter | Config File | Install |
|---|---|---|---|
| JavaScript/TypeScript | Prettier | .prettierrc |
npm i -g prettier |
| Python | Black | pyproject.toml |
pip install black |
| Python (fast) | Ruff | pyproject.toml |
pip install ruff |
| Go | gofmt | (built-in) | (with Go) |
| Rust | rustfmt | rustfmt.toml |
rustup component add rustfmt |
| Java | google-java-format | - | Download JAR |
| C/C++ | clang-format | .clang-format |
With LLVM |
| SQL | sql-formatter | - | npm i -g sql-formatter |
| Markdown | Prettier | .prettierrc |
npm i -g prettier |
Reference: Each formatterâs documentation
3. Bun as a Scripting Runtime
Bun is a fast JavaScript runtime ideal for CLI scripts:
// Reading stdin
const payload = await Bun.stdin.json();
// Spawning processes
const result = Bun.spawnSync(["prettier", "--write", filePath]);
// Checking exit codes
if (result.exitCode === 0) {
console.log("Success!");
}
Why Bun over Node?
- Faster startup time (~10x)
- Built-in TypeScript support (no compilation)
- Simpler APIs for CLI tools
- Better shell integration
Reference: Bun documentation - âScriptingâ
Questions to Guide Your Design
Before implementing, think through these decisions:
1. Which Tools Trigger Formatting?
+----------------+------------------+---------------------------+
| Tool | Creates/Modifies | Should Format? |
+----------------+------------------+---------------------------+
| Write | Creates files | YES |
| Edit | Modifies files | YES |
| MultiEdit | Batch modifies | YES |
| NotebookEdit | Jupyter cells | MAYBE (nbqa + formatters) |
| Read | No changes | NO |
| Glob | No changes | NO |
| Grep | No changes | NO |
| Bash | Maybe | COMPLEX (detect file ops) |
+----------------+------------------+---------------------------+
2. How to Map Extensions to Formatters?
Design your extension-to-formatter mapping:
const FORMATTERS: Record<string, {command: string[], check?: string[]}> = {
// JavaScript/TypeScript
'.js': { command: ['prettier', '--write'] },
'.jsx': { command: ['prettier', '--write'] },
'.ts': { command: ['prettier', '--write'] },
'.tsx': { command: ['prettier', '--write'] },
'.mjs': { command: ['prettier', '--write'] },
'.cjs': { command: ['prettier', '--write'] },
// Python
'.py': { command: ['black'] },
'.pyi': { command: ['black'] },
// Go
'.go': { command: ['gofmt', '-w'] },
// Rust
'.rs': { command: ['rustfmt'] },
// Web
'.css': { command: ['prettier', '--write'] },
'.scss': { command: ['prettier', '--write'] },
'.html': { command: ['prettier', '--write'] },
'.json': { command: ['prettier', '--write'] },
'.md': { command: ['prettier', '--write'] },
'.yaml': { command: ['prettier', '--write'] },
'.yml': { command: ['prettier', '--write'] },
};
3. Error Handling
What if the formatter isnât installed or the file has syntax errors?
+------------------------+----------------------------------+
| Scenario | How to Handle |
+------------------------+----------------------------------+
| Formatter not found | Skip silently, log warning |
| Syntax error in file | Log error, don't fail hook |
| Formatter timeout | Kill process, log warning |
| Unknown extension | Skip (no formatter for this) |
| File was deleted | Skip (nothing to format) |
+------------------------+----------------------------------+
Thinking Exercise
Map the Formatter Pipeline
Trace what happens when Claude writes a Python file:
+-----------------------------------------------------------------------+
| FORMATTER PIPELINE EXECUTION |
+-----------------------------------------------------------------------+
| |
| 1. Claude calls Write tool |
| tool_input: {file_path: "app.py", content: "..."} |
| |
| 2. Write tool executes |
| Creates file at /path/to/app.py |
| |
| 3. PostToolUse event fires |
| +----------------------------------------------------------+ |
| | Payload: | |
| | { | |
| | hook_event_name: "PostToolUse", | |
| | tool_name: "Write", | |
| | tool_input: {file_path: "app.py", content: "..."}, | |
| | tool_output: {success: true} | |
| | } | |
| +----------------------------------------------------------+ |
| |
| 4. Hook extracts file_path from tool_input |
| path = "app.py" |
| |
| 5. Hook detects .py extension |
| ext = ".py" |
| |
| 6. Hook looks up formatter |
| formatter = ["black", "app.py"] |
| |
| 7. Hook runs: black app.py |
| +----------------------+ |
| | reformatted app.py | |
| +----------------------+ |
| |
| 8. Hook logs result to stderr |
| "Formatted: app.py (Black)" |
| |
| 9. Hook exits 0 |
| Claude continues... |
| |
+-----------------------------------------------------------------------+
Questions While Tracing:
- What if the file_path is relative?
- Need to resolve against cwd from payload or process.cwd()
- Should you check if Black is installed before running?
- Yes, use
which blackor try-catch around spawn
- Yes, use
- What if Black modifies the file - does Claude know?
- No! Claudeâs next read will see formatted version
- This is usually fine, but could cause confusion in multi-file edits
The Interview Questions Theyâll Ask
1. âHow would you implement automatic code formatting in a CI/CD pipeline?â
Answer: Use a PostToolUse hook that detects file modifications, determines the appropriate formatter based on file extension, and runs it with project-specific configuration. The hook should:
- Check if the formatter is available
- Respect .prettierrc, pyproject.toml configs
- Handle errors gracefully
- Log results for debugging
- Exit 0 regardless of formatting success (donât break the pipeline)
2. âWhatâs the difference between blocking (PreToolUse) and post-processing (PostToolUse) hooks?â
Answer:
- PreToolUse runs before the tool and can block with exit 2. The tool hasnât executed yet, so youâre preventing an action.
- PostToolUse runs after the tool completes. The action is done, so you can only react to it (format, log, notify). Exit 2 has no special meaning.
The key distinction: PreToolUse is preventive, PostToolUse is reactive.
3. âHow would you handle a formatter that takes a long time (e.g., large files)?â
Answer:
- Set a timeout on the spawn process (e.g., 10 seconds)
- Kill the process if it exceeds timeout
- Log a warning but donât fail the hook
- Consider async formatting (queue the format, return immediately)
- For very large files, skip formatting or use faster alternatives
4. âShould your hook respect .prettierignore files?â
Answer: Yes! The hook should:
- Check if a .prettierignore (or equivalent) exists
- Either pass the ignore file to the formatter, or
- Check the file against ignore patterns before running
Most formatters handle this automatically when you run them from the project root.
5. âHow would you make the formatter hook configurable per-project?â
Answer: Create a .claude/formatter-config.json in each project:
{
"enabled": true,
"formatters": {
".py": {"command": ["ruff", "format"]}, // Use ruff instead of black
".ts": {"command": ["biome", "format", "--write"]} // Use biome
},
"ignore": ["*.generated.ts", "vendor/*"]
}
Load this config at hook start and merge with defaults.
Hints in Layers
Hint 1: Filter by Tool Name
Only run formatting for file-modifying tools:
const payload = await Bun.stdin.json();
if (!['Write', 'Edit', 'MultiEdit'].includes(payload.tool_name)) {
process.exit(0); // Not a file modification, skip
}
Hint 2: Use a Formatter Map
Create an object mapping extensions to commands:
const formatters: Record<string, string[]> = {
'.ts': ['prettier', '--write'],
'.tsx': ['prettier', '--write'],
'.py': ['black'],
'.go': ['gofmt', '-w'],
};
const ext = path.extname(filePath);
const formatter = formatters[ext];
Hint 3: Spawn Formatters Correctly
Use Bun.spawnSync for synchronous execution:
if (formatter) {
const result = Bun.spawnSync([...formatter, filePath]);
if (result.exitCode === 0) {
console.error(`Formatted: ${filePath}`);
} else {
console.error(`Format failed: ${result.stderr.toString()}`);
}
}
Hint 4: Handle Missing Formatters
Check if the formatter exists before running:
function formatterExists(name: string): boolean {
const result = Bun.spawnSync(['which', name]);
return result.exitCode === 0;
}
if (formatter && formatterExists(formatter[0])) {
// Run formatter
} else {
console.error(`Skipping format: ${formatter?.[0] ?? 'unknown'} not found`);
}
Books That Will Help
| Topic | Book | Chapter | Why It Helps |
|---|---|---|---|
| Code formatting philosophy | âClean Codeâ by Robert C. Martin | Ch. 5 | Why consistent formatting matters |
| TypeScript CLI tools | âProgramming TypeScriptâ by Boris Cherny | Ch. 1, 12 | Writing type-safe CLI scripts |
| Shell process spawning | âThe Linux Command Lineâ by William Shotts | Ch. 24 | Understanding process execution |
| Async patterns | âJavaScript: The Good Partsâ by Crockford | Ch. 4 | Handling async formatters |
| Node.js patterns | âNode.js Design Patternsâ by Casciaro | Ch. 6 | Process management patterns |
Implementation Guide
Complete Bun/TypeScript Implementation
#!/usr/bin/env bun
/**
* Auto-Formatter Pipeline - PostToolUse Hook
*
* Automatically formats files after Claude writes or edits them.
* Respects project-specific formatter configurations.
*/
import { $ } from "bun";
import * as path from "path";
import * as fs from "fs";
// ===== CONFIGURATION =====
interface FormatterConfig {
command: string[];
extensions: string[];
checkInstalled?: string;
}
const DEFAULT_FORMATTERS: FormatterConfig[] = [
// JavaScript/TypeScript (Prettier)
{
command: ["prettier", "--write"],
extensions: [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"],
checkInstalled: "prettier",
},
// Web formats (Prettier)
{
command: ["prettier", "--write"],
extensions: [".css", ".scss", ".less", ".html", ".json", ".md", ".yaml", ".yml"],
checkInstalled: "prettier",
},
// Python (Black)
{
command: ["black", "--quiet"],
extensions: [".py", ".pyi"],
checkInstalled: "black",
},
// Go (gofmt)
{
command: ["gofmt", "-w"],
extensions: [".go"],
checkInstalled: "gofmt",
},
// Rust (rustfmt)
{
command: ["rustfmt"],
extensions: [".rs"],
checkInstalled: "rustfmt",
},
// Shell (shfmt)
{
command: ["shfmt", "-w"],
extensions: [".sh", ".bash"],
checkInstalled: "shfmt",
},
];
const MODIFYING_TOOLS = ["Write", "Edit", "MultiEdit"];
// ===== HELPER FUNCTIONS =====
interface HookPayload {
hook_event_name: string;
session_id: string;
tool_name: string;
tool_input: {
file_path?: string;
content?: string;
edits?: Array<{ file_path: string }>;
};
tool_output: {
success?: boolean;
};
cwd?: string;
}
async function isInstalled(command: string): Promise<boolean> {
try {
const result = Bun.spawnSync(["which", command]);
return result.exitCode === 0;
} catch {
return false;
}
}
function getFormatter(extension: string): FormatterConfig | undefined {
return DEFAULT_FORMATTERS.find((f) => f.extensions.includes(extension));
}
function resolvePath(filePath: string, cwd?: string): string {
if (path.isAbsolute(filePath)) {
return filePath;
}
return path.resolve(cwd || process.cwd(), filePath);
}
function loadProjectConfig(projectPath: string): Partial<{
enabled: boolean;
formatters: Record<string, { command: string[] }>;
ignore: string[];
}> {
const configPath = path.join(projectPath, ".claude", "formatter-config.json");
try {
if (fs.existsSync(configPath)) {
return JSON.parse(fs.readFileSync(configPath, "utf-8"));
}
} catch {
// Config doesn't exist or is invalid
}
return {};
}
function shouldIgnore(filePath: string, ignorePatterns: string[]): boolean {
const fileName = path.basename(filePath);
const relativePath = filePath;
for (const pattern of ignorePatterns) {
// Simple glob matching
if (pattern.startsWith("*")) {
const suffix = pattern.slice(1);
if (fileName.endsWith(suffix) || relativePath.endsWith(suffix)) {
return true;
}
} else if (pattern.endsWith("*")) {
const prefix = pattern.slice(0, -1);
if (relativePath.startsWith(prefix)) {
return true;
}
} else if (relativePath.includes(pattern)) {
return true;
}
}
return false;
}
async function formatFile(
filePath: string,
formatter: FormatterConfig,
projectConfig: ReturnType<typeof loadProjectConfig>
): Promise<{ success: boolean; message: string }> {
// Check if formatter is installed
const checkCmd = formatter.checkInstalled || formatter.command[0];
if (!(await isInstalled(checkCmd))) {
return {
success: false,
message: `Formatter '${checkCmd}' not installed`,
};
}
// Check if file exists
if (!fs.existsSync(filePath)) {
return {
success: false,
message: `File not found: ${filePath}`,
};
}
// Check project-specific override
const ext = path.extname(filePath);
let command = formatter.command;
if (projectConfig.formatters?.[ext]?.command) {
command = projectConfig.formatters[ext].command;
}
// Run formatter
try {
const result = Bun.spawnSync([...command, filePath], {
cwd: path.dirname(filePath),
timeout: 10000, // 10 second timeout
});
if (result.exitCode === 0) {
return {
success: true,
message: `Formatted with ${command[0]}`,
};
} else {
const stderr = result.stderr.toString().trim();
return {
success: false,
message: stderr || `${command[0]} exited with code ${result.exitCode}`,
};
}
} catch (error) {
return {
success: false,
message: `Error running formatter: ${error}`,
};
}
}
function printResult(filePath: string, success: boolean, message: string) {
const icon = success ? "+" : "!";
const status = success ? "Formatted" : "Skipped";
console.error("");
console.error(`[${icon}] ${path.basename(filePath)}`);
console.error(` Status: ${status}`);
console.error(` ${message}`);
}
// ===== MAIN HANDLER =====
async function main() {
// Read payload from stdin
let payload: HookPayload;
try {
payload = await Bun.stdin.json();
} catch {
// Can't parse payload, exit silently
process.exit(0);
}
// Only process file-modifying tools
if (!MODIFYING_TOOLS.includes(payload.tool_name)) {
process.exit(0);
}
// Check if tool was successful
if (payload.tool_output?.success === false) {
process.exit(0);
}
// Collect file paths to format
const filePaths: string[] = [];
if (payload.tool_input.file_path) {
filePaths.push(payload.tool_input.file_path);
}
// MultiEdit may have multiple files
if (payload.tool_input.edits) {
for (const edit of payload.tool_input.edits) {
if (edit.file_path && !filePaths.includes(edit.file_path)) {
filePaths.push(edit.file_path);
}
}
}
if (filePaths.length === 0) {
process.exit(0);
}
// Load project config
const cwd = payload.cwd || process.cwd();
const projectConfig = loadProjectConfig(cwd);
// Check if formatting is disabled
if (projectConfig.enabled === false) {
process.exit(0);
}
const ignorePatterns = projectConfig.ignore || [];
// Format each file
console.error("\n+--------------------------------------------------+");
console.error("| AUTO-FORMATTER RESULTS |");
console.error("+--------------------------------------------------+");
for (const filePath of filePaths) {
const resolvedPath = resolvePath(filePath, cwd);
// Check ignore patterns
if (shouldIgnore(resolvedPath, ignorePatterns)) {
printResult(filePath, false, "Ignored by config");
continue;
}
// Find formatter for this file type
const ext = path.extname(resolvedPath);
const formatter = getFormatter(ext);
if (!formatter) {
printResult(filePath, false, `No formatter for ${ext}`);
continue;
}
// Format the file
const result = await formatFile(resolvedPath, formatter, projectConfig);
printResult(filePath, result.success, result.message);
}
console.error("+--------------------------------------------------+\n");
// Always exit 0 - formatting failures shouldn't break the session
process.exit(0);
}
main();
Configuration in settings.json
{
"hooks": [
{
"event": "PostToolUse",
"type": "command",
"command": "bun run ~/.claude/hooks/auto-formatter.ts",
"timeout": 15000
}
]
}
Project-Specific Config (.claude/formatter-config.json)
{
"enabled": true,
"formatters": {
".py": {
"command": ["ruff", "format"]
},
".ts": {
"command": ["biome", "format", "--write"]
}
},
"ignore": [
"*.generated.ts",
"vendor/*",
"dist/*",
"node_modules/*"
]
}
Architecture Diagram
+-----------------------------------------------------------------------+
| AUTO-FORMATTER HOOK FLOW |
+-----------------------------------------------------------------------+
| |
| Claude: Write tool |
| file_path: "src/components/Button.tsx" |
| | |
| v |
| +--------------------+ |
| | Write Tool Executes| |
| | Creates file | |
| +--------------------+ |
| | |
| v |
| +--------------------+ +----------------------------------------+ |
| | PostToolUse fires |---->| auto-formatter.ts | |
| +--------------------+ | | |
| | 1. Parse payload | |
| | tool_name: "Write" | |
| | file_path: "Button.tsx" | |
| | | |
| | 2. Check tool_name in MODIFYING_TOOLS | |
| | ["Write", "Edit", "MultiEdit"] - Yes| |
| | | |
| | 3. Load project config | |
| | .claude/formatter-config.json | |
| | | |
| | 4. Check ignore patterns | |
| | Not in ignore list - continue | |
| | | |
| | 5. Get extension: ".tsx" | |
| | | |
| | 6. Find formatter | |
| | .tsx -> ["prettier", "--write"] | |
| | | |
| | 7. Check if prettier installed | |
| | which prettier -> /usr/local/bin | |
| | | |
| | 8. Run formatter | |
| | prettier --write Button.tsx | |
| | | |
| | 9. Print result to stderr | |
| | | |
| | 10. exit(0) | |
| +----------------------------------------+ |
| |
| Claude continues with next action... |
| |
+-----------------------------------------------------------------------+
Extension-Formatter Mapping Diagram
+-----------------------------------------------------------------------+
| FILE EXTENSION ROUTING |
+-----------------------------------------------------------------------+
| |
| Input File Extension |
| | |
| +------+------+------+---+---+------+------+------+ |
| | | | | | | | | |
| v v v v v v v v |
| .ts .tsx .py .go .rs .sh .json .md |
| | | | | | | | | |
| +------+ | | | | +------+ |
| | | | | | | |
| v v v v v v |
| +--------+ +-----+ +-----+ +------+ +-----+ +--------+ |
| |Prettier| |Black| |gofmt| |rustfmt| |shfmt| |Prettier| |
| +--------+ +-----+ +-----+ +------+ +-----+ +--------+ |
| | | | | | | |
| v v v v v v |
| +-----------------------------------------------------+ |
| | Formatted File Output | |
| +-----------------------------------------------------+ |
| |
+-----------------------------------------------------------------------+
Learning Milestones
Milestone 1: Files Are Formatted After Write
Goal: See Prettier format a .ts file after Claude creates it
Test:
You: Create a simple TypeScript function at test.ts
# After Claude writes the file, you should see:
# [+] test.ts
# Status: Formatted
# Formatted with prettier
What Youâve Learned:
- PostToolUse timing
- Basic formatter invocation
- Bun process spawning
Milestone 2: Multiple Formatters Work
Goal: Python files use Black, Go files use gofmt
Test:
You: Create hello.py and hello.go with hello world programs
# Should see:
# [+] hello.py - Formatted with black
# [+] hello.go - Formatted with gofmt
What Youâve Learned:
- Extension-to-formatter routing
- Multiple formatter configurations
- Checking formatter availability
Milestone 3: Errors Are Handled Gracefully
Goal: Missing formatters donât crash the hook
Test:
# With rustfmt not installed
You: Create a Rust file at hello.rs
# Should see:
# [!] hello.rs
# Status: Skipped
# Formatter 'rustfmt' not installed
What Youâve Learned:
- Graceful degradation
- User-friendly error messages
- Always exiting 0
Common Mistakes to Avoid
Mistake 1: Blocking on Formatter Errors
// WRONG - Exits non-zero on format failure
if (result.exitCode !== 0) {
process.exit(1); // This doesn't block but signals error
}
// RIGHT - Always exit 0 for PostToolUse
if (result.exitCode !== 0) {
console.error("Format failed, but continuing...");
}
process.exit(0); // Always 0
Mistake 2: Not Handling Relative Paths
// WRONG - Assumes absolute path
const result = Bun.spawnSync(['prettier', '--write', filePath]);
// RIGHT - Resolve relative paths first
const resolved = path.isAbsolute(filePath)
? filePath
: path.resolve(cwd, filePath);
const result = Bun.spawnSync(['prettier', '--write', resolved]);
Mistake 3: No Timeout on Formatter
// WRONG - Could hang forever on large files
const result = Bun.spawnSync(['prettier', '--write', file]);
// RIGHT - Set reasonable timeout
const result = Bun.spawnSync(['prettier', '--write', file], {
timeout: 10000, // 10 seconds
});
Mistake 4: Formatting Deleted Files
// WRONG - Crashes if file was deleted
const result = Bun.spawnSync(['prettier', '--write', filePath]);
// RIGHT - Check file exists first
if (!fs.existsSync(filePath)) {
console.error("File not found, skipping format");
return;
}
Extension Ideas
Once the basic formatter works, consider these enhancements:
- Diff Output: Show what the formatter changed (prettier âcheck first)
- ESLint Integration: Run ESLint âfix after Prettier
- Pre-commit Style: Only format staged files in git repos
- Format on Save Log: Keep history of all formatting operations
- Formatter Metrics: Track which formatters run most, time spent
- Smart Formatter Detection: Detect from package.json, pyproject.toml
Summary
This project taught you to build a PostToolUse hook for automatic code formatting:
- PostToolUse Timing: Understanding when post-processing hooks run
- Formatter Routing: Mapping file extensions to appropriate formatters
- Process Spawning: Running external tools from Bun/TypeScript
- Error Handling: Graceful degradation when formatters are missing
- Project Config: Respecting project-specific preferences
With auto-formatting in place, Claudeâs output always matches your style. Project 4 will explore the Notification hook for multi-channel alerts, and Project 5 will tackle UserPromptSubmit for input validation.