--- /dev/null
+---
+name: merge-worktree
+description: Merge a workspace branch into main.
+---
+
+# merge-worktree
+
+Merge a worktree branch into main, rebasing for linear history. Can
+also recover from a deleted worktree.
+
+## Instructions
+
+This command must be invoked from within a worktree (the pre-tool-use
+hooks enforce this). When the user asks to merge a workspace,
+integrate branch changes, or "commit to main":
+
+1. **Show available workspaces**:
+ Run `$CLAUDE_PROJECT_DIR/.claude/scripts/branch-summary` to display
+ a markdown table of all non-main branches and their commits.
+
+2. **Ask which workspace to merge**:
+ Use AskUserQuestion (single-select). Present the workspace branch
+ names from step 1 as options. If only one non-main branch exists,
+ ask to confirm merging that one.
+
+3. **Execute the merge**:
+ Run `$CLAUDE_PROJECT_DIR/.claude/scripts/merge-worktree
+ <selected-branch>`. The script checks for conflicts without
+ touching the working tree. Handle its exit code:
+
+ - **0 (success):** The merge completed cleanly. Proceed to step 5.
+
+ - **1 (error):** Report the error to the user and stop.
+
+ - **2 (diverged):** The branch has diverged from main, so
+ fast-forward is not possible. Ask the user with a single-select
+ AskUserQuestion: "Branch <name> has diverged from main. Force-reset
+ main to <name>?" Present "Force reset" and "Skip" as options.
+ If they choose "Force reset", re-run the merge-worktree script
+ with `--force` and proceed to step 5. If they choose "Skip", stop.
+
+ - **3 (conflicts detected):** The script found merge conflicts.
+ Proceed to step 4.
+
+ If the worktree directory was deleted out-of-band, the script
+ auto-detects this and enters recovery mode (prunes stale
+ registration, deletes branch).
+
+4. **Resolve merge conflicts**:
+ The script detected that a merge would cause conflicts. Use
+ AskUserQuestion (single-select) with three options:
+
+ - **"Force reset (override)":** Re-run the merge-worktree script
+ as `merge-worktree <selected-branch> --force` to reset main to
+ the branch state. After it succeeds, proceed to step 5.
+
+ - **"Agent resolves":** In the main worktree at
+ `$CLAUDE_PROJECT_DIR`, merge the branch manually:
+ 1. `git checkout main`
+ 2. `git merge <selected-branch>`
+ 3. Resolve all conflicts, then commit.
+ After merging, proceed to step 5.
+
+ - **"Resolve manually":** Instruct the user to resolve conflicts
+ in the main worktree at `$CLAUDE_PROJECT_DIR`:
+ 1. `git checkout main`
+ 2. `git merge <selected-branch>`
+ 3. Fix conflicts and commit.
+ After they confirm, proceed to step 5.
+
+5. **Return to the main project directory**:
+ Call the `ExitWorktree` tool with `action: "keep"`. This restores
+ the session's PWD to the main project directory while preserving
+ the worktree on disk for later manual cleanup.
\ No newline at end of file
--- /dev/null
+#!/bin/sh
+# Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
+# License: GPL-3.0 or later.
+#
+# block-worktrees — Enforce worktree isolation.
+#
+# Matches all tool types (* — runs after create-worktree PWD gate).
+# Only active when already inside a worktree.
+
+if ! echo "$PWD" | grep -q '/worktrees/'
+then
+ exit 0
+fi
+
+stdin=$(cat)
+tool=$(echo "$stdin" \
+ | sed -n 's/.*"tool_name"[[:space:]]*:[[:space:]]*"\([^"]\+\)".*/\1/p')
+worktree="$CLAUDE_PROJECT_DIR/.claude/worktrees/$CLAUDE_CODE_SESSION_ID"
+
+die() {
+ echo \
+ '{"hookSpecificOutput":{' \
+ '"permissionDecision":"deny",' \
+ '"permissionDecisionReason":"'"$1"'"}}'
+ exit 0
+}
+
+case "$tool" in
+EnterWorktree)
+ die "Already in a worktree." ;;
+
+ExitWorktree)
+ action=$(echo "$stdin" \
+ | sed -n 's/.*"action"[[:space:]]*:[[:space:]]*"\([^"]\+\)".*/\1/p')
+ if [ "$action" = "remove" ]
+ then
+ die "Worktree deletion is disabled. Use" \
+ "ExitWorktree with action: keep instead."
+ fi
+ ;;
+
+Write|Edit)
+ path=$(echo "$stdin" \
+ | sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\([^"]\+\)".*/\1/p')
+ case "$path" in
+ "$CLAUDE_PROJECT_DIR"*)
+ if ! echo "$path" | grep -Fq "$worktree"
+ then
+ die "Write blocked: target outside worktree."
+ fi
+ ;;
+ esac
+ ;;
+
+Bash)
+ cmd=$(echo "$stdin" \
+ | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"]\+\)".*/\1/p')
+
+ case "$cmd" in
+ *"git checkout"*|*"git switch"*)
+ die "Branch switching disabled." ;;
+
+ *"git worktree remove"*|*"git worktree prune"*)
+ if ! echo "$cmd" | grep -q 'merge-worktree'
+ then die "Git on main blocked. Use /merge-worktree."
+ fi ;;
+
+ *"$CLAUDE_PROJECT_DIR"*|\
+ *'$CLAUDE_PROJECT_DIR'*|\
+ *'${CLAUDE_PROJECT_DIR}'*)
+ if echo "$cmd" | grep -Fq "$worktree"
+ then : # references own worktree — allowed
+ elif echo "$cmd" | grep -q 'merge-worktree'
+ then : # merge-worktree — allowed
+ else die "Blocked: reference to main project."
+ fi ;;
+ esac
+ ;;
+
+esac
+
+exit 0
\ No newline at end of file
--- /dev/null
+#!/bin/sh
+# Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
+# License: GPL-3.0 or later.
+#
+# merge-worktree — Merge a worktree branch into main, or recover
+# from a deleted worktree.
+#
+# This script operates on the main worktree (CLAUDE_PROJECT_DIR) as a
+# subprocess, independent of the session's current directory. When the
+# branch is based on main it rebases for linear history then
+# fast-forwards. When the branch has diverged (e.g. after squashing)
+# it can reset main with --force.
+#
+# The script does NOT remove the worktree or branch — that is handled
+# by ExitWorktree (called by the merge-worktree skill after the merge).
+# A subprocess cannot change the parent session's PWD, so letting the
+# harness handle cleanup ensures the session returns to the main
+# project directory.
+#
+# Usage:
+# merge-worktree <branch-name> [--force] # normal merge
+# merge-worktree <branch-name> --recover # cleanup after deletion
+# merge-worktree --list-stale # list orphaned worktrees
+#
+# Exit codes:
+# 0 — success
+# 1 — error (validation, unclean worktree, rebase failure, etc.)
+# 2 — branch diverged from main, needs --force
+# 3 — merge conflicts detected, needs --force or manual resolution
+
+set -eu
+
+PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
+
+# ── --list-stale ───────────────────────────────────────
+
+if [ "${1:-}" = "--list-stale" ]
+then
+ echo "=== Orphaned worktree branches ==="
+ found=0
+ for b in $(git -C "$PROJECT_DIR" branch \
+ | sed 's/^[* ]*//' | grep '^worktree-'); do
+ wt_path=$(git -C "$PROJECT_DIR" worktree list \
+ | grep -F "[$b]" | awk '{print $1}' || true)
+ if [ -z "$wt_path" ] || [ ! -d "$wt_path" ]
+ then
+ echo " $b"
+ found=1
+ fi
+ done
+ if [ "$found" -eq 0 ]
+ then
+ echo " (none)"
+ fi
+ exit 0
+fi
+
+# ── Arguments ─────────────────────────────────────────
+
+branch="${1:-}"
+flag="${2:-}"
+
+if [ -z "$branch" ]
+then
+ echo "Usage: merge-worktree <branch> [--force] [--recover]" >&2
+ echo " merge-worktree --list-stale" >&2
+ exit 1
+fi
+
+if [ "$branch" = "main" ]; then
+ echo "Error: Cannot merge main into itself." >&2
+ exit 1
+fi
+
+# Validate branch.
+if ! git -C "$PROJECT_DIR" rev-parse \
+ --verify "$branch" >/dev/null 2>&1
+then
+ echo "Error: Branch '$branch' does not exist." >&2
+ exit 1
+fi
+
+# Find worktree path.
+worktree_path=$(git -C "$PROJECT_DIR" worktree list \
+ | grep -F "[$branch]" | awk '{print $1}')
+
+# ── Explicit recovery ─────────────────────────────────
+
+if [ "$flag" = "--recover" ]
+then
+ echo "=== Recovery: worktree '$branch' ==="
+ git -C "$PROJECT_DIR" worktree prune 2>/dev/null || true
+ git -C "$PROJECT_DIR" branch -D "$branch" 2>/dev/null || true
+ echo "=== Recovery complete: '$branch' cleaned up ==="
+ exit 0
+fi
+
+# ── Auto-detect: worktree directory missing ───────────
+
+if [ -z "$worktree_path" ] || [ ! -d "$worktree_path" ]
+then
+ echo "=== Worktree directory missing — entering recovery mode ==="
+ git -C "$PROJECT_DIR" worktree prune 2>/dev/null || true
+ git -C "$PROJECT_DIR" branch -D "$branch" 2>/dev/null || true
+ echo "=== Recovery complete: '$branch' cleaned up ==="
+ exit 0
+fi
+
+# Check the worktree itself is clean.
+if [ -n "$(git -C "$worktree_path" status --porcelain)" ]
+then
+ echo "Error: Worktree has uncommitted changes." >&2
+ echo " Commit or stash them first, then re-run." >&2
+ exit 1
+fi
+
+# Check main worktree is clean.
+if [ -n "$(git -C "$PROJECT_DIR" \
+ status --porcelain --untracked-files=no)" ]
+then
+ echo "Error: Main worktree has uncommitted changes." >&2
+ echo " Commit or stash them first." >&2
+ exit 1
+fi
+
+# Ensure main is restored on any exit (rebase failure,
+# interruption).
+trap 'git -C "$PROJECT_DIR" checkout main \
+ 2>/dev/null || true' EXIT
+
+# Fetch from origin.
+echo "=== Fetching from origin ==="
+git -C "$PROJECT_DIR" fetch origin
+
+# Determine whether this is a normal fast-forward or a
+# divergent branch that needs a reset.
+if git -C "$PROJECT_DIR" merge-base --is-ancestor \
+ main "$branch" 2>/dev/null
+then
+ # ── Normal case: branch is based on main ──
+
+ # ── Conflict pre-check ──
+ echo "=== Checking for merge conflicts ==="
+ merge_result=$(git -C "$PROJECT_DIR" merge-tree \
+ --write-tree main "$branch" 2>&1)
+ conflict_files=$(echo "$merge_result" \
+ | grep -E '^[0-7]{6} [0-9a-f]{40} [123] ' \
+ | awk '{print $4}' | sort -u)
+ if [ -n "$conflict_files" ]
+ then
+ echo "Conflicts detected between main and '$branch'." >&2
+ echo "Conflicting files:" >&2
+ echo "$conflict_files" >&2
+ if [ "$flag" = "--force" ]
+ then
+ echo "=== Resetting main to '$branch' (override) ==="
+ current=$(git -C "$PROJECT_DIR" branch --show-current)
+ if [ "$current" != "main" ]
+ then
+ git -C "$PROJECT_DIR" checkout main
+ fi
+ git -C "$PROJECT_DIR" reset --hard "$branch"
+ trap '' EXIT
+ echo "=== Main reset to '$branch' ==="
+ exit 0
+ else
+ echo "Re-run with --force to reset main or resolve" \
+ "manually." >&2
+ exit 3
+ fi
+ fi
+
+ # Rebase branch onto main.
+ echo "=== Rebasing $branch onto main ==="
+ if ! git -C "$PROJECT_DIR" \
+ rebase main "$branch"
+ then
+ rc=$?
+ echo "Error: Rebase failed (conflicts)." >&2
+ echo " Resolve them in the main worktree:" >&2
+ echo " cd $PROJECT_DIR" >&2
+ echo " git status # see conflicted files" >&2
+ echo " # fix conflicts, then:" >&2
+ echo " git rebase --continue" >&2
+ echo " git checkout main" >&2
+ echo " git merge --ff-only $branch" >&2
+ exit $rc
+ fi
+
+ # Switch back to main if needed.
+ current=$(git -C "$PROJECT_DIR" branch --show-current)
+ if [ "$current" != "main" ]
+ then
+ echo "=== Switching to main ==="
+ git -C "$PROJECT_DIR" checkout main
+ fi
+
+ # Fast-forward merge.
+ echo "=== Merging $branch into main (fast-forward) ==="
+ if ! git -C "$PROJECT_DIR" \
+ merge --ff-only "$branch"
+ then
+ rc=$?
+ echo "Error: Fast-forward merge failed." >&2
+ echo " This should not happen after a successful" \
+ "rebase." >&2
+ echo " Try: git -C $PROJECT_DIR merge $branch" >&2
+ exit $rc
+ fi
+
+ echo "=== Branch '$branch' successfully merged into" \
+ "main ==="
+else
+ # ── Divergent case: branch was rewritten ──
+
+ if [ "$flag" = "--force" ]
+ then
+ echo "=== Resetting main to $branch ==="
+ current=$(git -C "$PROJECT_DIR" branch --show-current)
+ if [ "$current" != "main" ]
+ then
+ git -C "$PROJECT_DIR" checkout main
+ fi
+ git -C "$PROJECT_DIR" reset --hard "$branch"
+ trap '' EXIT
+ echo "=== Main reset to '$branch' ==="
+ else
+ echo "Branch '$branch' has diverged from main." >&2
+ echo "Fast-forward merge is not possible." >&2
+ echo "Re-run with --force to reset main to this" \
+ "branch." >&2
+ exit 2
+ fi
+fi
+
+echo "=== Branch '$branch' merged into main. ==="
+echo "=== Use ExitWorktree to return to main. ==="
\ No newline at end of file