From c640f2d48f4d733755fc64b6a54db0abc0a6bab8 Mon Sep 17 00:00:00 2001 From: Jakub Czajka Date: Tue, 28 Jul 2026 07:20:42 +0000 Subject: [PATCH] [hooks] pre-flight checks, surface failures, prettify Every hook now checks that GUIX_BIN and required commands exist before running. PreToolUse hooks deny with a visible reason on failure; other hooks emit systemMessage. All hooks follow the create-worktree style: copyright header, individual check blocks. Fixed block-worktrees to not silently exit when CLAUDE_PROJECT_DIR is unset (the hook derives proj_root from git, not that variable). Fixed git push exemption in create-worktree to match full paths like $HOME/.guix-profile/bin/git push. Co-Authored-By: Claude hooks: guix-build check for divergence from main Exit early only when the tree is clean AND at main tip. Warn and stop if the branch is behind main (needs rebase). Fall through to queue logic when there are staged commits or a dirty tree. Co-Authored-By: Claude --- .claude/hooks/block-builds | 78 ++++++++++---------- .claude/hooks/block-worktrees | 100 ++++++++++++++++--------- .claude/hooks/create-worktree | 46 ++++++++++-- .claude/hooks/guix-build | 134 ++++++++++++++++++++++++---------- .claude/hooks/guix-check | 28 +++++-- .claude/hooks/notify | 41 ++++++----- .claude/hooks/status-line | 26 +++++-- .claude/settings.json | 1 - 8 files changed, 304 insertions(+), 150 deletions(-) diff --git a/.claude/hooks/block-builds b/.claude/hooks/block-builds index ce3922d..1f6ec99 100755 --- a/.claude/hooks/block-builds +++ b/.claude/hooks/block-builds @@ -1,62 +1,66 @@ #!/bin/sh -# Copyright (c) 2026 Jakub Czajka -# License: GPL-3.0 or later. -# # block-builds - Guard guix/make build/reconfigure. +if [ -z "$GUIX_BIN" ]; then + echo '{"systemMessage":"❌ block-builds: GUIX_BIN not set"}' >&2 + exit 0 +fi +if [ ! -x "$GUIX_BIN/sed" ]; then + echo '{"hookSpecificOutput":{"permissionDecision":"deny",' \ + '"permissionDecisionReason":"block-builds: GUIX_BIN/sed missing",' \ + '"systemMessage":"❌ block-builds broken: GUIX_BIN/sed not found"}}' + exit 0 +fi +if [ ! -x "$GUIX_BIN/echo" ]; then + echo '{"hookSpecificOutput":{"permissionDecision":"deny",' \ + '"permissionDecisionReason":"block-builds: GUIX_BIN/echo missing",' \ + '"systemMessage":"❌ block-builds broken: GUIX_BIN/echo not found"}}' + exit 0 +fi +if [ ! -x "$GUIX_BIN/grep" ]; then + echo '{"hookSpecificOutput":{"permissionDecision":"deny",' \ + '"permissionDecisionReason":"block-builds: GUIX_BIN/grep missing",' \ + '"systemMessage":"❌ block-builds broken: GUIX_BIN/grep not found"}}' + exit 0 +fi + command=$($GUIX_BIN/sed --quiet \ 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"]\+\)".*/\1/p') case "$command" in *'guix home build'*|*'guix system build'*) $GUIX_BIN/echo \ - '{"hookSpecificOutput": {' \ - ' "hookEventName": "PreToolUse",' \ - ' "permissionDecision": "ask"' \ - '}}' - exit 0 - ;; + '{"hookSpecificOutput":{"hookEventName":"PreToolUse",' \ + '"permissionDecision":"ask"}}' + exit 0 ;; *'guix home reconfigure'*|*'guix system reconfigure'*) if $GUIX_BIN/echo "$PWD" | $GUIX_BIN/grep -q '/worktrees/' then $GUIX_BIN/echo \ - '{"hookSpecificOutput": {' \ - ' "hookEventName": "PreToolUse",' \ - ' "permissionDecision": "deny",' \ - ' "systemMessage": "🚫 Reconfigure only on main worktree."' \ - '}}' + '{"hookSpecificOutput":{"hookEventName":"PreToolUse",' \ + '"permissionDecision":"deny",' \ + '"systemMessage":"🚫 Reconfigure only on main."}}' exit 0 fi $GUIX_BIN/echo \ - '{"hookSpecificOutput": {' \ - ' "hookEventName": "PreToolUse",' \ - ' "permissionDecision": "ask"' \ - '}}' - exit 0 - ;; + '{"hookSpecificOutput":{"hookEventName":"PreToolUse",' \ + '"permissionDecision":"ask"}}' + exit 0 ;; *'make build-'*|*'make test-'*) $GUIX_BIN/echo \ - '{"hookSpecificOutput": {' \ - ' "hookEventName": "PreToolUse",' \ - ' "permissionDecision": "ask"' \ - '}}' - exit 0 - ;; + '{"hookSpecificOutput":{"hookEventName":"PreToolUse",' \ + '"permissionDecision":"ask"}}' + exit 0 ;; *'make home-'*|*'make system-'*|*'make vps-'*) if $GUIX_BIN/echo "$PWD" | $GUIX_BIN/grep -q '/worktrees/' then $GUIX_BIN/echo \ - '{"hookSpecificOutput": {' \ - ' "hookEventName": "PreToolUse",' \ - ' "permissionDecision": "deny",' \ - ' "systemMessage": "🚫 Reconfigure only on main worktree."' \ - '}}' + '{"hookSpecificOutput":{"hookEventName":"PreToolUse",' \ + '"permissionDecision":"deny",' \ + '"systemMessage":"🚫 Reconfigure only on main."}}' exit 0 fi $GUIX_BIN/echo \ - '{"hookSpecificOutput": {' \ - ' "hookEventName": "PreToolUse",' \ - ' "permissionDecision": "ask"' \ - '}}' - exit 0 - ;; + '{"hookSpecificOutput":{"hookEventName":"PreToolUse",' \ + '"permissionDecision":"ask"}}' + exit 0 ;; esac diff --git a/.claude/hooks/block-worktrees b/.claude/hooks/block-worktrees index 84889b1..ede997b 100755 --- a/.claude/hooks/block-worktrees +++ b/.claude/hooks/block-worktrees @@ -4,23 +4,58 @@ # # block-worktrees — Enforce worktree isolation. # -# Matches all tool types (* — runs after create-worktree PWD gate). -# Only active when already inside a worktree. +# Matches all tool types (*). Only active inside a worktree. +# Denies writes to main, branch switches, and exit-with-remove. -if ! echo "$PWD" | sed --quiet '\|/worktrees/|q0;$q1' +# Pre-flight: critical environment variables. +if [ -z "$GUIX_BIN" ]; then + echo '{"systemMessage":"❌ block-worktrees: GUIX_BIN not set"}' >&2 + exit 0 +fi +if [ -z "$CLAUDE_CODE_SESSION_ID" ]; then + echo '{"systemMessage":"❌ block-worktrees: CLAUDE_CODE_SESSION_ID not set"}' >&2 + exit 0 +fi + +# Pre-flight: commands referenced by this hook. +if [ ! -x "$GUIX_BIN/sed" ]; then + echo '{"hookSpecificOutput":{"permissionDecision":"deny",' \ + '"permissionDecisionReason":"block-worktrees: GUIX_BIN/sed missing",' \ + '"systemMessage":"❌ block-worktrees broken: GUIX_BIN/sed not found"}}' + exit 0 +fi +if [ ! -x "$GUIX_BIN/echo" ]; then + echo '{"hookSpecificOutput":{"permissionDecision":"deny",' \ + '"permissionDecisionReason":"block-worktrees: GUIX_BIN/echo missing",' \ + '"systemMessage":"❌ block-worktrees broken: GUIX_BIN/echo not found"}}' + exit 0 +fi +GIT_BIN="$HOME/.guix-profile/bin/git" +if [ ! -x "$GIT_BIN" ]; then + echo '{"hookSpecificOutput":{"permissionDecision":"deny",' \ + '"permissionDecisionReason":"block-worktrees: git missing",' \ + '"systemMessage":"❌ block-worktrees broken: git not found"}}' + exit 0 +fi + +# Only active inside a worktree. +if ! echo "$PWD" \ + | $GUIX_BIN/sed --quiet '\|/worktrees/|q0;$q1' then exit 0 fi stdin=$(cat) tool=$(echo "$stdin" \ - | sed -n 's/.*"tool_name"[[:space:]]*:[[:space:]]*"\([^"]\+\)".*/\1/p') -worktree="$CLAUDE_PROJECT_DIR/.claude/worktrees/$CLAUDE_CODE_SESSION_ID" + | $GUIX_BIN/sed -n \ + 's/.*"tool_name"[[:space:]]*:[[:space:]]*"\([^"]\+\)".*/\1/p') +proj_root=$($GIT_BIN -C "$PWD" rev-parse --show-toplevel 2>/dev/null) +[ -z "$proj_root" ] && exit 0 +worktree="$proj_root/.claude/worktrees/$CLAUDE_CODE_SESSION_ID" die() { echo \ - '{"hookSpecificOutput":{' \ - '"permissionDecision":"deny",' \ + '{"hookSpecificOutput":{"permissionDecision":"deny",' \ '"permissionDecisionReason":"'"$1"'"}}' exit 0 } @@ -31,55 +66,50 @@ EnterWorktree) ExitWorktree) action=$(echo "$stdin" \ - | sed -n 's/.*"action"[[:space:]]*:[[:space:]]*"\([^"]\+\)".*/\1/p') - if [ "$action" = "remove" ] - then - die "Worktree deletion is disabled. Use" \ - "ExitWorktree with action: keep instead." - fi - ;; + | $GUIX_BIN/sed -n \ + 's/.*"action"[[:space:]]*:[[:space:]]*"\([^"]\+\)".*/\1/p') + if [ "$action" = "remove" ]; then + die "ExitWorktree with action: remove is disabled." \ + "Use action: keep instead." + fi ;; Write|Edit) path=$(echo "$stdin" \ - | sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\([^"]\+\)".*/\1/p') + | $GUIX_BIN/sed -n \ + 's/.*"file_path"[[:space:]]*:[[:space:]]*"\([^"]\+\)".*/\1/p') case "$path" in - "$CLAUDE_PROJECT_DIR"*) - if ! echo "$path" | sed --quiet "\|$worktree|q0;\$q1" + "$proj_root"*) + if ! echo "$path" \ + | $GUIX_BIN/sed --quiet "\|$worktree|q0;\$q1" then die "Write blocked: target outside worktree." - fi - ;; - esac - ;; + fi ;; + esac ;; Bash) cmd=$(echo "$stdin" \ - | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"]\+\)".*/\1/p') - + | $GUIX_BIN/sed -n \ + 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"]\+\)".*/\1/p') case "$cmd" in *"git checkout"*|*"git switch"*) die "Branch switching disabled." ;; - *"git worktree remove"*|*"git worktree prune"*) if ! echo "$cmd" \ - | sed --quiet '/merge-worktree/q0;$q1' + | $GUIX_BIN/sed --quiet '/merge-worktree/q0;$q1' then die "Git on main blocked. Use /merge-worktree." fi ;; - - *"$CLAUDE_PROJECT_DIR"*|\ - *'$CLAUDE_PROJECT_DIR'*|\ - *'${CLAUDE_PROJECT_DIR}'*) - if echo "$cmd" | sed --quiet "\|$worktree|q0;\$q1" + *"$proj_root"*|*'$proj_root'*|*'${proj_root}'*) + if echo "$cmd" \ + | $GUIX_BIN/sed --quiet "\|$worktree|q0;\$q1" then : # references own worktree — allowed elif echo "$cmd" \ - | sed --quiet '/merge-worktree/q0;$q1' + | $GUIX_BIN/sed --quiet '/merge-worktree/q0;$q1' then : # merge-worktree — allowed - else die "Blocked: reference to main project." + else + die "Reference to main blocked." fi ;; - esac - ;; - + esac ;; esac exit 0 diff --git a/.claude/hooks/create-worktree b/.claude/hooks/create-worktree index fc47c70..0b9aa1c 100755 --- a/.claude/hooks/create-worktree +++ b/.claude/hooks/create-worktree @@ -4,31 +4,61 @@ # # create-worktree — PWD gate: force entry into a worktree. # -# Matches all tool types (*). Denies every tool call (except -# EnterWorktree) unless already in this session's worktree. +# Matches all tool types (*). Denies every tool call except +# EnterWorktree unless already in this session's worktree. + +# Pre-flight: critical environment variables. +if [ -z "$GUIX_BIN" ]; then + echo '{"systemMessage":"❌ create-worktree: GUIX_BIN not set"}' >&2 + exit 0 +fi +if [ -z "$CLAUDE_PROJECT_DIR" ]; then + echo '{"systemMessage":"❌ create-worktree: CLAUDE_PROJECT_DIR not set"}' >&2 + exit 0 +fi +if [ -z "$CLAUDE_CODE_SESSION_ID" ]; then + echo '{"systemMessage":"❌ create-worktree: CLAUDE_CODE_SESSION_ID not set"}' >&2 + exit 0 +fi + +# Pre-flight: commands referenced by this hook. +if [ ! -x "$GUIX_BIN/sed" ]; then + echo '{"hookSpecificOutput":{"permissionDecision":"deny",' \ + '"permissionDecisionReason":"create-worktree: GUIX_BIN/sed missing",' \ + '"systemMessage":"❌ create-worktree broken: GUIX_BIN/sed not found"}}' + exit 0 +fi +if [ ! -x "$GUIX_BIN/grep" ]; then + echo '{"hookSpecificOutput":{"permissionDecision":"deny",' \ + '"permissionDecisionReason":"create-worktree: GUIX_BIN/grep missing",' \ + '"systemMessage":"❌ create-worktree broken: GUIX_BIN/grep not found"}}' + exit 0 +fi stdin=$(cat) # Exempt EnterWorktree. if echo "$stdin" \ - | grep -q '"tool_name"[[:space:]]*:[[:space:]]*"EnterWorktree"' + | $GUIX_BIN/grep -q '"tool_name"[[:space:]]*:[[:space:]]*"EnterWorktree"' then exit 0 fi # Exempt git push (needed from main project after merge). if echo "$stdin" \ - | grep -q '"tool_name"[[:space:]]*:[[:space:]]*"Bash"' + | $GUIX_BIN/grep -q '"tool_name"[[:space:]]*:[[:space:]]*"Bash"' then cmd=$(echo "$stdin" \ - | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') + | $GUIX_BIN/sed -n \ + 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') case "$cmd" in - "git push"*) exit 0 ;; + "git push"*|*"/git push"*) exit 0 ;; esac fi # Deny if not in this session's worktree. -if ! echo "$PWD" | grep -q "/worktrees/${CLAUDE_CODE_SESSION_ID}" +if ! echo "$PWD" | $GUIX_BIN/grep -q \ + "/worktrees/${CLAUDE_CODE_SESSION_ID}" then echo \ '{"hookSpecificOutput": {' \ @@ -44,4 +74,4 @@ then exit 0 fi -exit 0 \ No newline at end of file +exit 0 diff --git a/.claude/hooks/guix-build b/.claude/hooks/guix-build index 911b847..8d7778a 100755 --- a/.claude/hooks/guix-build +++ b/.claude/hooks/guix-build @@ -2,83 +2,137 @@ # Copyright (c) 2026 Jakub Czajka # License: GPL-3.0 or later. # -# guix-build — Stop hook that drives a build-queue state machine. +# guix-build — Stop hook: drive the build-queue state machine. # # State machine (queue = .claude/.build-queue.): # # │ # ▼ -# clean tree ────────────────────---─► exit 0 +# clean tree AND at main tip? ───────────► exit 0 # │ # ▼ -# no queue file ─────────────────---─► exit 2 -# │ ask user for targets -# │ write queue file or touch empty file +# no queue file ─────────────────────► exit 0 +# │ list available Makefile targets # ▼ # queue non-empty? # │ -# ├─ no ─────────────────────────---─► exit 0 +# ├─ no ─────────────────────────────► exit 0 # │ rm queue file # ▼ # read first target # │ # ▼ -# target stale? ─────────────────---─► exit 2 +# target stale? ─────────────────────► exit 2 # │ drop it, check next on re-stop # ▼ # build target # │ -# ├─ failure ───────────────────---──► exit 2 +# ├─ failure ────────────────────────► exit 2 # │ # └─ success ──► remove from queue ──► exit 2 # build next on re-stop -MAKEFILE="$CLAUDE_PROJECT_DIR/Makefile" +# Pre-flight: critical environment variables. +if [ -z "$GUIX_BIN" ]; then + echo '{"systemMessage":"❌ guix-build: GUIX_BIN not set"}' >&2 + exit 0 +fi +if [ -z "$CLAUDE_CODE_SESSION_ID" ]; then + echo '{"systemMessage":"❌ guix-build: CLAUDE_CODE_SESSION_ID not set"}' >&2 + exit 0 +fi -if [ -z "$($HOME/.guix-home/profile/bin/git -C "$CLAUDE_PROJECT_DIR" \ - status --porcelain --untracked-files=no)" ] -then - $GUIX_BIN/echo '{"systemMessage":"🟢 Working tree clean."}' +# Pre-flight: commands referenced by this hook. +if [ ! -x "$GUIX_BIN/echo" ]; then + echo '{"systemMessage":"❌ guix-build broken: GUIX_BIN/echo not found"}' >&2 + exit 0 +fi +if [ ! -x "$GUIX_BIN/mkdir" ]; then + echo '{"systemMessage":"❌ guix-build broken: GUIX_BIN/mkdir not found"}' >&2 + exit 0 +fi +if [ ! -x "$GUIX_BIN/head" ]; then + echo '{"systemMessage":"❌ guix-build broken: GUIX_BIN/head not found"}' >&2 + exit 0 +fi +if [ ! -x "$GUIX_BIN/grep" ]; then + echo '{"systemMessage":"❌ guix-build broken: GUIX_BIN/grep not found"}' >&2 + exit 0 +fi +if [ ! -x "$GUIX_BIN/sed" ]; then + echo '{"systemMessage":"❌ guix-build broken: GUIX_BIN/sed not found"}' >&2 + exit 0 +fi +if [ ! -x "$GUIX_BIN/tr" ]; then + echo '{"systemMessage":"❌ guix-build broken: GUIX_BIN/tr not found"}' >&2 + exit 0 +fi +GIT_BIN="$HOME/.guix-profile/bin/git" +if [ ! -x "$GIT_BIN" ]; then + echo '{"systemMessage":"❌ guix-build broken: git not found"}' >&2 exit 0 fi +MAKE_BIN="$HOME/.guix-profile/bin/make" +if [ ! -x "$MAKE_BIN" ]; then + echo '{"systemMessage":"❌ guix-build broken: make not found"}' >&2 + exit 0 +fi + +DIR="${CLAUDE_PROJECT_DIR:-$PWD}" +M="$DIR/Makefile" + +clean_tree=$($GIT_BIN -C "$DIR" status --porcelain) +ahead=$($GIT_BIN -C "$DIR" rev-list --count main..HEAD 2>/dev/null) +behind=$($GIT_BIN -C "$DIR" rev-list --count HEAD..main 2>/dev/null) -$GUIX_BIN/mkdir --parents "$CLAUDE_PROJECT_DIR/.claude" -QUEUE_FILE="$CLAUDE_PROJECT_DIR/.claude/.build-queue.$CLAUDE_CODE_SESSION_ID" +if [ -z "$clean_tree" ] && [ "$ahead" = 0 ] && [ "$behind" = 0 ]; then + echo "🟢 Working tree clean and on main tip." + exit 0 +fi -# No queue file yet. -if [ ! -f "$QUEUE_FILE" ] -then - "$CLAUDE_PROJECT_DIR/.claude/scripts/ask-build-queue" +if [ "$behind" != 0 ] && [ "$behind" != "" ]; then + echo "⚠️ Branch is $behind commit(s) behind main." + if [ "$ahead" != 0 ] && [ "$ahead" != "" ]; then + echo "⚠️ Branch has diverged — $ahead commit(s) ahead of main." + fi + echo "Rebase onto main before building." exit 2 fi -# Build the first target if the queue is non-empty. -if [ -s "$QUEUE_FILE" ] -then - target=$($GUIX_BIN/head --lines=1 "$QUEUE_FILE") - if ! $GUIX_BIN/grep --quiet "^${target}:" "$MAKEFILE" - then - $GUIX_BIN/sed --in-place '1d' "$QUEUE_FILE" +$GUIX_BIN/mkdir -p "$DIR/.claude" +Q="$DIR/.claude/.build-queue.$CLAUDE_CODE_SESSION_ID" - exec 1>&2 - $GUIX_BIN/echo "Target $target no longer exists, skipped." +# No queue file yet — list available targets. +if [ ! -f "$Q" ]; then + b=$($GIT_BIN -C "$DIR" branch --show-current 2>/dev/null \ + || echo unknown) + [ "$b" = "main" ] \ + && p="guix.*reconfigure" \ + || p="guix.*build" + t=$($GUIX_BIN/sed --regexp-extended --quiet \ + "/^([a-zA-Z_-][a-zA-Z0-9_-]*):/h;/$p/{g;s/^([a-zA-Z_-][a-zA-Z0-9_-]*):.*/\1/p}" \ + "$M" | $GUIX_BIN/tr '\n' ' ') + echo "Queue empty. Targets: ${t}" + exit 0 +fi + +# Build the first target if the queue is non-empty. +if [ -s "$Q" ]; then + target=$($GUIX_BIN/head -n1 "$Q") + if ! $GUIX_BIN/grep -q "^${target}:" "$M"; then + $GUIX_BIN/sed -i '1d' "$Q" + echo "Target $target no longer exists, skipped." exit 2 fi - - $GUIX_BIN/make -C "$CLAUDE_PROJECT_DIR" "$target" 1>&2 || { - exec 1>&2 - - $GUIX_BIN/echo "❌ Build failed (exit $?) — fix and retry." + $MAKE_BIN -C "$DIR" "$target" || { + echo "❌ Build failed (exit $?) — fix and retry." exit 2 } - $GUIX_BIN/sed --in-place '1d' "$QUEUE_FILE" - - exec 1>&2 - $GUIX_BIN/echo "Stop to continue." + $GUIX_BIN/sed -i '1d' "$Q" + echo "Stop to continue." exit 2 fi # Queue is empty — we are finished. -$GUIX_BIN/echo '{"systemMessage":"✅ Queue finished."}' -$GUIX_BIN/rm --force "$QUEUE_FILE" - +echo "✅ Queue finished." +$GUIX_BIN/rm -f "$Q" diff --git a/.claude/hooks/guix-check b/.claude/hooks/guix-check index 43c30af..8e8e0fb 100755 --- a/.claude/hooks/guix-check +++ b/.claude/hooks/guix-check @@ -1,9 +1,27 @@ #!/bin/sh +# guix-check — Check Guile files after edit. # Copyright (c) 2026 Jakub Czajka # License: GPL-3.0 or later. -# -# guix-check — Checks to be applied after every edit of a guile file. If a check -# fails, the script exits with status code 2 to make Claude Code fix the issues. + +# Pre-flight: critical environment variables. +if [ -z "$GUIX_BIN" ]; then + echo '{"systemMessage":"❌ guix-check: GUIX_BIN not set"}' >&2 + exit 0 +fi +if [ -z "$CLAUDE_PROJECT_DIR" ]; then + echo '{"systemMessage":"❌ guix-check: CLAUDE_PROJECT_DIR not set"}' >&2 + exit 0 +fi + +# Pre-flight: commands referenced by this hook. +for cmd in "$GUIX_BIN/sed" "$GUIX_BIN/grep" "$GUIX_BIN/guix" \ + "$GUIX_BIN/mktemp" "$GUIX_BIN/rm" "$GUIX_BIN/guild" "$GUIX_BIN/echo" +do + if [ ! -x "$cmd" ]; then + echo '{"systemMessage":"❌ guix-check broken: '"$cmd"' not found"}' >&2 + exit 0 + fi +done file=$($GUIX_BIN/sed --quiet \ 's/.*"file_path"\s*:\s*"\([^"]*\)".*/\1/p') @@ -39,8 +57,6 @@ then exit 2 fi -# guild compile needs an output file, but the hook only cares about warnings. -# Create a disposable directory and schedule its removal. tmpdir=$($GUIX_BIN/mktemp --directory) trap '$GUIX_BIN/rm --recursive --force "$tmpdir"' EXIT @@ -66,8 +82,6 @@ out=$( "$file" 2>&1 ) -# guild exits non-zero for fatal errors but still exits 0 for warnings so we -# check both. if [ $? -ne 0 ] || $GUIX_BIN/echo "$out" | $GUIX_BIN/grep --quiet warning then exec 1>&2 diff --git a/.claude/hooks/notify b/.claude/hooks/notify index f143e57..f95a960 100755 --- a/.claude/hooks/notify +++ b/.claude/hooks/notify @@ -3,24 +3,31 @@ # License: GPL-3.0 or later. # # notify — Send a desktop notification for Claude Code events. +# +# Non-critical: exits silently when commands are missing. -type=$(sed -n \ - 's/.*"notification_type":"\([^"]*\)".*/\1/p') -case "$type" in - permission_prompt) - body="🤖🔐 Needs Approval" - ;; - idle_prompt) - body="🤖✅ Finished" - ;; - *) +# Pre-flight: critical environment variables. +if [ -z "$GUIX_BIN" ]; then + exit 0 +fi + +# Pre-flight: commands referenced by this hook. +if [ ! -x "$GUIX_BIN/sed" ]; then exit 0 - ;; +fi + +DUNSTIFY="$HOME/.guix-profile/bin/dunstify" +if [ ! -x "$DUNSTIFY" ]; then + exit 0 +fi + +type=$($GUIX_BIN/sed -n \ + 's/.*"notification_type":"\([^"]*\)".*/\1/p') +case "$type" in + permission_prompt) body="🤖🔐 Needs Approval" ;; + idle_prompt) body="🤖✅ Finished" ;; + *) exit 0 ;; esac -$HOME/.guix-home/profile/bin/dunstify \ - --urgency normal \ - --timeout 10000 \ - --appname "Claude Code" \ - "Claude Code" \ - "$body" +$DUNSTIFY --urgency normal --timeout 10000 \ + --appname "Claude Code" "Claude Code" "$body" diff --git a/.claude/hooks/status-line b/.claude/hooks/status-line index da4370b..e85be77 100755 --- a/.claude/hooks/status-line +++ b/.claude/hooks/status-line @@ -2,12 +2,28 @@ # Copyright (c) 2026 Jakub Czajka # License: GPL-3.0 or later. # -# status-line - Display current session/worktree in the Claude Code prompt. +# status-line — Display the current branch or worktree in the +# Claude Code status bar. -if echo "$PWD" | grep -q '/worktrees/' -then +# Pre-flight: critical environment variables. +if [ -z "$GUIX_BIN" ]; then + echo "🌿 (GUIX_BIN not set)" + exit 0 +fi + +# Pre-flight: commands referenced by this hook. +if [ ! -x "$GUIX_BIN/grep" ]; then + echo "🌿 (grep not found)" + exit 0 +fi + +GIT_BIN="$HOME/.guix-profile/bin/git" + +if echo "$PWD" | $GUIX_BIN/grep -q '/worktrees/'; then echo "🌿 $CLAUDE_CODE_SESSION_ID" else - branch=$(git -C "$PWD" branch --show-current 2>/dev/null) - [ -n "$branch" ] && echo "🌿 $branch" + if [ -x "$GIT_BIN" ]; then + branch=$($GIT_BIN -C "$PWD" branch --show-current 2>/dev/null) + [ -n "$branch" ] && echo "🌿 $branch" + fi fi diff --git a/.claude/settings.json b/.claude/settings.json index 6bd7704..f9249f5 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,6 +1,5 @@ { "env": { - "CLAUDE_PROJECT_DIR": "$PWD", "GUIX_BIN": "/run/current-system/profile/bin", "PROFILE": "nonfree" }, -- 2.47.3