Migrates 20 topic files from claude-foundations/best-practices/ to this standalone repo. Adds BESTPRACTICES.md index, CLAUDE.md conventions, and updated README.md. Container agents clone this repo to /best-practices. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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:
0if clean,1if lint errors remain - Missing tools: exit
0silently (command -vcheck) - No
set -e: individual commands may fail; execution must continue
PostToolUse Integration
The post-edit-lint.sh hook fires on Edit/Write/MultiEdit:
- Extracts
file_pathandcwdfrom stdin JSON - Walks up from
cwdto findformatters/directory - Creates a checkpoint using
git hash-object(fast, no commits) - Runs the matching formatter
- On clean pass: removes checkpoint, exits 0 (silent)
- 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:
- Claude reports the errors to the user
- If the user says "fix", Claude spawns a subagent via the Agent tool
- The subagent reads the file and errors, makes Edit calls to fix them
- Each Edit triggers the hook again (re-format, re-lint) in the subagent
- Fix iterations stay in the subagent's context, not the main conversation
Pre-commit Integration
pre-commit-lint.sh reuses the same formatter scripts:
- Iterates staged files (Added/Modified only)
- Runs matching formatters
- Re-stages formatted files
- Exits non-zero if lint errors remain (blocks commit)
Install: symlink .git/hooks/pre-commit → pre-commit-lint.sh, or use setup-formatters.sh which does this automatically.
Setup Checklist
- Run
scripts/setup-formatters.sh <project> <ext> [<ext> ...]to symlink formatters - Install the required tools (
ruff,shfmt,shellcheck,biome,prettier,sqlfluff) - Create per-project config files as needed (
pyproject.toml,.editorconfig, etc.) - Run
scripts/install-hooks.shto install the PostToolUse hook (one-time global setup) - Use
/linter scanto verify everything is connected