Files
claude-foundations/hooks/post-edit-lint.sh
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

87 lines
2.6 KiB
Bash
Executable File

#!/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