mirror of
https://github.com/ivuorinen/dotfiles.git
synced 2026-03-20 16:07:03 +00:00
refactor(claude): migrate hooks to external scripts and add new hooks
Replace inline command strings in settings.json with external scripts in .claude/hooks/ for readability and maintainability. Consolidate three PostToolUse formatters into one script and add markdown/yaml formatting. Add new hooks: SessionStart context banner, Stop lint gate, async Bats test runner, idle desktop notification, and PostToolUseFailure logger.
This commit is contained in:
13
.claude/hooks/async-bats.sh
Executable file
13
.claude/hooks/async-bats.sh
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Async Bats runner: run matching test file when a script is edited.
|
||||||
|
# Runs in background (async: true) — output appears on next turn.
|
||||||
|
|
||||||
|
fp=$(jq -r '.tool_input.file_path // empty')
|
||||||
|
[ -z "$fp" ] && exit 0
|
||||||
|
|
||||||
|
name=$(basename "$fp")
|
||||||
|
test_file="$CLAUDE_PROJECT_DIR/tests/${name}.bats"
|
||||||
|
[ ! -f "$test_file" ] && exit 0
|
||||||
|
|
||||||
|
echo "Running $test_file ..."
|
||||||
|
"$CLAUDE_PROJECT_DIR/node_modules/.bin/bats" "$test_file"
|
||||||
14
.claude/hooks/log-failures.sh
Executable file
14
.claude/hooks/log-failures.sh
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# PostToolUseFailure logger: append tool failures to a local log file.
|
||||||
|
|
||||||
|
log_file="$CLAUDE_PROJECT_DIR/.claude/hook-failures.log"
|
||||||
|
|
||||||
|
entry=$(jq -c '{
|
||||||
|
time: (now | strftime("%Y-%m-%dT%H:%M:%SZ")),
|
||||||
|
tool: .tool_name,
|
||||||
|
error: .error
|
||||||
|
}')
|
||||||
|
|
||||||
|
echo "$entry" >> "$log_file"
|
||||||
|
|
||||||
|
exit 0
|
||||||
13
.claude/hooks/notify-idle.sh
Executable file
13
.claude/hooks/notify-idle.sh
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Notification hook: alert when Claude goes idle.
|
||||||
|
# Uses pushover if available, falls back to macOS native notification.
|
||||||
|
|
||||||
|
msg=$(jq -r '.message // "Claude is waiting for input"')
|
||||||
|
|
||||||
|
if command -v pushover > /dev/null; then
|
||||||
|
pushover "Claude Code" "$msg"
|
||||||
|
elif command -v osascript > /dev/null; then
|
||||||
|
osascript -e "display notification \"$msg\" with title \"Claude Code\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
30
.claude/hooks/post-edit-format.sh
Executable file
30
.claude/hooks/post-edit-format.sh
Executable file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Post-edit formatter: auto-format file based on extension.
|
||||||
|
# Receives tool output JSON on stdin.
|
||||||
|
|
||||||
|
fp=$(jq -r '.tool_input.file_path // empty')
|
||||||
|
[ -z "$fp" ] || [ ! -f "$fp" ] && exit 0
|
||||||
|
|
||||||
|
case "$fp" in
|
||||||
|
*.sh | */bin/*)
|
||||||
|
head -1 "$fp" | grep -qE '^#!.*(ba)?sh' \
|
||||||
|
&& command -v shfmt > /dev/null \
|
||||||
|
&& shfmt -i 2 -bn -ci -sr -fn -w "$fp"
|
||||||
|
;;
|
||||||
|
*.fish)
|
||||||
|
command -v fish_indent > /dev/null && fish_indent -w "$fp"
|
||||||
|
;;
|
||||||
|
*.lua)
|
||||||
|
command -v stylua > /dev/null && stylua "$fp"
|
||||||
|
;;
|
||||||
|
*.md)
|
||||||
|
command -v biome > /dev/null && biome format --write "$fp" 2> /dev/null
|
||||||
|
command -v markdown-table-formatter > /dev/null \
|
||||||
|
&& markdown-table-formatter "$fp" 2> /dev/null
|
||||||
|
;;
|
||||||
|
*.yml | *.yaml)
|
||||||
|
command -v prettier > /dev/null && prettier --write "$fp" 2> /dev/null
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
exit 0
|
||||||
23
.claude/hooks/pre-edit-block.sh
Executable file
23
.claude/hooks/pre-edit-block.sh
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Pre-edit guard: block vendor/lock files and secrets.d real fish files.
|
||||||
|
# Receives tool input JSON on stdin.
|
||||||
|
|
||||||
|
fp=$(jq -r '.tool_input.file_path // empty')
|
||||||
|
[ -z "$fp" ] && exit 0
|
||||||
|
|
||||||
|
case "$fp" in
|
||||||
|
*/fzf-tmux | */yarn.lock | */.yarn/*)
|
||||||
|
echo "BLOCKED: $fp is a vendor/lock file — do not edit directly" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
*/secrets.d/*.fish)
|
||||||
|
case "$(basename "$fp")" in
|
||||||
|
*.example.fish | *.fish.example) exit 0 ;;
|
||||||
|
esac
|
||||||
|
echo "BLOCKED: do not edit $fp directly — it is gitignored." >&2
|
||||||
|
echo "Copy the matching .fish.example file and edit that locally." >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
exit 0
|
||||||
15
.claude/hooks/session-start-context.sh
Executable file
15
.claude/hooks/session-start-context.sh
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# SessionStart context: print branch, dirty file count, and last commit.
|
||||||
|
|
||||||
|
cd "$CLAUDE_PROJECT_DIR" || exit 0
|
||||||
|
|
||||||
|
branch=$(git branch --show-current 2> /dev/null)
|
||||||
|
dirty=$(git status --short 2> /dev/null | wc -l | tr -d ' ')
|
||||||
|
last=$(git log -1 --oneline 2> /dev/null)
|
||||||
|
|
||||||
|
echo "=== Dotfiles session context ==="
|
||||||
|
echo "Branch : ${branch:-unknown}"
|
||||||
|
echo "Dirty : ${dirty} file(s)"
|
||||||
|
echo "Last : ${last}"
|
||||||
|
|
||||||
|
exit 0
|
||||||
16
.claude/hooks/stop-lint-gate.sh
Executable file
16
.claude/hooks/stop-lint-gate.sh
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Stop gate: run yarn lint before Claude finishes.
|
||||||
|
# Exit 2 sends feedback back and keeps Claude working.
|
||||||
|
|
||||||
|
cd "$CLAUDE_PROJECT_DIR" || exit 0
|
||||||
|
|
||||||
|
output=$(yarn lint 2>&1)
|
||||||
|
status=$?
|
||||||
|
|
||||||
|
if [ $status -ne 0 ]; then
|
||||||
|
echo "Lint failed — fix before finishing:" >&2
|
||||||
|
echo "$output" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -1,12 +1,23 @@
|
|||||||
{
|
{
|
||||||
"hooks": {
|
"hooks": {
|
||||||
|
"SessionStart": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start-context.sh",
|
||||||
|
"statusMessage": "Loading project context..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"PreToolUse": [
|
"PreToolUse": [
|
||||||
{
|
{
|
||||||
"matcher": "Edit|Write",
|
"matcher": "Edit|Write",
|
||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "fp=$(cat | jq -r '.tool_input.file_path // empty') && [ -n \"$fp\" ] && case \"$fp\" in */fzf-tmux|*/yarn.lock|*/.yarn/*) echo \"BLOCKED: $fp is a vendor/lock file — do not edit directly\" >&2; exit 2;; esac; exit 0"
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/pre-edit-block.sh"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -17,15 +28,48 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "fp=$(cat | jq -r '.tool_input.file_path // empty') && [ -n \"$fp\" ] && [ -f \"$fp\" ] && case \"$fp\" in *.sh|*/bin/*) head -1 \"$fp\" | grep -qE '^#!.*(ba)?sh' && command -v shfmt > /dev/null && shfmt -i 2 -bn -ci -sr -fn -w \"$fp\";; esac; exit 0"
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/post-edit-format.sh",
|
||||||
|
"statusMessage": "Formatting..."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "fp=$(cat | jq -r '.tool_input.file_path // empty') && [ -n \"$fp\" ] && [ -f \"$fp\" ] && case \"$fp\" in *.fish) command -v fish_indent > /dev/null && fish_indent -w \"$fp\";; esac; exit 0"
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/async-bats.sh",
|
||||||
},
|
"async": true,
|
||||||
|
"statusMessage": "Running tests..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PostToolUseFailure": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "fp=$(cat | jq -r '.tool_input.file_path // empty') && [ -n \"$fp\" ] && [ -f \"$fp\" ] && case \"$fp\" in *.lua) command -v stylua > /dev/null && stylua \"$fp\";; esac; exit 0"
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/log-failures.sh",
|
||||||
|
"async": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Notification": [
|
||||||
|
{
|
||||||
|
"matcher": "idle_prompt",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/notify-idle.sh",
|
||||||
|
"async": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Stop": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/stop-lint-gate.sh",
|
||||||
|
"statusMessage": "Running lint gate..."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user