]> git.ekhem.eu.org Git - guix.git/commitdiff
[ai] Replace guard-commands with block-builds, block-worktrees, and create-worktree...
authorJakub Czajka <jakub@ekhem.eu.org>
Mon, 6 Jul 2026 10:23:44 +0000 (12:23 +0200)
committerJakub Czajka <jakub@ekhem.eu.org>
Tue, 7 Jul 2026 21:37:07 +0000 (23:37 +0200)
Splits the monolithic guard-commands hook into three single-purpose
hooks with more granular matchers in settings.json.

block-builds:
  Guards guix and make build/reconfigure commands. Asks on main,
  denies in worktrees for destructive operations (reconfigure,
  home-*, system-*).

block-worktrees:
  Denies git checkout/switch outright. Enforces EnterWorktree
  (must not already be in a worktree) and ExitWorktree (must not
  have unpushed commits diverging from main). Sends a desktop
  notification on EnterWorktree success.

create-worktree:
  Uses a universal matcher ("*") to deny all tool calls on main
  unless already inside the session's worktree. Allows
  EnterWorktree through to let the user enter an isolated session.

.claude/hooks/block-builds [new file with mode: 0755]
.claude/hooks/block-worktrees [new file with mode: 0755]
.claude/hooks/create-worktree [new file with mode: 0755]
.claude/hooks/guard-commands [deleted file]
.claude/settings.json
README

