]> git.ekhem.eu.org Git - guix.git/commitdiff
[hooks] pre-flight checks, surface failures, prettify
authorJakub Czajka <jakub@ekhem.eu.org>
Tue, 28 Jul 2026 07:20:42 +0000 (07:20 +0000)
committerJakub Czajka <jakub@ekhem.eu.org>
Thu, 30 Jul 2026 16:01:40 +0000 (16:01 +0000)
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
.claude/hooks/block-builds
.claude/hooks/block-worktrees
.claude/hooks/create-worktree
.claude/hooks/guix-build
.claude/hooks/guix-check
.claude/hooks/notify
.claude/hooks/status-line
.claude/settings.json

index ce3922d860ca1fc0c039c5d733a1a516dfa4408e..1f6ec99c7e20ee863148fa5c1a7ba2cbccbb5cd7 100755 (executable)
@@ -1,62 +1,66 @@
 #!/bin/sh
-# Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
-# 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
index 84889b1efe4ac32e7fb2033cdd1160d838404b1f..ede997baee122305e434fe39bc582425fb496959 100755 (executable)
@@ -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
index fc47c70411ab76245972ccdf393fcb58c7624719..0b9aa1c310b083e77e37e805ea027407825bb09a 100755 (executable)
@@ -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
index 911b847384a8557e618541a520afb4687e1c927d..8d7778a2abf39a49cf06714d7fccac403f448358 100755 (executable)
 # Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
 # 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.<session_id>):
 #
 #   │
 #   ▼
-#   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"
index 43c30af10703e0ce4fe3c82d6740e415baad5e23..8e8e0fb141c23e225bf7c5fa88d749b69e84a0fb 100755 (executable)
@@ -1,9 +1,27 @@
 #!/bin/sh
+# guix-check — Check Guile files after edit.
 # Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
 # 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
index f143e5789a7555ab4dd7a6b1edc6b1b6cba1b86f..f95a960b29e538c52a263af6821d9666abc8f519 100755 (executable)
@@ -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"
index da4370b5fcfc93f775a50af284ce7b150f57d162..e85be774dc7cfb5ccf95af8737328826874d08df 100755 (executable)
@@ -2,12 +2,28 @@
 # Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
 # 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
index 6bd7704f1d324100f8d042a8d1589fc962c1525b..f9249f50e5de002ad952d67dc8d95633884e0632 100644 (file)
@@ -1,6 +1,5 @@
 {
   "env": {
-    "CLAUDE_PROJECT_DIR": "$PWD",
     "GUIX_BIN": "/run/current-system/profile/bin",
     "PROFILE": "nonfree"
   },