]> git.ekhem.eu.org Git - guix.git/commitdiff
[ai] Add guix-build stop hook, guard-commands hook, and Makefile build targets
authorJakub Czajka <jakub@ekhem.eu.org>
Sun, 21 Jun 2026 22:05:33 +0000 (00:05 +0200)
committerJakub Czajka <jakub@ekhem.eu.org>
Tue, 7 Jul 2026 18:47:47 +0000 (20:47 +0200)
guard-commands guards against dangerous or branch-inappropriate
commands: guix reconfigure (denied on branches), guix build
(ask everywhere), make reconfigure targets (denied on
branches), git checkout/switch (denied everywhere).

Makefile gains build-home-nonfree, build-home-libre,
build-system-nonfree, and build-system-libre targets for
the build queue.

.claude/hooks/guard-commands [new file with mode: 0755]
.claude/hooks/guix-build [new file with mode: 0755]
.claude/settings.json
Makefile

diff --git a/.claude/hooks/guard-commands b/.claude/hooks/guard-commands
new file mode 100755 (executable)
index 0000000..3962a77
--- /dev/null
@@ -0,0 +1,89 @@
+#!/bin/sh
+# Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
+# License: GPL-3.0 or later.
+#
+# guard-commands — PreToolUse hook that guards against dangerous or
+# branch-inappropriate commands.
+
+cmd=$($GUIX_BIN/cat | $GUIX_BIN/sed --regexp-extended --quiet \
+    's/.*"command"\s*:\s*"([^"]+)".*/\1/p')
+[ -z "$cmd" ] && exit 0
+
+branch=$($HOME/.guix-home/profile/bin/git -C "$CLAUDE_PROJECT_DIR" \
+    branch --show-current 2>/dev/null)
+
+# --- git checkout / git switch: deny everywhere ---
+if $GUIX_BIN/echo "$cmd" | $GUIX_BIN/grep --extended-regexp --quiet \
+    '\bgit (checkout|switch)\b'; then
+    $GUIX_BIN/echo \
+        '{"hookSpecificOutput": {' \
+        '  "hookEventName": "PreToolUse",' \
+        '  "permissionDecision": "deny",' \
+        '  "systemMessage": "Branch switching disabled. Use EnterWorktree."' \
+        '}}'
+    exit 0
+fi
+
+# --- make build-*: ask everywhere ---
+if $GUIX_BIN/echo "$cmd" | $GUIX_BIN/grep --extended-regexp --quiet \
+    '\bmake build-'; then
+    $GUIX_BIN/echo \
+        '{"hookSpecificOutput": {' \
+        '  "hookEventName": "PreToolUse",' \
+        '  "permissionDecision": "ask"' \
+        '}}'
+    exit 0
+fi
+
+# --- guix build: ask everywhere ---
+if $GUIX_BIN/echo "$cmd" | $GUIX_BIN/grep --extended-regexp --quiet \
+    '\bguix (home|system) build\b'; then
+    $GUIX_BIN/echo \
+        '{"hookSpecificOutput": {' \
+        '  "hookEventName": "PreToolUse",' \
+        '  "permissionDecision": "ask"' \
+        '}}'
+    exit 0
+fi
+
+# --- guix reconfigure: ask on main, deny on branches ---
+if $GUIX_BIN/echo "$cmd" | $GUIX_BIN/grep --extended-regexp --quiet \
+    '\bguix (home|system) reconfigure\b'; then
+    if [ "$branch" != "main" ]; then
+        $GUIX_BIN/echo \
+            '{"hookSpecificOutput": {' \
+            '  "hookEventName": "PreToolUse",' \
+            '  "permissionDecision": "deny",' \
+            '  "systemMessage": "guix reconfigure blocked. Merge into main."' \
+            '}}'
+        exit 0
+    fi
+    $GUIX_BIN/echo \
+        '{"hookSpecificOutput": {' \
+        '  "hookEventName": "PreToolUse",' \
+        '  "permissionDecision": "ask"' \
+        '}}'
+    exit 0
+fi
+
+# --- make home-* / make system-*: ask on main, deny on branches ---
+if $GUIX_BIN/echo "$cmd" | $GUIX_BIN/grep --extended-regexp --quiet \
+    '\bmake (home|system)-'; then
+    if [ "$branch" != "main" ]; then
+        $GUIX_BIN/echo \
+            '{"hookSpecificOutput": {' \
+            '  "hookEventName": "PreToolUse",' \
+            '  "permissionDecision": "deny",' \
+            '  "systemMessage": "Reconfigure blocked. Merge into main."' \
+            '}}'
+        exit 0
+    fi
+    $GUIX_BIN/echo \
+        '{"hookSpecificOutput": {' \
+        '  "hookEventName": "PreToolUse",' \
+        '  "permissionDecision": "ask"' \
+        '}}'
+    exit 0
+fi
+
+exit 0
diff --git a/.claude/hooks/guix-build b/.claude/hooks/guix-build
new file mode 100755 (executable)
index 0000000..72f1ba1
--- /dev/null
@@ -0,0 +1,97 @@
+#!/bin/sh
+# 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.
+#
+# State machine (queue = .claude/.build-queue.<session_id>):
+#
+#   │
+#   ▼
+#   clean tree ────────────────────---─► exit 0
+#   │
+#   ▼
+#   no queue file ─────────────────---─► exit 2
+#   │                                    ask user for targets
+#   │                                    write queue file or touch empty file
+#   ▼
+#   queue non-empty?
+#   │
+#   ├─ no ─────────────────────────---─► exit 0
+#   │                                    rm queue file
+#   ▼
+#   read first target
+#   │
+#   ▼
+#   target stale? ─────────────────---─► exit 2
+#   │                                    drop it, check next on re-stop
+#   ▼
+#   build target
+#   │
+#   ├─ failure ───────────────────---──► exit 2
+#   │
+#   └─ success ──► remove from queue ──► exit 2
+#                                        build next on re-stop
+
+# Print space-separated Makefile targets with guix reconfigure.
+MAKEFILE="$CLAUDE_PROJECT_DIR/Makefile"
+build_targets() {
+    $GUIX_BIN/sed --regexp-extended --quiet \
+        '/^([a-zA-Z_-][a-zA-Z0-9_-]*):/h
+         /guix.*reconfigure/{g;s/^([a-zA-Z_-][a-zA-Z0-9_-]*):.*/\1/p}' \
+        "$1" | $GUIX_BIN/tr '\n' ' '
+}
+
+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."}'
+    exit 0
+fi
+
+$GUIX_BIN/mkdir --parents "$CLAUDE_PROJECT_DIR/.claude"
+QUEUE_FILE="$CLAUDE_PROJECT_DIR/.claude/.build-queue.$CLAUDE_CODE_SESSION_ID"
+
+# No queue file yet — ask the user which targets to build.
+if [ ! -f "$QUEUE_FILE" ]; then
+    exec 1>&2
+    $GUIX_BIN/echo "First, use AskUserQuestion (single-select) to ask whether"
+    $GUIX_BIN/echo "to build at all. Options: [Build targets, Skip build]."
+    $GUIX_BIN/echo ""
+    $GUIX_BIN/echo "If Build, use AskUserQuestion (multiSelect, max 4) with"
+    $GUIX_BIN/echo "these targets: $(build_targets "$MAKEFILE")."
+    $GUIX_BIN/echo ""
+    $GUIX_BIN/echo "Write selected targets to ${QUEUE_FILE} (one per line),"
+    $GUIX_BIN/echo "then stop so the build begins."
+    $GUIX_BIN/echo ""
+    $GUIX_BIN/echo "If Skip: write an empty file (touch ${QUEUE_FILE})."
+    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"
+
+        exec 1>&2
+        $GUIX_BIN/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."
+        exit 2
+    }
+    $GUIX_BIN/sed --in-place '1d' "$QUEUE_FILE"
+
+    exec 1>&2
+    $GUIX_BIN/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"
+exit 0
index d15a65932ec3ab0a4d4c5c89bea5296aa69085e4..dc5acb1ff9c86b521be6a6b5e30a23cfbeaa4048 100644 (file)
@@ -4,6 +4,17 @@
     "PROFILE": "nonfree"
   },
   "hooks": {
+    "PreToolUse": [
+      {
+        "matcher": "Bash",
+        "hooks": [
+          {
+            "type": "command",
+            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/guard-commands"
+          }
+        ]
+      }
+    ],
     "PostToolUse": [
       {
         "matcher": "Edit|Write",
         ]
       }
     ],