diff --git a/.claude/hooks/block-builds b/.claude/hooks/block-builds
new file mode 100755 (executable)
index 0000000..253b13c
--- /dev/null
@@ -0,0 +1,62 @@
+#!/bin/sh
+# Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
+# License: GPL-3.0 or later.
+#
+# block-builds - Guard guix/make build/reconfigure.
+
+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
+        ;;
+    *'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."' \
+                '}}'
+            exit 0
+        fi
+        $GUIX_BIN/echo \
+            '{"hookSpecificOutput": {' \
+            ' "hookEventName": "PreToolUse",' \
+            ' "permissionDecision": "ask"' \
+            '}}'
+        exit 0
+        ;;
+    *'make build-'*)
+        $GUIX_BIN/echo \
+            '{"hookSpecificOutput": {' \
+            ' "hookEventName": "PreToolUse",' \
+            ' "permissionDecision": "ask"' \
+            '}}'
+        exit 0
+        ;;
+    *'make home-'*|*'make system-'*)
+        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."' \
+                '}}'
+            exit 0
+        fi
+        $GUIX_BIN/echo \
+            '{"hookSpecificOutput": {' \
+            ' "hookEventName": "PreToolUse",' \
+            ' "permissionDecision": "ask"' \
+            '}}'
+        exit 0
+        ;;
+esac
diff --git a/.claude/hooks/block-worktrees b/.claude/hooks/block-worktrees
new file mode 100755 (executable)
index 0000000..7cec6cf
--- /dev/null
@@ -0,0 +1,72 @@
+#!/bin/sh
+# Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
+# License: GPL-3.0 or later.
+#
+# block-worktrees - Block workspace switches.
+
+tool=$($GUIX_BIN/sed --quiet \
+    's/.*"tool_name"[[:space:]]*:[[:space:]]*"\([^"]\+\)".*/\1/p')
+case "$tool" in
+Bash)
+    # Deny git checkout/switch.
+    if $GUIX_BIN/grep -q '"command".*"git checkout"'; then
+        $GUIX_BIN/echo \
+            '{"hookSpecificOutput": {' \
+            ' "hookEventName": "PreToolUse",' \
+            ' "permissionDecision": "deny",' \
+            ' "systemMessage": "🚫 Branch switching disabled."' \
+            '}}'
+        exit 0
+    fi
+    if $GUIX_BIN/grep -q '"command".*"git switch"'; then
+        $GUIX_BIN/echo \
+            '{"hookSpecificOutput": {' \
+            ' "hookEventName": "PreToolUse",' \
+            ' "permissionDecision": "deny",' \
+            ' "systemMessage": "🚫 Branch switching disabled."' \
+            '}}'
+        exit 0
+    fi
+    ;;
+
+EnterWorktree)
+    # Allow only from the main worktree.
+    if $GUIX_BIN/echo "$PWD" | $GUIX_BIN/grep -q '/worktrees/'; then
+        $GUIX_BIN/echo \
+            '{"hookSpecificOutput": {' \
+            ' "hookEventName": "PreToolUse",' \
+            ' "permissionDecision": "deny",' \
+            ' "systemMessage": "🚫 Already in a worktree."' \
+            '}}'
+        exit 0
+    fi
+
+    $HOME/.guix-home/profile/bin/dunstify \
+        --urgency normal --timeout 5000 \
+        --appname "Claude Code" \
+        "Claude Code" \
+        "🚀 Switched to worktree."
+    ;;
+
+ExitWorktree)
+    # Only meaningful from within a worktree.
+    if ! $GUIX_BIN/echo "$PWD" | $GUIX_BIN/grep -q '/worktrees/'; then
+        exit 0
+    fi
+
+    branch=$($HOME/.guix-home/profile/bin/git -C "$PWD" \
+        branch --show-current 2>/dev/null)
+
+    if $HOME/.guix-home/profile/bin/git -C "$CLAUDE_PROJECT_DIR" \
+        merge-base --is-ancestor "$branch" main 2>/dev/null; then
+        exit 0
+    fi
+
+    $GUIX_BIN/echo \
+        '{"hookSpecificOutput": {' \
+        ' "hookEventName": "PreToolUse",' \
+        ' "permissionDecision": "deny",' \
+        ' "systemMessage": "🚫 Use /merge-worktree to merge to main."' \
+        '}}'
+    ;;
+esac
diff --git a/.claude/hooks/create-worktree b/.claude/hooks/create-worktree
new file mode 100755 (executable)
index 0000000..a5bd6e3
--- /dev/null
@@ -0,0 +1,31 @@
+#!/bin/sh
+# Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
+# License: GPL-3.0 or later.
+#
+# create-worktree - Enforce worktree entry on main.
+#
+# Matches all tool types (*). Denies every tool call (except EnterWorktree)
+# unless already in this session's worktree.
+
+# Let EnterWorktree pass through.
+if $GUIX_BIN/grep -q '"tool_name"[[:space:]]*:[[:space:]]*"EnterWorktree"'; then
+    exit 0
+fi
+
+# Allow if already is this session's worktree.
+if $GUIX_BIN/echo "$PWD" | $GUIX_BIN/grep --quiet \
+    "/worktrees/${CLAUDE_CODE_SESSION_ID}"; then
+    exit 0
+fi
+
+# Deny -- must EnterWorktree with this session's ID as the name.
+${GUIX_BIN}/echo \
+    '{"hookSpecificOutput": {' \
+    '"hookEventName": "PreToolUse",' \
+    '"permissionDecision": "deny",' \
+    '"permissionDecisionReason": "Not in the right worktree. Call' \
+      'EnterWorktree with name='"$CLAUDE_CODE_SESSION_ID"' to create and' \
+      'switch into a worktree, then retry. You MUST use this session name.",' \
+    '"systemMessage": "🚧 Not in the right worktree. Call EnterWorktree with' \
+      'name='"$CLAUDE_CODE_SESSION_ID"'."' \
+    '}}'
diff --git a/.claude/hooks/guard-commands b/.claude/hooks/guard-commands
deleted file mode 100755 (executable)
index 3962a77..0000000
+++ /dev/null
@@ -1,89 +0,0 @@
-#!/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
index 2c30fb5718c8dc291cacdd5b1b210dad7d384548..8353c12c0fdd3e85311b9de824eda07450534872 100644 (file)
@@ -9,12 +9,51 @@
   },
   "hooks": {
     "PreToolUse": [
+      {
+        "matcher": "*",
+        "hooks": [
+          {
+            "type": "command",
+            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/create-worktree"
+          }
+        ]
+      },
+      {
+        "matcher": "Bash",
+        "if": "Bash(guix *)",
+        "hooks": [
+          {
+            "type": "command",
+            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-builds"
+          }
+        ]
+      },
       {
         "matcher": "Bash",
+        "if": "Bash(make *)",
+        "hooks": [
+          {
+            "type": "command",
+            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-builds"
+          }
+        ]
+      },
+      {
+        "matcher": "Bash",
+        "if": "Bash(git *)",
+        "hooks": [
+          {
+            "type": "command",
+            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-worktrees"
+          }
+        ]
+      },
+      {
+        "matcher": "EnterWorktree|ExitWorktree",
         "hooks": [
           {
             "type": "command",
-            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/guard-commands"
+            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-worktrees"
           }
         ]
       }
diff --git a/README b/README
index 02dc15cddf39e10308b1b55eee31ae757b302a71..400401e8b18c79a867e23a960ae59715a94bccb3 100644 (file)
--- a/README
+++ b/README
@@ -36,3 +36,10 @@ TODO
   2. Update the version field in conf/home/claude-code.scm.
   3. Add claude-code at the end, run guix build -f conf/home/claude-code.scm and
      get the correct hash from the "hash mismatch" error message.
+
+* Avoid merge conflicts when running multiple Claude Code instances.
+
+  1. Work on a separate branch per instance (e.g. claude-<session_id>).
+  2. Or use EnterWorktree for automatic isolation.
+  3. Or add a PreToolUse hook that auto-creates/switches to a per-session
+     branch before Edit/Write.