#!/bin/bash # PostToolUse:Edit Hook - Suggest lint/format/test commands after file edits # Detects file type from edited path and suggests appropriate commands # # Input: JSON with tool_input.file_path containing the edited file # Output: JSON with systemMessage containing suggested commands set -euo pipefail # Read input from stdin input=$(cat) # Extract the edited file path from tool_input file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty') # If no file path, silently succeed if [[ -z "$file_path" ]]; then echo '{"continue": true}' exit 0 fi # Get the file extension (lowercase) extension="${file_path##*.}" extension=$(echo "$extension" | tr '[:upper:]' '[:lower:]') # Detect project type and available tools PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}" HAS_CORE_CLI="false" if command -v core &>/dev/null; then HAS_CORE_CLI="true" fi # Build suggestions based on file type SUGGESTIONS="" QUICK_CMD="" case "$extension" in go) if [[ "$HAS_CORE_CLI" == "true" ]]; then QUICK_CMD="core go qa quick" SUGGESTIONS="| Task | Command |\\n" SUGGESTIONS+="|------|---------|\\n" SUGGESTIONS+="| Quick check | \`core go qa quick\` |\\n" SUGGESTIONS+="| Fix all issues | \`core go qa --fix\` |\\n" SUGGESTIONS+="| Run tests | \`core go qa --only=test\` |\\n" SUGGESTIONS+="| Full QA with coverage | \`core go qa --coverage\` |\\n" else QUICK_CMD="go fmt \"$file_path\" && go vet \"$file_path\"" SUGGESTIONS="| Task | Command |\\n" SUGGESTIONS+="|------|---------|\\n" SUGGESTIONS+="| Format file | \`go fmt \"$file_path\"\` |\\n" SUGGESTIONS+="| Vet file | \`go vet \"$file_path\"\` |\\n" SUGGESTIONS+="| Run tests | \`go test ./...\` |\\n" SUGGESTIONS+="| All checks | \`go fmt ./... && go vet ./... && go test ./...\` |\\n" fi ;; ts|tsx) # Check for package.json in project if [[ -f "$PROJECT_DIR/package.json" ]]; then # Check what scripts are available HAS_LINT=$(jq -r '.scripts.lint // empty' "$PROJECT_DIR/package.json" 2>/dev/null || echo "") HAS_TEST=$(jq -r '.scripts.test // empty' "$PROJECT_DIR/package.json" 2>/dev/null || echo "") HAS_FORMAT=$(jq -r '.scripts.format // empty' "$PROJECT_DIR/package.json" 2>/dev/null || echo "") SUGGESTIONS="| Task | Command |\\n" SUGGESTIONS+="|------|---------|\\n" if [[ -n "$HAS_LINT" ]]; then QUICK_CMD="npm run lint" SUGGESTIONS+="| Lint | \`npm run lint\` |\\n" fi if [[ -n "$HAS_FORMAT" ]]; then SUGGESTIONS+="| Format | \`npm run format\` |\\n" fi if [[ -n "$HAS_TEST" ]]; then SUGGESTIONS+="| Test | \`npm test\` |\\n" fi # TypeScript specific SUGGESTIONS+="| Type check | \`npx tsc --noEmit\` |\\n" else SUGGESTIONS="| Task | Command |\\n" SUGGESTIONS+="|------|---------|\\n" SUGGESTIONS+="| Type check | \`npx tsc --noEmit\` |\\n" QUICK_CMD="npx tsc --noEmit" fi ;; js|jsx|mjs|cjs) # Check for package.json in project if [[ -f "$PROJECT_DIR/package.json" ]]; then HAS_LINT=$(jq -r '.scripts.lint // empty' "$PROJECT_DIR/package.json" 2>/dev/null || echo "") HAS_TEST=$(jq -r '.scripts.test // empty' "$PROJECT_DIR/package.json" 2>/dev/null || echo "") HAS_FORMAT=$(jq -r '.scripts.format // empty' "$PROJECT_DIR/package.json" 2>/dev/null || echo "") SUGGESTIONS="| Task | Command |\\n" SUGGESTIONS+="|------|---------|\\n" if [[ -n "$HAS_LINT" ]]; then QUICK_CMD="npm run lint" SUGGESTIONS+="| Lint | \`npm run lint\` |\\n" fi if [[ -n "$HAS_FORMAT" ]]; then SUGGESTIONS+="| Format | \`npm run format\` |\\n" fi if [[ -n "$HAS_TEST" ]]; then SUGGESTIONS+="| Test | \`npm test\` |\\n" fi fi ;; py) SUGGESTIONS="| Task | Command |\\n" SUGGESTIONS+="|------|---------|\\n" # Check for common Python tools if command -v ruff &>/dev/null; then QUICK_CMD="ruff check \"$file_path\"" SUGGESTIONS+="| Lint | \`ruff check \"$file_path\"\` |\\n" SUGGESTIONS+="| Fix issues | \`ruff check --fix \"$file_path\"\` |\\n" SUGGESTIONS+="| Format | \`ruff format \"$file_path\"\` |\\n" elif command -v flake8 &>/dev/null; then QUICK_CMD="flake8 \"$file_path\"" SUGGESTIONS+="| Lint | \`flake8 \"$file_path\"\` |\\n" elif command -v pylint &>/dev/null; then QUICK_CMD="pylint \"$file_path\"" SUGGESTIONS+="| Lint | \`pylint \"$file_path\"\` |\\n" fi # Check for pytest if command -v pytest &>/dev/null; then SUGGESTIONS+="| Test | \`pytest\` |\\n" elif [[ -f "$PROJECT_DIR/setup.py" ]] || [[ -f "$PROJECT_DIR/pyproject.toml" ]]; then SUGGESTIONS+="| Test | \`python -m pytest\` |\\n" fi # Type checking if command -v mypy &>/dev/null; then SUGGESTIONS+="| Type check | \`mypy \"$file_path\"\` |\\n" fi ;; php) if [[ "$HAS_CORE_CLI" == "true" ]]; then QUICK_CMD="core php qa --quick" SUGGESTIONS="| Task | Command |\\n" SUGGESTIONS+="|------|---------|\\n" SUGGESTIONS+="| Quick check | \`core php qa --quick\` |\\n" SUGGESTIONS+="| Fix all issues | \`core php qa --fix\` |\\n" SUGGESTIONS+="| Run tests | \`core php test\` |\\n" SUGGESTIONS+="| Static analysis | \`core php stan\` |\\n" elif [[ -f "$PROJECT_DIR/composer.json" ]]; then SUGGESTIONS="| Task | Command |\\n" SUGGESTIONS+="|------|---------|\\n" # Check for Laravel Pint if [[ -f "$PROJECT_DIR/vendor/bin/pint" ]]; then QUICK_CMD="./vendor/bin/pint \"$file_path\"" SUGGESTIONS+="| Format | \`./vendor/bin/pint \"$file_path\"\` |\\n" fi # Check for PHPStan if [[ -f "$PROJECT_DIR/vendor/bin/phpstan" ]]; then SUGGESTIONS+="| Static analysis | \`./vendor/bin/phpstan analyse \"$file_path\"\` |\\n" fi # Check for PHPUnit or Pest if [[ -f "$PROJECT_DIR/vendor/bin/phpunit" ]]; then SUGGESTIONS+="| Test | \`./vendor/bin/phpunit\` |\\n" elif [[ -f "$PROJECT_DIR/vendor/bin/pest" ]]; then SUGGESTIONS+="| Test | \`./vendor/bin/pest\` |\\n" fi # Check for composer scripts HAS_LINT=$(jq -r '.scripts.lint // empty' "$PROJECT_DIR/composer.json" 2>/dev/null || echo "") HAS_TEST=$(jq -r '.scripts.test // empty' "$PROJECT_DIR/composer.json" 2>/dev/null || echo "") if [[ -n "$HAS_LINT" ]]; then SUGGESTIONS+="| Lint (composer) | \`composer lint\` |\\n" fi if [[ -n "$HAS_TEST" ]]; then SUGGESTIONS+="| Test (composer) | \`composer test\` |\\n" fi fi ;; sh|bash) SUGGESTIONS="| Task | Command |\\n" SUGGESTIONS+="|------|---------|\\n" if command -v shellcheck &>/dev/null; then QUICK_CMD="shellcheck \"$file_path\"" SUGGESTIONS+="| Check script | \`shellcheck \"$file_path\"\` |\\n" SUGGESTIONS+="| Check (verbose) | \`shellcheck -x \"$file_path\"\` |\\n" else SUGGESTIONS+="| Syntax check | \`bash -n \"$file_path\"\` |\\n" QUICK_CMD="bash -n \"$file_path\"" fi ;; json) SUGGESTIONS="| Task | Command |\\n" SUGGESTIONS+="|------|---------|\\n" if command -v jq &>/dev/null; then QUICK_CMD="jq . \"$file_path\" > /dev/null" SUGGESTIONS+="| Validate JSON | \`jq . \"$file_path\" > /dev/null\` |\\n" SUGGESTIONS+="| Pretty print | \`jq . \"$file_path\"\` |\\n" fi # Check if it's package.json if [[ "$(basename "$file_path")" == "package.json" ]]; then SUGGESTIONS+="| Install deps | \`npm install\` |\\n" fi ;; yaml|yml) SUGGESTIONS="| Task | Command |\\n" SUGGESTIONS+="|------|---------|\\n" if command -v yamllint &>/dev/null; then QUICK_CMD="yamllint \"$file_path\"" SUGGESTIONS+="| Validate YAML | \`yamllint \"$file_path\"\` |\\n" elif command -v yq &>/dev/null; then QUICK_CMD="yq . \"$file_path\" > /dev/null" SUGGESTIONS+="| Validate YAML | \`yq . \"$file_path\" > /dev/null\` |\\n" fi ;; md|markdown) SUGGESTIONS="| Task | Command |\\n" SUGGESTIONS+="|------|---------|\\n" if command -v markdownlint &>/dev/null; then QUICK_CMD="markdownlint \"$file_path\"" SUGGESTIONS+="| Lint markdown | \`markdownlint \"$file_path\"\` |\\n" fi ;; rs) SUGGESTIONS="| Task | Command |\\n" SUGGESTIONS+="|------|---------|\\n" QUICK_CMD="cargo fmt -- --check" SUGGESTIONS+="| Format check | \`cargo fmt -- --check\` |\\n" SUGGESTIONS+="| Format | \`cargo fmt\` |\\n" SUGGESTIONS+="| Lint | \`cargo clippy\` |\\n" SUGGESTIONS+="| Test | \`cargo test\` |\\n" SUGGESTIONS+="| Check | \`cargo check\` |\\n" ;; *) # Unknown file type - no suggestions echo '{"continue": true}' exit 0 ;; esac # If no suggestions were built, exit silently if [[ -z "$SUGGESTIONS" ]]; then echo '{"continue": true}' exit 0 fi # Build the message MSG="**Post-Edit Suggestions** for \`$(basename "$file_path")\`\\n\\n" MSG+="$SUGGESTIONS" # Add recommended quick command if available if [[ -n "$QUICK_CMD" ]]; then MSG+="\\n**Recommended:** \`$QUICK_CMD\`" fi # Escape for JSON ESCAPED_MSG=$(echo -e "$MSG" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | tr '\n' ' ' | sed 's/ */ /g') # Output JSON response with additionalContext for Claude cat << EOF { "continue": true, "hookSpecificOutput": { "hookEventName": "PostToolUse", "additionalContext": "$ESCAPED_MSG" } } EOF