From 6cb81bb6052f7407bfb5111246e3e84cc96ec74e Mon Sep 17 00:00:00 2001 From: igerber Date: Sun, 1 Feb 2026 08:01:27 -0500 Subject: [PATCH 1/3] Add git worktree management slash commands Add /worktree-new, /worktree-rm, and /worktree-ls commands for managing parallel development worktrees directly from Claude Code sessions. Each command resolves paths dynamically from the main worktree root, validates input, and handles edge cases (existing branches, uncommitted changes). Co-Authored-By: Claude Opus 4.5 --- .claude/commands/worktree-ls.md | 35 +++++++++++ .claude/commands/worktree-new.md | 98 +++++++++++++++++++++++++++++++ .claude/commands/worktree-rm.md | 99 ++++++++++++++++++++++++++++++++ 3 files changed, 232 insertions(+) create mode 100644 .claude/commands/worktree-ls.md create mode 100644 .claude/commands/worktree-new.md create mode 100644 .claude/commands/worktree-rm.md diff --git a/.claude/commands/worktree-ls.md b/.claude/commands/worktree-ls.md new file mode 100644 index 00000000..e5affd02 --- /dev/null +++ b/.claude/commands/worktree-ls.md @@ -0,0 +1,35 @@ +--- +description: List all active git worktrees with status +argument-hint: "" +--- + +# List Git Worktrees + +## Instructions + +### 1. Get Worktree List + +```bash +git worktree list +``` + +### 2. Check Status of Each + +For each worktree path returned, check for uncommitted changes: + +```bash +git -C status --porcelain | wc -l +``` + +### 3. Display Results + +Show a table with: +- **Path** +- **Branch** +- **Commit** (short hash) +- **Status**: "clean" or "N uncommitted changes" + +If there's only the main worktree, add: +``` +No additional worktrees. Use /worktree-new to create one. +``` diff --git a/.claude/commands/worktree-new.md b/.claude/commands/worktree-new.md new file mode 100644 index 00000000..3929f48f --- /dev/null +++ b/.claude/commands/worktree-new.md @@ -0,0 +1,98 @@ +--- +description: Create a new git worktree with full dev environment for parallel work +argument-hint: " [base-branch]" +--- + +# Create Git Worktree + +Create an isolated worktree for parallel development. Arguments: $ARGUMENTS + +## Instructions + +### 1. Parse Arguments + +Parse `$ARGUMENTS` to extract: +- **name** (required): First argument — used as both directory suffix and branch name +- **base-ref** (optional): Second argument — existing branch or ref to check out + instead of creating a new branch + +If no name is provided, abort with: +``` +Error: Name required. Usage: /worktree-new [base-branch] +Example: /worktree-new feature-bacon-fix +``` + +Validate that **name** contains only `[a-zA-Z0-9._-]`. If it contains spaces, +slashes, or other shell metacharacters, abort: +``` +Error: Name must contain only letters, digits, dots, hyphens, and underscores. +Got: +``` + +### 2. Resolve Paths + +Derive paths dynamically (do NOT hardcode the repo name): + +```bash +MAIN_ROOT="$(git worktree list --porcelain | head -1 | sed 's/^worktree //')" +REPO_NAME="$(basename "$MAIN_ROOT")" +PARENT_DIR="$(dirname "$MAIN_ROOT")" +WORKTREE_PATH="${PARENT_DIR}/${REPO_NAME}-" +``` + +Use `$WORKTREE_PATH` (the absolute path) for all subsequent commands. + +### 3. Validate + +```bash +git worktree list +``` + +- If a worktree already exists at `$WORKTREE_PATH`, abort with an error. +- If a branch named `` already exists and no base-ref was given, ask the user + whether to check out that existing branch or pick a different name. + +### 4. Create the Worktree + +```bash +# If base-ref provided: +git worktree add "$WORKTREE_PATH" + +# If no base-ref (create new branch from current HEAD): +git worktree add -b "$WORKTREE_PATH" +``` + +### 5. Set Up Python Environment + +Note to user: dependency installation may take a moment on a fresh venv. + +```bash +python3 -m venv "$WORKTREE_PATH/.venv" +"$WORKTREE_PATH/.venv/bin/pip" install --upgrade pip +"$WORKTREE_PATH/.venv/bin/pip" install -e "$WORKTREE_PATH[dev]" +``` + +Do NOT use `-q` — let pip output stream so the user sees progress. + +### 6. Build Rust Backend (best-effort) + +Use `--manifest-path` to avoid changing directories: + +```bash +"$WORKTREE_PATH/.venv/bin/maturin" develop --manifest-path "$WORKTREE_PATH/Cargo.toml" +``` + +If maturin is not installed in the venv or the build fails, note that pure-Python +mode will be used and continue. This is not an error. + +### 7. Report + +Print: + +``` +Worktree ready: $WORKTREE_PATH +Branch: + +To start working: + cd $WORKTREE_PATH && source .venv/bin/activate && claude +``` diff --git a/.claude/commands/worktree-rm.md b/.claude/commands/worktree-rm.md new file mode 100644 index 00000000..37b06d15 --- /dev/null +++ b/.claude/commands/worktree-rm.md @@ -0,0 +1,99 @@ +--- +description: Remove a git worktree and optionally delete its branch +argument-hint: "" +--- + +# Remove Git Worktree + +Remove an existing worktree. Arguments: $ARGUMENTS + +## Instructions + +### 1. Parse Arguments + +Extract **name** from `$ARGUMENTS`. If empty, abort: +``` +Error: Name required. Usage: /worktree-rm +Tip: Run /worktree-ls to see active worktrees. +``` + +Validate that **name** contains only `[a-zA-Z0-9._-]`. If not, abort: +``` +Error: Name must contain only letters, digits, dots, hyphens, and underscores. +Got: +``` + +### 2. Resolve Paths + +```bash +MAIN_ROOT="$(git worktree list --porcelain | head -1 | sed 's/^worktree //')" +REPO_NAME="$(basename "$MAIN_ROOT")" +PARENT_DIR="$(dirname "$MAIN_ROOT")" +WORKTREE_PATH="${PARENT_DIR}/${REPO_NAME}-" +``` + +### 3. Validate + +```bash +git worktree list +``` + +If no worktree exists at `$WORKTREE_PATH`, abort: +``` +Error: No worktree found at $WORKTREE_PATH +Active worktrees: +``` + +### 4. Check for Uncommitted Work + +Capture the branch name first (needed for step 6, before the worktree is removed): + +```bash +BRANCH=$(git -C "$WORKTREE_PATH" rev-parse --abbrev-ref HEAD) +``` + +Then check for uncommitted changes: + +```bash +git -C "$WORKTREE_PATH" status --porcelain +``` + +If there are uncommitted changes, warn the user with AskUserQuestion: +- Option 1: "Abort — I have unsaved work" +- Option 2: "Remove anyway — discard changes" + +**If the user chooses "Abort", stop immediately. Do NOT continue to step 5.** + +### 5. Remove the Worktree + +If the worktree had uncommitted changes and the user chose "Remove anyway": +```bash +git worktree remove "$WORKTREE_PATH" --force +``` + +If the worktree was clean (step 4 found no changes): +```bash +git worktree remove "$WORKTREE_PATH" +``` + +### 6. Try to Delete the Branch + +Only attempt branch deletion if `$BRANCH` equals `` (meaning we created it +via `/worktree-new ` without a base-ref). If the branch is something else +(e.g., `feature/existing-branch`), skip deletion — the user didn't create it. + +```bash +git branch -d "$BRANCH" +``` + +Let the output print naturally: +- If the branch was merged, it will be deleted and git prints a confirmation. +- If not fully merged, git prints a warning — relay that to the user + (suggest `git branch -D "$BRANCH"` if they want to force-delete). + +### 7. Report + +``` +Removed worktree: $WORKTREE_PATH +[Branch status from step 6] +``` From e72c7aa7f2ffbbcb30b2b2d4764514e675c7f6fc Mon Sep 17 00:00:00 2001 From: igerber Date: Sun, 1 Feb 2026 08:08:54 -0500 Subject: [PATCH 2/3] Address PR #120 review: harden worktree commands against injection and brittle parsing - Validate base-ref input (character allowlist + git rev-parse --verify) before shell use - Disallow leading-dash names in worktree-new and worktree-rm to prevent option injection - Add -- separators to git worktree add and git branch -d commands - Switch worktree-ls to --porcelain output for reliable parsing (handles spaces in paths) - Specify exact git command for "use existing branch" path in worktree-new Co-Authored-By: Claude Opus 4.5 --- .claude/commands/worktree-ls.md | 20 +++++++++++++++----- .claude/commands/worktree-new.md | 30 +++++++++++++++++++++++++----- .claude/commands/worktree-rm.md | 9 +++++---- 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/.claude/commands/worktree-ls.md b/.claude/commands/worktree-ls.md index e5affd02..84709b1f 100644 --- a/.claude/commands/worktree-ls.md +++ b/.claude/commands/worktree-ls.md @@ -9,24 +9,34 @@ argument-hint: "" ### 1. Get Worktree List +Use porcelain format for reliable, machine-parseable output: + ```bash -git worktree list +git worktree list --porcelain ``` -### 2. Check Status of Each +### 2. Parse and Check Status + +Parse the porcelain output into records. Each worktree is a block of lines +separated by a blank line, with fields: +- `worktree ` — the absolute path +- `HEAD ` — the full commit hash +- `branch refs/heads/` — the branch (or `detached` if HEAD is detached) -For each worktree path returned, check for uncommitted changes: +For each worktree path, check for uncommitted changes: ```bash -git -C status --porcelain | wc -l +git -C "$WORKTREE_PATH" status --porcelain | wc -l ``` +Quote `$WORKTREE_PATH` in all commands to handle paths with spaces. + ### 3. Display Results Show a table with: - **Path** - **Branch** -- **Commit** (short hash) +- **Commit** (short hash — first 7 characters of HEAD) - **Status**: "clean" or "N uncommitted changes" If there's only the main worktree, add: diff --git a/.claude/commands/worktree-new.md b/.claude/commands/worktree-new.md index 3929f48f..a2b1f74d 100644 --- a/.claude/commands/worktree-new.md +++ b/.claude/commands/worktree-new.md @@ -22,13 +22,28 @@ Error: Name required. Usage: /worktree-new [base-branch] Example: /worktree-new feature-bacon-fix ``` -Validate that **name** contains only `[a-zA-Z0-9._-]`. If it contains spaces, -slashes, or other shell metacharacters, abort: +Validate that **name** starts with a letter or digit, followed by `[a-zA-Z0-9._-]`. +If it starts with `-` or contains spaces, slashes, or other shell metacharacters, abort: ``` -Error: Name must contain only letters, digits, dots, hyphens, and underscores. +Error: Name must start with a letter or digit and contain only letters, digits, dots, hyphens, and underscores. Got: ``` +If **base-ref** is provided, apply the same character validation (must match +`^[a-zA-Z0-9][a-zA-Z0-9._/-]*$` — slashes are allowed for refs like `origin/main`). +Then verify the ref exists: + +```bash +git rev-parse --verify --quiet "$BASE_REF" +``` + +If verification fails, abort: +``` +Error: Ref not found: +Available branches: + +``` + ### 2. Resolve Paths Derive paths dynamically (do NOT hardcode the repo name): @@ -51,15 +66,20 @@ git worktree list - If a worktree already exists at `$WORKTREE_PATH`, abort with an error. - If a branch named `` already exists and no base-ref was given, ask the user whether to check out that existing branch or pick a different name. + If the user chooses to use the existing branch: + ```bash + git worktree add -- "$WORKTREE_PATH" "" + ``` + Then skip step 4 and continue to step 5. ### 4. Create the Worktree ```bash # If base-ref provided: -git worktree add "$WORKTREE_PATH" +git worktree add -- "$WORKTREE_PATH" "$BASE_REF" # If no base-ref (create new branch from current HEAD): -git worktree add -b "$WORKTREE_PATH" +git worktree add -b "" -- "$WORKTREE_PATH" ``` ### 5. Set Up Python Environment diff --git a/.claude/commands/worktree-rm.md b/.claude/commands/worktree-rm.md index 37b06d15..5102ddcc 100644 --- a/.claude/commands/worktree-rm.md +++ b/.claude/commands/worktree-rm.md @@ -17,9 +17,10 @@ Error: Name required. Usage: /worktree-rm Tip: Run /worktree-ls to see active worktrees. ``` -Validate that **name** contains only `[a-zA-Z0-9._-]`. If not, abort: +Validate that **name** starts with a letter or digit, followed by `[a-zA-Z0-9._-]`. +If it starts with `-` or contains invalid characters, abort: ``` -Error: Name must contain only letters, digits, dots, hyphens, and underscores. +Error: Name must start with a letter or digit and contain only letters, digits, dots, hyphens, and underscores. Got: ``` @@ -83,13 +84,13 @@ via `/worktree-new ` without a base-ref). If the branch is something else (e.g., `feature/existing-branch`), skip deletion — the user didn't create it. ```bash -git branch -d "$BRANCH" +git branch -d -- "$BRANCH" ``` Let the output print naturally: - If the branch was merged, it will be deleted and git prints a confirmation. - If not fully merged, git prints a warning — relay that to the user - (suggest `git branch -D "$BRANCH"` if they want to force-delete). + (suggest `git branch -D -- "$BRANCH"` if they want to force-delete). ### 7. Report From fdf44d07989788bfd789ec86dafd1af073b3fe29 Mon Sep 17 00:00:00 2001 From: igerber Date: Sun, 1 Feb 2026 08:22:03 -0500 Subject: [PATCH 3/3] Fix worktree-new: always create branch from base-ref and guard against checked-out branches - Step 4: Use `git worktree add -b` when base-ref is provided so the command creates a new branch instead of checking out the ref directly. This avoids detached HEAD for tags/remote refs and failures when the ref is already checked out elsewhere. - Step 3: Before offering to reuse an existing branch, check `git worktree list --porcelain` to detect if the branch is already checked out in another worktree and abort with a clear message. - Step 1: Update base-ref description to say "branch from" instead of "check out". Co-Authored-By: Claude Opus 4.5 --- .claude/commands/worktree-new.md | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/.claude/commands/worktree-new.md b/.claude/commands/worktree-new.md index a2b1f74d..0bb52283 100644 --- a/.claude/commands/worktree-new.md +++ b/.claude/commands/worktree-new.md @@ -13,8 +13,8 @@ Create an isolated worktree for parallel development. Arguments: $ARGUMENTS Parse `$ARGUMENTS` to extract: - **name** (required): First argument — used as both directory suffix and branch name -- **base-ref** (optional): Second argument — existing branch or ref to check out - instead of creating a new branch +- **base-ref** (optional): Second argument — existing branch, tag, or ref to branch + from (creates branch `` starting at that ref) If no name is provided, abort with: ``` @@ -64,19 +64,26 @@ git worktree list ``` - If a worktree already exists at `$WORKTREE_PATH`, abort with an error. -- If a branch named `` already exists and no base-ref was given, ask the user - whether to check out that existing branch or pick a different name. - If the user chooses to use the existing branch: - ```bash - git worktree add -- "$WORKTREE_PATH" "" - ``` - Then skip step 4 and continue to step 5. +- If a branch named `` already exists and no base-ref was given: + - First check if the branch is already checked out in a worktree + (parse `git worktree list --porcelain` for a `branch refs/heads/` line). + - If checked out elsewhere, abort: + ``` + Error: Branch '' is already checked out in worktree at . + Use a different name or remove that worktree first. + ``` + - Otherwise, ask the user whether to check out that existing branch + or pick a different name. If the user chooses to use it: + ```bash + git worktree add -- "$WORKTREE_PATH" "" + ``` + Then skip step 4 and continue to step 5. ### 4. Create the Worktree ```bash -# If base-ref provided: -git worktree add -- "$WORKTREE_PATH" "$BASE_REF" +# If base-ref provided (create new branch starting at base-ref): +git worktree add -b "" -- "$WORKTREE_PATH" "$BASE_REF" # If no base-ref (create new branch from current HEAD): git worktree add -b "" -- "$WORKTREE_PATH"