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