+    "Stop": [
+      {
+        "hooks": [
+          {
+            "type": "command",
+            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/guix-build",
+            "statusMessage": "Build and check Guix."
+          }
+        ]
+      }
+    ],
     "Notification": [
       {
         "matcher": "permission_prompt|idle_prompt",
index 6a0ee245d2881b91cd6b5058a86387e0c144d078..425cb4a1a38fcd70fb4ad013746664dd3b265d96 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-# Copyright (c) 2022-2024 Jakub Czajka <jakub@ekhem.eu.org>
+# Copyright (c) 2022-2026 Jakub Czajka <jakub@ekhem.eu.org>
 # License: GPL-3.0 or later.
 
 guix-profile := ~/.guix-home/profile
@@ -19,6 +19,27 @@ system-nonfree:
 system-libre:
        sudo GUIX_PACKAGE_PATH=`pwd` PROFILE=libre guix system reconfigure system.scm
 
+# Build-only targets — validate configuration without activating.
+# These build derivations and cache them in the store so a
+# subsequent reconfigure is near-instant.  The build queue runs
+# these on non-main branches; on main it runs the reconfigure
+# targets directly.
+build-home-nonfree:
+       GUIX_PACKAGE_PATH=`pwd` PROFILE=nonfree \
+           guix home build home.scm
+
+build-home-libre:
+       GUIX_PACKAGE_PATH=`pwd` PROFILE=libre \
+           guix home build home.scm
+
+build-system-nonfree:
+       GUIX_PACKAGE_PATH=`pwd` PROFILE=nonfree \
+           guix system build system.scm
+
+build-system-libre:
+       GUIX_PACKAGE_PATH=`pwd` PROFILE=libre \
+           guix system build system.scm
+
 tags:
        find $(guix-store) -wholename "*guix-$(guix-version)*.scm" -or \
                -wholename "*guix-module-union*.scm" -or \