Skip to content

Instantly share code, notes, and snippets.

@lira
Forked from pedronauck/golint.sh
Created August 29, 2025 23:53
Show Gist options
  • Save lira/105b5ee38f934ea8e608ea0bb6dcbde0 to your computer and use it in GitHub Desktop.
Save lira/105b5ee38f934ea8e608ea0bb6dcbde0 to your computer and use it in GitHub Desktop.
Go and Typescript hooks for lint and check on Claude Code
#!/bin/bash
# Set up logging
LOG_FILE="$HOME/.claude/hooks/golangci-lint.log"
mkdir -p "$(dirname "$LOG_FILE")"
# Function to log with timestamp
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
# Start logging
log "=== Hook execution started (JSON mode) ==="
# Read JSON input from stdin
input=$(cat)
# Log the raw input
log "Raw input received: $input"
# Extract file path from the JSON input
file_path=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.target_file // empty')
log "Extracted file_path: '$file_path'"
# Check if file path is not empty and is a Go file
if [[ -n "$file_path" && "$file_path" != "null" && "$file_path" != "empty" ]]; then
log "File path is not empty: $file_path"
# Check if it's a Go file
if [[ "$file_path" == *.go ]]; then
log "File is a Go file: $file_path"
# Check if the file actually exists
if [[ -f "$file_path" ]]; then
log "Running golangci-lint on $file_path..."
# Get the directory containing the Go file
dir=$(dirname "$file_path")
log "Changing to directory: $dir"
# Check if golangci-lint is available
if command -v golangci-lint &> /dev/null; then
log "golangci-lint command found"
# Change to the directory and run golangci-lint
cd "$dir" && {
log "Running: golangci-lint run --fix --allow-parallel-runners"
# Capture golangci-lint output and exit code
lint_output=$(golangci-lint run --fix --allow-parallel-runners 2>&1)
exit_code=$?
# Log the output
if [[ -n "$lint_output" ]]; then
log "golangci-lint output: $lint_output"
fi
log "golangci-lint exit code: $exit_code"
if [[ $exit_code -eq 0 ]]; then
log "golangci-lint completed successfully - no issues found"
# Return success JSON (no blocking)
decision='{"suppressOutput": false}'
log "Hook decision: $decision"
echo "$decision"
else
# Lint errors found - block the operation with JSON response
log "golangci-lint found issues - blocking operation"
# Create detailed reason message
reason="golangci-lint found issues in $file_path"
if [[ -n "$lint_output" ]]; then
reason="$reason:\n\n$lint_output\n\nPlease fix these issues before proceeding."
else
reason="$reason. Please fix these issues before proceeding."
fi
# Return blocking JSON response
decision=$(jq -n --arg reason "$reason" '{
"decision": "block",
"reason": $reason
}')
log "Hook decision: $decision"
echo "$decision"
fi
}
else
log "golangci-lint command not found"
decision=$(jq -n '{
"decision": "block",
"reason": "golangci-lint command not found in PATH. Please install golangci-lint."
}')
log "Hook decision: $decision"
echo "$decision"
fi
else
log "File does not exist: $file_path"
decision='{}'
log "Hook decision: $decision"
echo "$decision"
fi
else
log "Not a Go file, skipping: $file_path"
decision='{}'
log "Hook decision: $decision"
echo "$decision"
fi
else
log "File path is empty or null, skipping"
decision='{}'
log "Hook decision: $decision"
echo "$decision"
fi
log "=== Hook execution completed (JSON mode) ==="
log ""
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "/Users/<NOME_DO_USUARIO>/.claude/hooks/golangci-lint.sh"
}
]
},
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "/Users/<NOME_DO_USUARIO>/.claude/hooks/typescript-check.sh"
}
]
}
]
}
}
#!/bin/bash
# Set up logging
LOG_FILE="$HOME/.claude/hooks/typescript-check.log"
mkdir -p "$(dirname "$LOG_FILE")"
# Function to log with timestamp
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
# Start logging
log "=== Hook execution started (JSON mode) ==="
# Read JSON input from stdin
input=$(cat)
# Log the raw input
log "Raw input received: $input"
# Extract file path from the JSON input
file_path=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.target_file // empty')
log "Extracted file_path: '$file_path'"
# Check if file path is not empty and is a TypeScript file
if [[ -n "$file_path" && "$file_path" != "null" && "$file_path" != "empty" ]]; then
log "File path is not empty: $file_path"
# Check if it's a TypeScript file
if [[ "$file_path" == *.ts || "$file_path" == *.tsx ]]; then
log "File is a TypeScript file: $file_path"
# Check if the file actually exists
if [[ -f "$file_path" ]]; then
log "Running TypeScript check on $file_path..."
# Get the directory containing the TypeScript file
dir=$(dirname "$file_path")
log "Changing to directory: $dir"
# Look for tsconfig.json in current directory or parent directories
tsconfig_dir="$dir"
while [[ "$tsconfig_dir" != "/" ]]; do
if [[ -f "$tsconfig_dir/tsconfig.json" ]]; then
log "Found tsconfig.json in: $tsconfig_dir"
break
fi
tsconfig_dir=$(dirname "$tsconfig_dir")
done
# If no tsconfig.json found, use the file's directory
if [[ ! -f "$tsconfig_dir/tsconfig.json" ]]; then
log "No tsconfig.json found, using file directory: $dir"
tsconfig_dir="$dir"
fi
# Function to find TypeScript compiler
find_tsc() {
# Try common locations for tsc
local tsc_paths=(
"$(command -v tsc 2>/dev/null)"
"$(which tsc 2>/dev/null)"
"$tsconfig_dir/node_modules/.bin/tsc"
"$(pwd)/node_modules/.bin/tsc"
"$HOME/.local/state/fnm_multishells/*/bin/tsc"
"$HOME/.nvm/versions/node/*/bin/tsc"
"$HOME/.volta/bin/tsc"
"/usr/local/bin/tsc"
"/opt/homebrew/bin/tsc"
)
for path in "${tsc_paths[@]}"; do
# Handle glob patterns
if [[ "$path" == *"*"* ]]; then
for expanded_path in $path; do
if [[ -x "$expanded_path" ]]; then
echo "$expanded_path"
return 0
fi
done
elif [[ -x "$path" && -n "$path" ]]; then
echo "$path"
return 0
fi
done
return 1
}
# Try to find TypeScript compiler
tsc_cmd=$(find_tsc)
if [[ -n "$tsc_cmd" ]]; then
log "Found tsc at: $tsc_cmd"
# Change to the directory with tsconfig.json and run TypeScript check
cd "$tsconfig_dir" && {
log "Running: $tsc_cmd --noEmit"
# Capture tsc output and exit code
tsc_output=$("$tsc_cmd" --noEmit 2>&1)
exit_code=$?
# Log the output
if [[ -n "$tsc_output" ]]; then
log "tsc output: $tsc_output"
fi
log "tsc exit code: $exit_code"
if [[ $exit_code -eq 0 ]]; then
log "TypeScript check completed successfully - no type errors found"
# Return success JSON (no blocking)
decision='{"suppressOutput": false}'
log "Hook decision: $decision"
echo "$decision"
else
# Type errors found - block the operation with JSON response
log "TypeScript check found type errors - blocking operation"
# Create detailed reason message
reason="TypeScript compiler found type errors in $file_path"
if [[ -n "$tsc_output" ]]; then
reason="$reason:\n\n$tsc_output\n\nPlease fix these type errors before proceeding."
else
reason="$reason. Please fix these type errors before proceeding."
fi
# Return blocking JSON response
decision=$(jq -n --arg reason "$reason" '{
"decision": "block",
"reason": $reason
}')
log "Hook decision: $decision"
echo "$decision"
fi
}
else
log "TypeScript compiler not found in any expected location"
log "WARNING: TypeScript check skipped - proceeding without type validation"
# Instead of blocking, warn and allow the operation to proceed
decision='{"suppressOutput": false, "warning": "TypeScript compiler not found. Install TypeScript globally or locally to enable type checking."}'
log "Hook decision: $decision"
echo "$decision"
fi
else
log "File does not exist: $file_path"
decision='{}'
log "Hook decision: $decision"
echo "$decision"
fi
else
log "Not a TypeScript file, skipping: $file_path"
decision='{}'
log "Hook decision: $decision"
echo "$decision"
fi
else
log "File path is empty or null, skipping"
decision='{}'
log "Hook decision: $decision"
echo "$decision"
fi
log "=== Hook execution completed (JSON mode) ==="
log ""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment