Last active
July 11, 2025 20:44
-
-
Save ariel-frischer/aceac390fa5cbcb87c482efc59a8efe1 to your computer and use it in GitHub Desktop.
General Claude Code Lint+Format Hook
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# Claude Code PostToolUse Hook for Auto-formatting and Linting | |
# | |
# This script integrates with Claude Code's hook system to automatically | |
# format and lint files after they're edited or created. | |
# | |
# How it works: | |
# 1. Claude Code calls this script after Write/Edit/MultiEdit|Update operations | |
# 2. The script receives JSON input via stdin containing: | |
# - session_id: Current Claude session | |
# - transcript_path: Path to conversation JSON | |
# - hook_event_name: "PostToolUse" | |
# - tool_name: The tool that was used (Write, Edit, etc.) | |
# - tool_input: Contains file_path and other tool-specific data | |
# - tool_response: The result of the tool operation | |
# | |
# 3. The script extracts the file path from the JSON using jq | |
# 4. Based on file extension, it runs appropriate linters/formatters | |
# 5. Exit codes determine Claude's behavior: | |
# - Exit 0: Success (silent, no output shown to user) | |
# - Exit 2: Blocking error (stderr shown to Claude for processing) | |
# - Other: Non-blocking error (stderr shown to user) | |
# | |
# Supported file types: | |
# - Python: ruff (import sorting, linting, formatting) + mypy (type checking) | |
# - JavaScript/TypeScript: eslint + prettier | |
# - Shell scripts: shellcheck + shfmt | |
# - JSON: jq validation and formatting | |
# - YAML: yamllint | |
# - TOML: taplo | |
# - Terraform: terraform fmt + validate | |
# - Dockerfile: hadolint | |
# | |
# The script attempts to auto-fix issues where possible. If fixes can't | |
# be applied automatically (e.g., syntax errors), it reports the error | |
# back to Claude via stderr with exit code 2. | |
set -e | |
# Read JSON from stdin | |
json_input=$(cat) | |
# Extract file path from JSON based on tool type | |
# For Write/Edit/MultiEdit tools, the file path is in tool_input.file_path | |
file_path=$(echo "$json_input" | jq -r '.tool_input.file_path // empty') | |
# If no file path found, exit silently | |
if [ -z "$file_path" ]; then | |
exit 0 | |
fi | |
# Check if file exists | |
if [ ! -f "$file_path" ]; then | |
exit 0 | |
fi | |
handle_python() { | |
local file="$1" | |
# Try to fix imports first | |
if ! ruff check --select I --fix "$file" 2>&1; then | |
echo "Failed to fix imports in $file" >&2 | |
exit 2 | |
fi | |
# Then try to fix other issues | |
if ! ruff check --fix "$file" 2>&1; then | |
echo "Ruff check failed for $file" >&2 | |
exit 2 | |
fi | |
# Format the file | |
if ! ruff format "$file" 2>&1; then | |
echo "Ruff format failed for $file" >&2 | |
exit 2 | |
fi | |
# Type check - this might fail for syntax errors | |
if ! mypy "$file" 2>&1; then | |
echo "MyPy type checking failed for $file" >&2 | |
exit 2 | |
fi | |
} | |
handle_javascript() { | |
local file="$1" | |
if command -v eslint >/dev/null 2>&1; then | |
if ! eslint --fix "$file" 2>&1; then | |
echo "ESLint failed for $file" >&2 | |
exit 2 | |
fi | |
fi | |
if command -v prettier >/dev/null 2>&1; then | |
if ! prettier --write "$file" 2>&1; then | |
echo "Prettier failed for $file" >&2 | |
exit 2 | |
fi | |
fi | |
} | |
handle_typescript() { | |
local file="$1" | |
if command -v eslint >/dev/null 2>&1; then | |
if ! eslint --fix "$file" 2>&1; then | |
echo "ESLint failed for $file" >&2 | |
exit 2 | |
fi | |
fi | |
if command -v prettier >/dev/null 2>&1; then | |
if ! prettier --write "$file" 2>&1; then | |
echo "Prettier failed for $file" >&2 | |
exit 2 | |
fi | |
fi | |
if command -v tsc >/dev/null 2>&1; then | |
if ! tsc --noEmit "$file" 2>&1; then | |
echo "TypeScript compilation failed for $file" >&2 | |
exit 2 | |
fi | |
fi | |
} | |
handle_shell() { | |
local file="$1" | |
if command -v shellcheck >/dev/null 2>&1; then | |
if ! shellcheck "$file" 2>&1; then | |
echo "ShellCheck failed for $file" >&2 | |
exit 2 | |
fi | |
fi | |
if command -v shfmt >/dev/null 2>&1; then | |
if ! shfmt -w "$file" 2>&1; then | |
echo "shfmt failed for $file" >&2 | |
exit 2 | |
fi | |
fi | |
} | |
handle_json() { | |
local file="$1" | |
if command -v jq >/dev/null 2>&1; then | |
tmp=$(mktemp) | |
if ! jq . "$file" >"$tmp" 2>&1; then | |
echo "JSON validation failed for $file" >&2 | |
rm -f "$tmp" | |
exit 2 | |
fi | |
mv "$tmp" "$file" | |
fi | |
} | |
handle_yaml() { | |
local file="$1" | |
if command -v yamllint >/dev/null 2>&1; then | |
if ! yamllint "$file" 2>&1; then | |
echo "YAML validation failed for $file" >&2 | |
exit 2 | |
fi | |
fi | |
} | |
handle_toml() { | |
local file="$1" | |
if command -v taplo >/dev/null 2>&1; then | |
if ! taplo fmt "$file" 2>&1; then | |
echo "TOML formatting failed for $file" >&2 | |
exit 2 | |
fi | |
fi | |
} | |
handle_tf() { | |
local file="$1" | |
if command -v terraform >/dev/null 2>&1; then | |
if ! terraform fmt "$file" 2>&1; then | |
echo "Terraform fmt failed for $file" >&2 | |
exit 2 | |
fi | |
if ! terraform validate "$file" 2>&1; then | |
echo "Terraform validation failed for $file" >&2 | |
exit 2 | |
fi | |
fi | |
} | |
handle_dockerfile() { | |
local file="$1" | |
if command -v hadolint >/dev/null 2>&1; then | |
if ! hadolint "$file" 2>&1; then | |
echo "Hadolint failed for $file" >&2 | |
exit 2 | |
fi | |
fi | |
} | |
# Process the file based on its extension | |
case "$file_path" in | |
*.py | *.pyi | *.pyx | *.pxd) | |
handle_python "$file_path" | |
;; | |
*.js | *.cjs | *.mjs) | |
handle_javascript "$file_path" | |
;; | |
*.ts | *.tsx) | |
handle_typescript "$file_path" | |
;; | |
*.sh) | |
handle_shell "$file_path" | |
;; | |
*.json) | |
handle_json "$file_path" | |
;; | |
*.yaml | *.yml) | |
handle_yaml "$file_path" | |
;; | |
*.toml) | |
handle_toml "$file_path" | |
;; | |
*.tf | *.tfvars | *.hcl) | |
handle_tf "$file_path" | |
;; | |
*dockerfile | *Dockerfile) | |
handle_dockerfile "$file_path" | |
;; | |
*) | |
# Silently skip files we don't handle | |
;; | |
esac | |
# Exit with success if we reach here | |
exit 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add to .claude/settings.json: