# 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 [ ...]` 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