Files
claude-foundations/best-practices/linting.md
Paul O'Reilly d3a92326de Add composable multi-language linting and formatting system
PostToolUse hook auto-formats files after Edit/Write/MultiEdit with
git-blob checkpoints for safe revert. Pre-commit hook for staged files.
Canonical formatter scripts for py, sh, ts, sql, json (+ symlinks for
js, yaml, md). Install and setup scripts for project opt-in.

Includes best-practices/linting.md, HOOKS.md docs, README.md, and
session log.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 11:50:36 +13:00

3.9 KiB

Linting & Formatting

Automated code formatting and linting integrated into the Claude Code workflow via PostToolUse hooks and git pre-commit hooks.

Architecture

claude-foundations/formatters/   ← canonical formatter scripts (one per extension)
project/formatters/              ← symlinks to the formatters this project uses
~/.claude/hooks/post-edit-lint.sh ← dispatches to project formatters on Edit/Write
.git/hooks/pre-commit            ← symlink to pre-commit-lint.sh

Projects opt in by symlinking only the formatters they need. No formatter = no-op.

Tool Choices by Language

Language Formatter Linter Config File
Python ruff format ruff check pyproject.toml
Shell shfmt shellcheck .editorconfig
TypeScript/JavaScript biome format biome check biome.json
SQL sqlfluff fix sqlfluff lint .sqlfluff
JSON/YAML/Markdown prettier .prettierrc

Why these tools?

  • ruff: Rust-based, extremely fast, replaces black + isort + flake8 + pyflakes in one tool
  • biome: Rust-based, replaces prettier + eslint for JS/TS in one tool
  • shfmt + shellcheck: The standard combo for shell scripts; shfmt formats, shellcheck catches bugs
  • prettier: Handles JSON/YAML/MD well; biome doesn't cover these yet

Formatter Script Contract

Every script in formatters/ follows the same interface:

  • Input: $1 = absolute file path
  • Behaviour: format the file in place, then lint it
  • Stdout: suppressed
  • Stderr: lint warnings/errors that couldn't be auto-fixed
  • Exit code: 0 if clean, 1 if lint errors remain
  • Missing tools: exit 0 silently (command -v check)
  • No set -e: individual commands may fail; execution must continue

PostToolUse Integration

The post-edit-lint.sh hook fires on Edit/Write/MultiEdit:

  1. Extracts file_path and cwd from stdin JSON
  2. Walks up from cwd to find formatters/ directory
  3. Creates a checkpoint using git hash-object (fast, no commits)
  4. Runs the matching formatter
  5. On clean pass: removes checkpoint, exits 0 (silent)
  6. On lint errors: keeps checkpoint, exits 2 (feeds errors to Claude)

Exit code 2 is special for PostToolUse — it feeds stderr back to Claude as feedback without blocking the edit.

Checkpoint Mechanism

Uses git hash-object -w to store pre-format content as an orphan blob (~1ms, no commits, no stash). The .pre-lint file stores only a 40-char SHA. Falls back to cp outside git repos.

# Revert after lint errors:
git cat-file blob "$(cat file.py.pre-lint)" > file.py
rm file.py.pre-lint

Subagent Fix Pattern

When lint errors occur, Claude sees the errors via stderr. The recommended workflow:

  1. Claude reports the errors to the user
  2. If the user says "fix", Claude spawns a subagent via the Agent tool
  3. The subagent reads the file and errors, makes Edit calls to fix them
  4. Each Edit triggers the hook again (re-format, re-lint) in the subagent
  5. Fix iterations stay in the subagent's context, not the main conversation

Pre-commit Integration

pre-commit-lint.sh reuses the same formatter scripts:

  1. Iterates staged files (Added/Modified only)
  2. Runs matching formatters
  3. Re-stages formatted files
  4. Exits non-zero if lint errors remain (blocks commit)

Install: symlink .git/hooks/pre-commitpre-commit-lint.sh, or use setup-formatters.sh which does this automatically.

Setup Checklist

  1. Run scripts/setup-formatters.sh <project> <ext> [<ext> ...] to symlink formatters
  2. Install the required tools (ruff, shfmt, shellcheck, biome, prettier, sqlfluff)
  3. Create per-project config files as needed (pyproject.toml, .editorconfig, etc.)
  4. Run scripts/install-hooks.sh to install the PostToolUse hook (one-time global setup)
  5. Use /linter scan to verify everything is connected