--- /dev/null
+#!/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
--- /dev/null
+#!/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
"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",
-# 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
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 \