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>
This commit is contained in:
54
HOOKS.md
54
HOOKS.md
@@ -7,6 +7,32 @@ Claude Code hooks that run automatically in response to events. Source of truth
|
||||
| Hook | Event | File |
|
||||
|------|-------|------|
|
||||
| Pre-compact backup | PreCompact (auto + manual) | `hooks/pre-compact-backup.sh` |
|
||||
| Post-edit lint | PostToolUse (Edit/Write/MultiEdit) | `hooks/post-edit-lint.sh` |
|
||||
| Pre-commit lint | Git pre-commit | `hooks/pre-commit-lint.sh` |
|
||||
|
||||
## Post-edit Lint
|
||||
|
||||
Formats and lints files after every Edit/Write/MultiEdit. Dispatches to project-local `formatters/` directory (symlinks to `claude-foundations/formatters/`).
|
||||
|
||||
**Flow:**
|
||||
1. Extracts `file_path` and `cwd` from stdin JSON
|
||||
2. Walks up from `cwd` to find `formatters/` directory
|
||||
3. Creates a `git hash-object` checkpoint (`.pre-lint` file with blob SHA)
|
||||
4. Runs the matching formatter script
|
||||
5. On clean pass: removes checkpoint, exits 0 (silent)
|
||||
6. On lint errors: keeps checkpoint, exits 2 (feeds errors back to Claude via stderr)
|
||||
|
||||
**Requires:** `python3` (for JSON parsing from stdin). Formatter tools are optional — missing tools silently pass.
|
||||
|
||||
**Revert:** `git cat-file blob $(cat <file>.pre-lint) > <file>`
|
||||
|
||||
## Pre-commit Lint
|
||||
|
||||
Git pre-commit hook that runs project formatters on staged files. Reuses the same formatter scripts.
|
||||
|
||||
**Flow:** iterates staged files → runs matching formatter → 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 `scripts/setup-formatters.sh`.
|
||||
|
||||
## Pre-compact Backup
|
||||
|
||||
@@ -18,10 +44,23 @@ Saves a copy of the session transcript before context compaction so no conversat
|
||||
|
||||
## Installation
|
||||
|
||||
The easiest way is to run the install script:
|
||||
|
||||
```bash
|
||||
cd ~/dev/claude/projects/claude-foundations
|
||||
scripts/install-hooks.sh
|
||||
```
|
||||
|
||||
This symlinks all hooks into `~/.claude/hooks/` and prints the `settings.json` config to add.
|
||||
|
||||
### Manual installation
|
||||
|
||||
1. Symlink each hook into `~/.claude/hooks/`:
|
||||
```bash
|
||||
mkdir -p ~/.claude/hooks
|
||||
ln -sf "$(pwd)/hooks/pre-compact-backup.sh" ~/.claude/hooks/pre-compact-backup.sh
|
||||
ln -sf "$(pwd)/hooks/post-edit-lint.sh" ~/.claude/hooks/post-edit-lint.sh
|
||||
ln -sf "$(pwd)/hooks/pre-commit-lint.sh" ~/.claude/hooks/pre-commit-lint.sh
|
||||
```
|
||||
|
||||
2. Add the hook configuration to `~/.claude/settings.json`:
|
||||
@@ -31,17 +70,28 @@ Saves a copy of the session transcript before context compaction so no conversat
|
||||
"PreCompact": [
|
||||
{
|
||||
"matcher": "auto",
|
||||
"hooks": [{ "type": "command", "command": "~/.claude/hooks/pre-compact-backup.sh", "timeout": 15 }]
|
||||
"hooks": [{ "type": "command", "command": "~/.claude/hooks/pre-compact-backup.sh", "timeout": 15, "statusMessage": "Backing up transcript before compaction..." }]
|
||||
},
|
||||
{
|
||||
"matcher": "manual",
|
||||
"hooks": [{ "type": "command", "command": "~/.claude/hooks/pre-compact-backup.sh", "timeout": 15 }]
|
||||
"hooks": [{ "type": "command", "command": "~/.claude/hooks/pre-compact-backup.sh", "timeout": 15, "statusMessage": "Backing up transcript before compaction..." }]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write|MultiEdit",
|
||||
"hooks": [{ "type": "command", "command": "~/.claude/hooks/post-edit-lint.sh", "timeout": 30, "statusMessage": "Formatting and linting..." }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. For per-project formatters, run:
|
||||
```bash
|
||||
scripts/setup-formatters.sh <project-dir> <ext> [<ext> ...]
|
||||
```
|
||||
|
||||
## Adding New Hooks
|
||||
|
||||
1. Create the script in `hooks/`
|
||||
|
||||
139
README.md
Normal file
139
README.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# claude-foundations
|
||||
|
||||
Shared infrastructure for Claude Code sessions: hooks, formatters, best practices, and the knowledge distillation pipeline.
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
claude-foundations/
|
||||
formatters/ # Canonical formatter scripts (one per extension)
|
||||
py # ruff format + ruff check
|
||||
sh # shfmt + shellcheck
|
||||
ts # biome format + biome check
|
||||
js -> ts # symlink (biome handles both)
|
||||
sql # sqlfluff fix + sqlfluff lint
|
||||
json # prettier
|
||||
yaml -> json # symlink (prettier auto-detects)
|
||||
md -> json # symlink
|
||||
hooks/
|
||||
post-edit-lint.sh # PostToolUse: auto-format/lint on Edit/Write
|
||||
pre-commit-lint.sh # Git pre-commit: lint staged files
|
||||
pre-compact-backup.sh # PreCompact: backup transcript before compaction
|
||||
scripts/
|
||||
install-hooks.sh # Symlink hooks into ~/.claude/hooks/
|
||||
setup-formatters.sh # Set up formatters for a project
|
||||
best-practices/ # Generalised best practices (one file per topic)
|
||||
memory/ # Session logs and reflections
|
||||
settings.yaml # Knowledge pipeline configuration
|
||||
CLAUDE.md # Global project guidelines (symlinked to ~/dev/claude/)
|
||||
HOOKS.md # Hook documentation
|
||||
TODO.md # Improvement backlog
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Install hooks (one-time)
|
||||
|
||||
```bash
|
||||
cd ~/dev/claude/projects/claude-foundations
|
||||
scripts/install-hooks.sh
|
||||
```
|
||||
|
||||
This symlinks all hooks into `~/.claude/hooks/` and prints the `settings.json` config to add. The PostToolUse hook is what triggers auto-formatting on every Edit/Write.
|
||||
|
||||
### 2. Opt a project into auto-formatting
|
||||
|
||||
```bash
|
||||
scripts/setup-formatters.sh ~/dev/claude/projects/<project-name> <ext> [<ext> ...]
|
||||
```
|
||||
|
||||
For example, to enable Python and shell formatting for `cluster-bootstrap`:
|
||||
|
||||
```bash
|
||||
scripts/setup-formatters.sh ~/dev/claude/projects/cluster-bootstrap py sh
|
||||
```
|
||||
|
||||
This creates a `formatters/` directory in the target project with symlinks back to the canonical formatter scripts here. It also installs a git pre-commit hook if one doesn't exist.
|
||||
|
||||
**Available formatters:** `py`, `sh`, `ts`, `js`, `sql`, `json`, `yaml`, `md`
|
||||
|
||||
Run with no arguments to see the full list:
|
||||
|
||||
```bash
|
||||
scripts/setup-formatters.sh
|
||||
```
|
||||
|
||||
### 3. Install required tools
|
||||
|
||||
Each formatter gracefully skips if its tool isn't installed, but for full functionality:
|
||||
|
||||
| Formatter | Tools needed | Install |
|
||||
|-----------|-------------|---------|
|
||||
| `py` | ruff | `pip install ruff` or `pipx install ruff` |
|
||||
| `sh` | shfmt, shellcheck | `sudo apt install shfmt shellcheck` |
|
||||
| `ts`/`js` | biome | `npm i -g @biomejs/biome` |
|
||||
| `sql` | sqlfluff | `pip install sqlfluff` |
|
||||
| `json`/`yaml`/`md` | prettier | `npm i -g prettier` |
|
||||
|
||||
### 4. Add per-project config (optional)
|
||||
|
||||
Formatters respect project-level config files:
|
||||
|
||||
| Formatter | Config file | Purpose |
|
||||
|-----------|------------|---------|
|
||||
| `py` | `pyproject.toml` | ruff rules, line length, target Python version |
|
||||
| `sh` | `.editorconfig` | shfmt indent style, binary ops, switch cases |
|
||||
| `ts`/`js` | `biome.json` | biome rules, formatting options |
|
||||
| `sql` | `.sqlfluff` | SQL dialect, rules |
|
||||
| `json`/`yaml`/`md` | `.prettierrc` | prettier options |
|
||||
|
||||
Without config files, tools use their defaults.
|
||||
|
||||
### 5. Verify setup
|
||||
|
||||
Use the `/linter` skill inside a Claude Code session:
|
||||
|
||||
```
|
||||
/linter
|
||||
```
|
||||
|
||||
This scans the project, shows which formatters are active, which tools are installed, and suggests fixes for any gaps.
|
||||
|
||||
## How It Works
|
||||
|
||||
### PostToolUse hook (auto-format on edit)
|
||||
|
||||
When Claude edits or writes a file:
|
||||
|
||||
1. The `post-edit-lint.sh` hook fires
|
||||
2. It walks up from the working directory to find `formatters/`
|
||||
3. If a formatter exists for the file's extension, it:
|
||||
- Creates a checkpoint (git blob hash stored in `.pre-lint`)
|
||||
- Runs the formatter (format in place + lint)
|
||||
- On clean pass: removes checkpoint, exits silently
|
||||
- On lint errors: keeps checkpoint, exits with errors shown to Claude
|
||||
|
||||
### Pre-commit hook (lint on commit)
|
||||
|
||||
When you `git commit`, the `pre-commit-lint.sh` hook:
|
||||
|
||||
1. Finds staged files with matching formatters
|
||||
2. Runs each formatter
|
||||
3. Re-stages formatted files
|
||||
4. Blocks the commit if lint errors remain
|
||||
|
||||
### Reverting after lint
|
||||
|
||||
If you don't like what the formatter did:
|
||||
|
||||
```bash
|
||||
git cat-file blob "$(cat <file>.pre-lint)" > <file>
|
||||
rm <file>.pre-lint
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `scripts/install-hooks.sh` | Symlink all hooks to `~/.claude/hooks/` and print settings.json config |
|
||||
| `scripts/setup-formatters.sh` | Create formatter symlinks in a target project |
|
||||
10
TODO.md
10
TODO.md
@@ -2,9 +2,9 @@
|
||||
|
||||
Prioritised by impact and effort. Each item references its detailed topic file in `improvements/`.
|
||||
|
||||
## 1. Configure PostToolUse auto-formatting hook
|
||||
**Impact: High | Effort: Low**
|
||||
Auto-format code after every Edit/Write. Removes all formatting rules from CLAUDE.md, saves tokens, and guarantees consistent output. Start with `shfmt` for shell and expand.
|
||||
## 1. ~~Configure PostToolUse auto-formatting hook~~ → DONE
|
||||
**Impact: High | Effort: Low** ✅
|
||||
Composable multi-language linting system implemented. Canonical formatters in `formatters/` (py, sh, ts, sql, json + symlinks for js, yaml, md). PostToolUse dispatcher hook with checkpoint/revert. Pre-commit hook. Setup scripts. `/linter` skill for auditing.
|
||||
See: [hooks-and-automation.md](improvements/hooks-and-automation.md)
|
||||
|
||||
## 2. ~~Create global `~/.claude/CLAUDE.md`~~ → DONE (parent symlink)
|
||||
@@ -61,3 +61,7 @@ See: [planning-and-workflow.md](improvements/planning-and-workflow.md)
|
||||
- **Parent CLAUDE.md symlink** — `~/dev/claude/CLAUDE.md` → claude-foundations, all projects inherit automatically.
|
||||
- **PreCompact transcript backup hook** — `~/.claude/hooks/pre-compact-backup.sh` with auto+manual matchers.
|
||||
- **claude-foundations Gitea repo** — Created under skynet org, pushed.
|
||||
|
||||
## Completed (2026-03-13)
|
||||
|
||||
- **PostToolUse auto-formatting hook** — Composable multi-language linting system: 5 formatter scripts + 3 symlinks, PostToolUse dispatcher with git-blob checkpoints, pre-commit hook, setup scripts, `/linter` skill, `best-practices/linting.md`.
|
||||
|
||||
@@ -15,3 +15,4 @@ Generalised best practices extracted from real project work. Each topic file is
|
||||
- [Milestones & Reflections](milestones.md) — Milestone workflow, verification, reflection process
|
||||
- [Debugging Methodology](debugging.md) — Systematic diagnosis, full-chain testing, common pitfalls
|
||||
- [Claude Code Skills](skills-development.md) — Skill authoring, context injection, tool restrictions
|
||||
- [Linting & Formatting](linting.md) — Tool choices per language, PostToolUse hook, pre-commit integration, formatter contract
|
||||
|
||||
94
best-practices/linting.md
Normal file
94
best-practices/linting.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# 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
|
||||
1
formatters/js
Symbolic link
1
formatters/js
Symbolic link
@@ -0,0 +1 @@
|
||||
ts
|
||||
19
formatters/json
Executable file
19
formatters/json
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Formatter: JSON/YAML/Markdown — prettier
|
||||
# Contract: $1 = absolute file path, format in place, lint, exit 0 if clean / 1 if errors
|
||||
# Missing tools: exit 0 silently
|
||||
|
||||
FILE="$1"
|
||||
[ -z "$FILE" ] && exit 0
|
||||
[ -f "$FILE" ] || exit 0
|
||||
|
||||
command -v prettier >/dev/null 2>&1 || exit 0
|
||||
|
||||
# Format in place (prettier auto-detects parser from extension)
|
||||
ERRORS=$(prettier --write "$FILE" 2>&1)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "$ERRORS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
1
formatters/md
Symbolic link
1
formatters/md
Symbolic link
@@ -0,0 +1 @@
|
||||
json
|
||||
25
formatters/py
Executable file
25
formatters/py
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# Formatter: Python — ruff format + ruff check
|
||||
# Contract: $1 = absolute file path, format in place, lint, exit 0 if clean / 1 if errors
|
||||
# Missing tools: exit 0 silently
|
||||
|
||||
FILE="$1"
|
||||
[ -z "$FILE" ] && exit 0
|
||||
[ -f "$FILE" ] || exit 0
|
||||
|
||||
command -v ruff >/dev/null 2>&1 || exit 0
|
||||
|
||||
# Format in place
|
||||
ruff format --quiet "$FILE" 2>/dev/null
|
||||
|
||||
# Auto-fix what we can
|
||||
ruff check --fix --quiet "$FILE" 2>/dev/null
|
||||
|
||||
# Final lint pass — remaining errors go to stderr
|
||||
ERRORS=$(ruff check "$FILE" 2>&1)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "$ERRORS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
31
formatters/sh
Executable file
31
formatters/sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
# Formatter: Shell — shfmt + shellcheck
|
||||
# Contract: $1 = absolute file path, format in place, lint, exit 0 if clean / 1 if errors
|
||||
# Missing tools: exit 0 silently
|
||||
|
||||
FILE="$1"
|
||||
[ -z "$FILE" ] && exit 0
|
||||
[ -f "$FILE" ] || exit 0
|
||||
|
||||
HAD_ERRORS=0
|
||||
|
||||
# Format with shfmt if available
|
||||
if command -v shfmt >/dev/null 2>&1; then
|
||||
shfmt -w "$FILE" 2>/dev/null
|
||||
fi
|
||||
|
||||
# Lint with shellcheck if available
|
||||
if command -v shellcheck >/dev/null 2>&1; then
|
||||
ERRORS=$(shellcheck -f gcc "$FILE" 2>&1)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "$ERRORS" >&2
|
||||
HAD_ERRORS=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# If neither tool is installed, silently pass
|
||||
if ! command -v shfmt >/dev/null 2>&1 && ! command -v shellcheck >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exit $HAD_ERRORS
|
||||
22
formatters/sql
Executable file
22
formatters/sql
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# Formatter: SQL — sqlfluff fix + sqlfluff lint
|
||||
# Contract: $1 = absolute file path, format in place, lint, exit 0 if clean / 1 if errors
|
||||
# Missing tools: exit 0 silently
|
||||
|
||||
FILE="$1"
|
||||
[ -z "$FILE" ] && exit 0
|
||||
[ -f "$FILE" ] || exit 0
|
||||
|
||||
command -v sqlfluff >/dev/null 2>&1 || exit 0
|
||||
|
||||
# Auto-fix in place
|
||||
sqlfluff fix --force --quiet "$FILE" 2>/dev/null
|
||||
|
||||
# Final lint pass — remaining errors go to stderr
|
||||
ERRORS=$(sqlfluff lint "$FILE" 2>&1)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "$ERRORS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
25
formatters/ts
Executable file
25
formatters/ts
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# Formatter: TypeScript/JavaScript — biome format + biome check
|
||||
# Contract: $1 = absolute file path, format in place, lint, exit 0 if clean / 1 if errors
|
||||
# Missing tools: exit 0 silently
|
||||
|
||||
FILE="$1"
|
||||
[ -z "$FILE" ] && exit 0
|
||||
[ -f "$FILE" ] || exit 0
|
||||
|
||||
command -v biome >/dev/null 2>&1 || exit 0
|
||||
|
||||
# Format in place
|
||||
biome format --write "$FILE" 2>/dev/null
|
||||
|
||||
# Auto-fix what we can
|
||||
biome check --fix "$FILE" 2>/dev/null
|
||||
|
||||
# Final lint pass — remaining errors go to stderr
|
||||
ERRORS=$(biome check "$FILE" 2>&1)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "$ERRORS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
1
formatters/yaml
Symbolic link
1
formatters/yaml
Symbolic link
@@ -0,0 +1 @@
|
||||
json
|
||||
86
hooks/post-edit-lint.sh
Executable file
86
hooks/post-edit-lint.sh
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
# PostToolUse hook: format and lint edited/written files.
|
||||
# Dispatches to project-local formatters/ directory (symlinks to claude-foundations/formatters/).
|
||||
#
|
||||
# Input: JSON on stdin with tool_name, file_path, cwd fields
|
||||
# Exit 0: silent pass (no formatter, clean lint, or missing tools)
|
||||
# Exit 2: lint errors remain — stderr fed back to Claude as feedback
|
||||
|
||||
# Parse stdin JSON for file_path and cwd
|
||||
INPUT="$(cat)"
|
||||
FILE_PATH="$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('file_path',''))" 2>/dev/null)"
|
||||
CWD="$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('cwd',''))" 2>/dev/null)"
|
||||
|
||||
# No file path — nothing to do
|
||||
[ -z "$FILE_PATH" ] && exit 0
|
||||
|
||||
# Make path absolute if relative
|
||||
if [[ "$FILE_PATH" != /* ]]; then
|
||||
FILE_PATH="${CWD:-.}/$FILE_PATH"
|
||||
fi
|
||||
|
||||
# File must exist
|
||||
[ -f "$FILE_PATH" ] || exit 0
|
||||
|
||||
# Extract extension (normalise yml → yaml)
|
||||
EXT="${FILE_PATH##*.}"
|
||||
case "$EXT" in
|
||||
yml) EXT="yaml" ;;
|
||||
esac
|
||||
|
||||
# No extension — nothing to do
|
||||
[ -z "$EXT" ] || [ "$EXT" = "$FILE_PATH" ] && exit 0
|
||||
|
||||
# Find the project's formatters/ directory by walking up from cwd
|
||||
find_formatters_dir() {
|
||||
local dir="${1:-$CWD}"
|
||||
while [ "$dir" != "/" ] && [ -n "$dir" ]; do
|
||||
if [ -d "$dir/formatters" ]; then
|
||||
echo "$dir/formatters"
|
||||
return 0
|
||||
fi
|
||||
dir="$(dirname "$dir")"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
FORMATTERS_DIR="$(find_formatters_dir "$CWD")"
|
||||
[ -z "$FORMATTERS_DIR" ] && exit 0
|
||||
|
||||
# Check if a formatter exists for this extension
|
||||
FORMATTER="$FORMATTERS_DIR/$EXT"
|
||||
[ -x "$FORMATTER" ] || exit 0
|
||||
|
||||
# --- Checkpoint: save pre-format content ---
|
||||
if git -C "$(dirname "$FILE_PATH")" rev-parse --git-dir >/dev/null 2>&1; then
|
||||
# Inside a git repo — use git hash-object for efficient checkpoint
|
||||
BLOB="$(git -C "$(dirname "$FILE_PATH")" hash-object -w "$FILE_PATH" 2>/dev/null)"
|
||||
if [ -n "$BLOB" ]; then
|
||||
echo "$BLOB" > "${FILE_PATH}.pre-lint"
|
||||
fi
|
||||
else
|
||||
# Not a git repo — fall back to file copy
|
||||
cp "$FILE_PATH" "${FILE_PATH}.pre-lint"
|
||||
fi
|
||||
|
||||
# --- Run formatter ---
|
||||
LINT_OUTPUT=$("$FORMATTER" "$FILE_PATH" 2>&1)
|
||||
FORMATTER_EXIT=$?
|
||||
|
||||
if [ $FORMATTER_EXIT -eq 0 ]; then
|
||||
# Clean — remove checkpoint
|
||||
rm -f "${FILE_PATH}.pre-lint"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Lint errors remain ---
|
||||
# Keep .pre-lint for revert, exit 2 to feed errors back to Claude
|
||||
{
|
||||
echo "Lint errors in ${FILE_PATH}:"
|
||||
echo "$LINT_OUTPUT"
|
||||
echo ""
|
||||
echo "To fix: use the Agent tool to resolve these lint errors (keeps fix loop out of main context)."
|
||||
echo "To revert formatting: git cat-file blob \$(cat ${FILE_PATH}.pre-lint) > ${FILE_PATH}"
|
||||
} >&2
|
||||
|
||||
exit 2
|
||||
54
hooks/pre-commit-lint.sh
Executable file
54
hooks/pre-commit-lint.sh
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# Git pre-commit hook: format and lint staged files using project formatters.
|
||||
# Reuses the same formatter scripts from formatters/.
|
||||
# Exits non-zero if lint errors remain after auto-fix (blocks commit).
|
||||
|
||||
# Find formatters/ directory relative to repo root
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"
|
||||
[ -z "$REPO_ROOT" ] && exit 0
|
||||
|
||||
FORMATTERS_DIR="$REPO_ROOT/formatters"
|
||||
[ -d "$FORMATTERS_DIR" ] || exit 0
|
||||
|
||||
HAD_ERRORS=0
|
||||
ERROR_OUTPUT=""
|
||||
|
||||
# Iterate staged files (only Added/Modified, not Deleted)
|
||||
while IFS= read -r FILE; do
|
||||
[ -z "$FILE" ] && continue
|
||||
|
||||
ABS_FILE="$REPO_ROOT/$FILE"
|
||||
[ -f "$ABS_FILE" ] || continue
|
||||
|
||||
# Extract extension (normalise yml → yaml)
|
||||
EXT="${FILE##*.}"
|
||||
case "$EXT" in
|
||||
yml) EXT="yaml" ;;
|
||||
esac
|
||||
[ -z "$EXT" ] || [ "$EXT" = "$FILE" ] && continue
|
||||
|
||||
# Check for formatter
|
||||
FORMATTER="$FORMATTERS_DIR/$EXT"
|
||||
[ -x "$FORMATTER" ] || continue
|
||||
|
||||
# Run formatter
|
||||
ERRORS=$("$FORMATTER" "$ABS_FILE" 2>&1)
|
||||
EXIT_CODE=$?
|
||||
|
||||
# Re-stage the file (formatter may have modified it)
|
||||
git add "$ABS_FILE"
|
||||
|
||||
if [ $EXIT_CODE -ne 0 ]; then
|
||||
HAD_ERRORS=1
|
||||
ERROR_OUTPUT="${ERROR_OUTPUT}${FILE}:\n${ERRORS}\n\n"
|
||||
fi
|
||||
done < <(git diff --cached --name-only --diff-filter=AM)
|
||||
|
||||
if [ $HAD_ERRORS -ne 0 ]; then
|
||||
echo "Pre-commit lint errors — commit blocked:" >&2
|
||||
echo -e "$ERROR_OUTPUT" >&2
|
||||
echo "Fix the errors above and try again." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
22
memory/log/2026-03-13.100758.md
Normal file
22
memory/log/2026-03-13.100758.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Session Log — 2026-03-13
|
||||
|
||||
## Summary
|
||||
Implemented the composable multi-language linting and formatting system for Claude Code. Created formatter scripts, PostToolUse/pre-commit hooks, install scripts, `/linter` skill, best-practices documentation, and a README with project opt-in instructions.
|
||||
|
||||
## Decisions
|
||||
- Decision: Formatter scripts exit 1 on lint errors, dispatcher hook decides exit code (exit 2 for PostToolUse feedback) — Rationale: separates formatter logic from hook semantics; same scripts work for both PostToolUse and pre-commit
|
||||
- Decision: Checkpoint uses `git hash-object -w` with `.pre-lint` sidecar file — Rationale: fast (~1ms), no commits/stash, orphan blobs auto-GC'd; falls back to `cp` outside git repos
|
||||
- Decision: Projects opt in via symlinks in `formatters/` rather than config — Rationale: zero-config, visible in `ls`, no parsing needed; hook walks up directory tree to find `formatters/`
|
||||
- Decision: Symlinked README.md into `~/dev/claude/` for visibility — Rationale: makes setup guide discoverable from the top-level working directory
|
||||
|
||||
## Key Context
|
||||
- claude-foundations lives at `~/dev/claude/projects/claude-foundations/` (not `~/dev/claude/claude-foundations/`)
|
||||
- PostToolUse exit code 2 feeds stderr back to Claude as feedback without blocking the edit
|
||||
- Agent-type hooks are read-only (no Edit/Write) — lint fixing must happen via Agent tool subagent, not hooks
|
||||
- All hooks in an array run in parallel, not sequentially
|
||||
- Formatter contract: `$1` = absolute path, format in place, stderr for errors, exit 0/1, no `set -e`, silent on missing tools
|
||||
|
||||
## Process Notes
|
||||
- Plan was thorough and complete — implementation was straightforward with minimal deviation
|
||||
- Smoke tests confirmed all four paths: clean file, missing tools, no formatters dir, no extension
|
||||
- No actual lint errors were triggered during testing since test files were already clean
|
||||
46
scripts/install-hooks.sh
Executable file
46
scripts/install-hooks.sh
Executable file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# Symlink all hooks from claude-foundations/hooks/ into ~/.claude/hooks/
|
||||
# and print the settings.json configuration to add.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
HOOKS_SRC="$(cd "$SCRIPT_DIR/../hooks" && pwd)"
|
||||
HOOKS_DST="$HOME/.claude/hooks"
|
||||
|
||||
mkdir -p "$HOOKS_DST"
|
||||
|
||||
echo "Installing hooks from $HOOKS_SRC → $HOOKS_DST"
|
||||
echo ""
|
||||
|
||||
for HOOK in "$HOOKS_SRC"/*.sh; do
|
||||
NAME="$(basename "$HOOK")"
|
||||
ln -sf "$HOOK" "$HOOKS_DST/$NAME"
|
||||
echo " ✓ $NAME"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Hooks symlinked. Ensure your ~/.claude/settings.json includes:"
|
||||
echo ""
|
||||
cat <<'EOF'
|
||||
{
|
||||
"hooks": {
|
||||
"PreCompact": [
|
||||
{
|
||||
"matcher": "auto",
|
||||
"hooks": [{ "type": "command", "command": "~/.claude/hooks/pre-compact-backup.sh", "timeout": 15, "statusMessage": "Backing up transcript before compaction..." }]
|
||||
},
|
||||
{
|
||||
"matcher": "manual",
|
||||
"hooks": [{ "type": "command", "command": "~/.claude/hooks/pre-compact-backup.sh", "timeout": 15, "statusMessage": "Backing up transcript before compaction..." }]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write|MultiEdit",
|
||||
"hooks": [{ "type": "command", "command": "~/.claude/hooks/post-edit-lint.sh", "timeout": 30, "statusMessage": "Formatting and linting..." }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
EOF
|
||||
74
scripts/setup-formatters.sh
Executable file
74
scripts/setup-formatters.sh
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
# Set up formatters for a project by symlinking selected formatters from claude-foundations.
|
||||
#
|
||||
# Usage: setup-formatters.sh <project-dir> <ext> [<ext> ...]
|
||||
# Example: setup-formatters.sh ~/dev/claude/projects/my-project py sh
|
||||
#
|
||||
# This creates a formatters/ directory in the target project and symlinks
|
||||
# the requested formatter scripts from claude-foundations/formatters/.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
FOUNDATIONS_FORMATTERS="$(cd "$SCRIPT_DIR/../formatters" && pwd)"
|
||||
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Usage: $(basename "$0") <project-dir> <ext> [<ext> ...]"
|
||||
echo ""
|
||||
echo "Available formatters:"
|
||||
for f in "$FOUNDATIONS_FORMATTERS"/*; do
|
||||
[ -e "$f" ] || continue
|
||||
NAME="$(basename "$f")"
|
||||
if [ -L "$f" ]; then
|
||||
TARGET="$(readlink "$f")"
|
||||
echo " $NAME → $TARGET"
|
||||
else
|
||||
echo " $NAME"
|
||||
fi
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PROJECT_DIR="$1"
|
||||
shift
|
||||
|
||||
if [ ! -d "$PROJECT_DIR" ]; then
|
||||
echo "Error: $PROJECT_DIR is not a directory" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DEST="$PROJECT_DIR/formatters"
|
||||
mkdir -p "$DEST"
|
||||
|
||||
# Compute relative path from project formatters/ to foundations formatters/
|
||||
REL_PATH="$(python3 -c "import os.path; print(os.path.relpath('$FOUNDATIONS_FORMATTERS', '$DEST'))")"
|
||||
|
||||
echo "Setting up formatters in $DEST"
|
||||
echo ""
|
||||
|
||||
for EXT in "$@"; do
|
||||
SRC="$FOUNDATIONS_FORMATTERS/$EXT"
|
||||
if [ ! -e "$SRC" ]; then
|
||||
echo " ✗ $EXT — not found in $FOUNDATIONS_FORMATTERS" >&2
|
||||
continue
|
||||
fi
|
||||
ln -sf "$REL_PATH/$EXT" "$DEST/$EXT"
|
||||
echo " ✓ $EXT"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Done. Add formatters/ to your .gitignore or commit the symlinks."
|
||||
|
||||
# Also set up pre-commit hook if git repo
|
||||
if git -C "$PROJECT_DIR" rev-parse --git-dir >/dev/null 2>&1; then
|
||||
GIT_DIR="$(git -C "$PROJECT_DIR" rev-parse --git-dir)"
|
||||
PRECOMMIT="$GIT_DIR/hooks/pre-commit"
|
||||
PRECOMMIT_SRC="$SCRIPT_DIR/../hooks/pre-commit-lint.sh"
|
||||
|
||||
if [ ! -e "$PRECOMMIT" ]; then
|
||||
ln -sf "$(cd "$(dirname "$PRECOMMIT_SRC")" && pwd)/$(basename "$PRECOMMIT_SRC")" "$PRECOMMIT"
|
||||
echo "Git pre-commit hook installed: $PRECOMMIT → pre-commit-lint.sh"
|
||||
else
|
||||
echo "Git pre-commit hook already exists at $PRECOMMIT (not overwritten)."
|
||||
fi
|
||||
fi
|
||||
Reference in New Issue
Block a user