Files
best-practices/linting.md
Paul O'Reilly 3efe153ca1 Populate best practices from claude-foundations
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>
2026-03-28 17:46:13 +13:00

95 lines
3.9 KiB
Markdown

# 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.
```bash
# 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-commit``pre-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