From: Jakub Czajka Date: Mon, 6 Jul 2026 10:23:44 +0000 (+0200) Subject: [ai] Replace guard-commands with block-builds, block-worktrees, and create-worktree... X-Git-Url: https://git.ekhem.eu.org/?a=commitdiff_plain;h=f8a4bd066159775772bad4a3d27d24c35def797e;p=guix.git [ai] Replace guard-commands with block-builds, block-worktrees, and create-worktree hooks. 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. --- diff --git a/.claude/hooks/block-builds b/.claude/hooks/block-builds new file mode 100755 index 0000000..253b13c --- /dev/null +++ b/.claude/hooks/block-builds @@ -0,0 +1,62 @@ +#!/bin/sh +# Copyright (c) 2026 Jakub Czajka +# 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 index 0000000..7cec6cf --- /dev/null +++ b/.claude/hooks/block-worktrees @@ -0,0 +1,72 @@ +#!/bin/sh +# Copyright (c) 2026 Jakub Czajka +# 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 index 0000000..a5bd6e3 --- /dev/null +++ b/.claude/hooks/create-worktree @@ -0,0 +1,31 @@ +#!/bin/sh +# Copyright (c) 2026 Jakub Czajka +# 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 index 3962a77..0000000 --- a/.claude/hooks/guard-commands +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/sh -# Copyright (c) 2026 Jakub Czajka -# 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/settings.json b/.claude/settings.json index 2c30fb5..8353c12 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -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 02dc15c..400401e 100644 --- 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-). + 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.