fix: Harden worktree feature with security and UX improvements#1
fix: Harden worktree feature with security and UX improvements#1
Conversation
Enable developers to work on multiple features simultaneously using git worktrees instead of switching branches. Features can be created in separate working directories, allowing independent development contexts. Key changes: - Add configure-worktree.sh/ps1 scripts for mode and strategy configuration - Extend create-new-feature scripts with worktree creation and fallback logic - Add read_config_value/Get-ConfigValue functions to common scripts - Extend JSON output with FEATURE_ROOT and MODE fields for AI agent context - Support nested, sibling, and custom worktree placement strategies - Add graceful fallback to branch mode on worktree creation failure - Detect and warn about uncommitted changes and orphaned worktrees Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add .specify/config.json to .gitignore to prevent committing local preferences with absolute paths - Add warning when jq is not available and config file will be overwritten with only worktree settings - Fix brittle JSON parsing in common.sh to support booleans and numbers - Add nullglob handling for empty specs directory in get_highest_from_specs - Improve fallback warning messages to clarify context switch when worktree creation fails and branch mode is used - Sync updated scripts to .specify/scripts/bash/ Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add the ability to configure git worktree mode during project initialization, allowing users to develop multiple features in parallel directories. New CLI options: - --git-mode (branch/worktree) - --worktree-strategy (sibling/nested/custom) - --worktree-path (for custom strategy) Improvements: - Interactive selection for git workflow when options not provided - Git 2.5+ version check for worktree support - Conflict detection for --no-git + --git-mode worktree - Worktree location preview before confirmation - Auto-add .worktrees/ to .gitignore for nested strategy - Prominent agent notification when worktree mode is used Worktree naming convention updated to <repo>-<branch> for sibling and custom strategies for better clarity. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use selected_script to show correct configure-worktree script path (bash vs PowerShell) in worktree mode notice - Remove duplicate file read in gitignore update logic - Standardize default worktree strategy to "sibling" across all files (CLI, bash scripts, PowerShell scripts, design doc) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previous commit incorrectly replaced all occurrences of "nested" with "sibling". This fix restores the configure-worktree scripts and only updates the default values shown when displaying configuration. "nested" remains a valid strategy option alongside "sibling" and "custom". Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update .specify/scripts/bash/configure-worktree.sh with sibling as default strategy (matching scripts/bash/ and Python init command) - Update .specify/scripts/bash/create-new-feature.sh with: - sibling as default strategy - repo_name-branch_name naming convention for sibling/custom strategies - Ensures installed scripts match source templates Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…dling - Gate pre-flight warnings (uncommitted changes, orphaned worktrees) on worktree mode only — branch-mode users no longer see irrelevant warnings - Replace silent worktree-to-branch fallbacks with clear, actionable error messages that guide users toward resolution - Fix jq injection vulnerability in configure-worktree.sh by using --arg for all user-supplied values - Fix PowerShell temp file leak in writability checks with try/finally - Add optional config file path to read_config_value to avoid redundant get_repo_root() subprocess calls - Restore HAS_GIT in PowerShell JSON output and add to bash for cross-platform consistency - Move WORKTREE_DESIGN.md into specs/001-git-worktrees/ - Bump version to 0.0.23 with CHANGELOG entry Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds comprehensive git worktree support to Spec Kit, allowing users to develop multiple features simultaneously in parallel directories rather than switching branches in a single working copy. The implementation includes security fixes (jq injection, temp file cleanup), improved error handling with actionable messages, and cross-platform support for both bash and PowerShell.
Changes:
- Added git worktree mode with three placement strategies (nested, sibling, custom) configurable via CLI and interactive prompts
- Replaced silent worktree-to-branch fallbacks with explicit error exits and actionable suggestions
- Fixed security vulnerabilities: jq injection in bash configure script and PowerShell temp file leak
- Added
FEATURE_ROOTandMODEfields to JSON output; restoredHAS_GITfield for cross-platform parity - Performance optimization: bash
read_config_valuenow accepts optional config file path to avoid redundant subprocess calls
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
src/specify_cli/__init__.py |
Added CLI options for git mode selection, validation, interactive prompts, config writing, and git version checking |
pyproject.toml |
Version bump to 0.0.23 |
scripts/bash/create-new-feature.sh |
Implemented worktree support with error handling and pre-flight checks |
scripts/bash/configure-worktree.sh |
New configuration script with jq injection fix using --arg |
scripts/bash/common.sh |
Added read_config_value with optional config file path parameter |
scripts/powershell/create-new-feature.ps1 |
Implemented worktree support with temp file cleanup via try/finally |
scripts/powershell/configure-worktree.ps1 |
New PowerShell configuration script |
scripts/powershell/common.ps1 |
Added Get-ConfigValue function |
.specify/scripts/bash/* |
Template copies of bash scripts |
templates/commands/specify.md |
Added worktree mode instructions and notification requirements for AI agents |
specs/001-git-worktrees/WORKTREE_DESIGN.md |
Design documentation for worktree feature |
specs/001-git-worktrees/CHANGELOG.md |
Feature-specific changelog |
CHANGELOG.md |
Main changelog with version 0.0.23 changes |
.gitignore |
Added .specify/config.json and .worktrees/ entries |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Write JSON manually | ||
| cat > "$CONFIG_FILE" << EOF | ||
| { | ||
| "git_mode": "$CURRENT_MODE", | ||
| "worktree_strategy": "$CURRENT_STRATEGY", | ||
| "worktree_custom_path": "$CURRENT_PATH" | ||
| } | ||
| EOF |
There was a problem hiding this comment.
The fallback JSON construction (when jq is not available) uses unquoted variable expansion in a heredoc, which could lead to shell injection if CURRENT_PATH contains special characters like quotes or backticks. While the parent directory checks mitigate some risks, paths could still contain characters that break JSON or cause security issues.
Consider using proper JSON escaping or requiring jq for worktree mode. The jq path uses --arg which is safe, but the fallback is vulnerable.
| # Write JSON manually | |
| cat > "$CONFIG_FILE" << EOF | |
| { | |
| "git_mode": "$CURRENT_MODE", | |
| "worktree_strategy": "$CURRENT_STRATEGY", | |
| "worktree_custom_path": "$CURRENT_PATH" | |
| } | |
| EOF | |
| # JSON-escape a string for safe inclusion in JSON string values | |
| json_escape() { | |
| local input=$1 | |
| local output="" | |
| local i char | |
| # Iterate over each character | |
| while IFS= read -r -n1 char; do | |
| case $char in | |
| '"') output+='\\"' ;; | |
| '\\') output+='\\\\' ;; | |
| $'\b') output+='\\b' ;; | |
| $'\f') output+='\\f' ;; | |
| $'\n') output+='\\n' ;; | |
| $'\r') output+='\\r' ;; | |
| $'\t') output+='\\t' ;; | |
| *) | |
| printf -v hex '%02X' "'$char" | |
| if (( 0x$hex < 0x20 )); then | |
| output+="\\u00$hex" | |
| else | |
| output+="$char" | |
| fi | |
| ;; | |
| esac | |
| done <<< "$input" | |
| printf '%s' "$output" | |
| } | |
| # Escape values for JSON | |
| ESC_MODE=$(json_escape "$CURRENT_MODE") | |
| ESC_STRATEGY=$(json_escape "$CURRENT_STRATEGY") | |
| ESC_PATH=$(json_escape "$CURRENT_PATH") | |
| # Write JSON manually using safely escaped values | |
| { | |
| printf '{\n' | |
| printf ' "git_mode": "%s",\n' "$ESC_MODE" | |
| printf ' "worktree_strategy": "%s",\n' "$ESC_STRATEGY" | |
| printf ' "worktree_custom_path": "%s"\n' "$ESC_PATH" | |
| printf '}\n' | |
| } > "$CONFIG_FILE" |
| $strategy = Get-ConfigValue -Key "worktree_strategy" -Default "sibling" | ||
| $customPath = Get-ConfigValue -Key "worktree_custom_path" -Default "" |
There was a problem hiding this comment.
The PowerShell Get-ConfigValue function internally calls Get-RepoRoot on every invocation (line 148 of common.ps1). While this works correctly, it's less optimal than the bash version which accepts an optional config file path parameter to avoid redundant subprocess calls.
Consider adding an optional -ConfigFile parameter to Get-ConfigValue for consistency with the bash implementation and to enable the same performance optimization.
src/specify_cli/__init__.py
Outdated
| location_desc = strategy_desc.get(selected_worktree_strategy, "in a separate directory") | ||
|
|
||
| # Use the correct script path based on selected script type | ||
| configure_script = ".specify/scripts/powershell/configure-worktree.ps1" if selected_script == "ps" else ".specify/scripts/bash/configure-worktree.sh" |
There was a problem hiding this comment.
The script path for PowerShell references .specify/scripts/powershell/configure-worktree.ps1, but based on the file structure, PowerShell scripts are located at scripts/powershell/configure-worktree.ps1 (without the .specify/ prefix). Only bash scripts are in the .specify/scripts/ directory.
This path should be scripts/powershell/configure-worktree.ps1 to match the actual file location, or alternatively, PowerShell scripts should be added to the .specify/scripts/powershell/ directory for consistency with the bash scripts.
| configure_script = ".specify/scripts/powershell/configure-worktree.ps1" if selected_script == "ps" else ".specify/scripts/bash/configure-worktree.sh" | |
| configure_script = "scripts/powershell/configure-worktree.ps1" if selected_script == "ps" else ".specify/scripts/bash/configure-worktree.sh" |
There was a problem hiding this comment.
@copilot we removed the .specify directory from the repo. This was created by using speckit for this project. items in .specify should be ignored.
scripts/bash/configure-worktree.sh
Outdated
| # Fallback without jq: construct JSON manually | ||
| # Warn user about potential data loss | ||
| if [[ -f "$CONFIG_FILE" ]]; then | ||
| >&2 echo "[specify] Warning: jq not found. Config file will be rewritten with only worktree settings." | ||
| >&2 echo "[specify] Install jq to preserve other configuration keys." | ||
| fi | ||
|
|
||
| # Read existing values | ||
| CURRENT_MODE=$(read_config_value "git_mode" "branch") | ||
| CURRENT_STRATEGY=$(read_config_value "worktree_strategy" "sibling") | ||
| CURRENT_PATH=$(read_config_value "worktree_custom_path" "") | ||
|
|
||
| # Apply updates | ||
| [[ -n "$MODE" ]] && CURRENT_MODE="$MODE" | ||
| [[ -n "$STRATEGY" ]] && CURRENT_STRATEGY="$STRATEGY" | ||
| if [[ -n "$CUSTOM_PATH" ]]; then | ||
| CURRENT_PATH="$CUSTOM_PATH" | ||
| elif [[ "$STRATEGY" == "nested" || "$STRATEGY" == "sibling" ]]; then | ||
| CURRENT_PATH="" | ||
| fi | ||
|
|
||
| # Write JSON manually | ||
| cat > "$CONFIG_FILE" << EOF | ||
| { | ||
| "git_mode": "$CURRENT_MODE", | ||
| "worktree_strategy": "$CURRENT_STRATEGY", | ||
| "worktree_custom_path": "$CURRENT_PATH" | ||
| } | ||
| EOF |
There was a problem hiding this comment.
The fallback JSON construction (when jq is not available) uses unquoted variable expansion in a heredoc, which could lead to shell injection if CURRENT_PATH contains special characters like quotes or backticks. While the parent directory checks mitigate some risks, paths could still contain characters that break JSON or cause security issues.
Consider using proper JSON escaping or requiring jq for worktree mode. The jq path uses --arg which is safe, but the fallback is vulnerable.
| # Fallback without jq: construct JSON manually | |
| # Warn user about potential data loss | |
| if [[ -f "$CONFIG_FILE" ]]; then | |
| >&2 echo "[specify] Warning: jq not found. Config file will be rewritten with only worktree settings." | |
| >&2 echo "[specify] Install jq to preserve other configuration keys." | |
| fi | |
| # Read existing values | |
| CURRENT_MODE=$(read_config_value "git_mode" "branch") | |
| CURRENT_STRATEGY=$(read_config_value "worktree_strategy" "sibling") | |
| CURRENT_PATH=$(read_config_value "worktree_custom_path" "") | |
| # Apply updates | |
| [[ -n "$MODE" ]] && CURRENT_MODE="$MODE" | |
| [[ -n "$STRATEGY" ]] && CURRENT_STRATEGY="$STRATEGY" | |
| if [[ -n "$CUSTOM_PATH" ]]; then | |
| CURRENT_PATH="$CUSTOM_PATH" | |
| elif [[ "$STRATEGY" == "nested" || "$STRATEGY" == "sibling" ]]; then | |
| CURRENT_PATH="" | |
| fi | |
| # Write JSON manually | |
| cat > "$CONFIG_FILE" << EOF | |
| { | |
| "git_mode": "$CURRENT_MODE", | |
| "worktree_strategy": "$CURRENT_STRATEGY", | |
| "worktree_custom_path": "$CURRENT_PATH" | |
| } | |
| EOF | |
| >&2 echo "[specify] Error: jq is required to update worktree configuration safely." | |
| >&2 echo "[specify] Please install jq (https://stedolan.github.io/jq/) and re-run this command." | |
| exit 1 |
….specify/ The 3 scripts under .specify/scripts/bash/ were copies of scripts/bash/ and created a maintenance burden (issue M6 from PR review). Untrack them and broaden .gitignore from just .specify/config.json to the entire .specify/ directory, which is generated per-project by specify init. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
@brianluby I've opened a new pull request, #2, to work on those changes. Once the pull request is ready, I'll request review from you. |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Fix CHANGELOG date (2025 → 2026) and remove stale "automatic fallback" line that contradicts the actual error-exit behavior - Escape backslashes and quotes in jq-fallback JSON construction to prevent broken output from paths with special characters - Add -ConfigFile parameter to PowerShell Get-ConfigValue for parity with bash read_config_value, avoiding redundant Get-RepoRoot calls - Fix script path in __init__.py: .specify/scripts/ → scripts/ since .specify/ is now gitignored Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Summary
configure-worktree.shby using--argfor user-supplied values; fix PowerShell temp file leak withtry/finallycleanupread_config_valuenow accepts optional config file path to avoid redundantget_repo_root()subprocess callsHAS_GITin PowerShell JSON output and add to bash for cross-platform parityWORKTREE_DESIGN.mdtospecs/001-git-worktrees/; bump to v0.0.23 with CHANGELOGTest plan
create-new-feature.shin branch mode — verify no worktree warnings appearcreate-new-feature.sh --jsonin branch mode — verifyHAS_GIT,FEATURE_ROOT,MODEall presentconfigure-worktree.sh --mode worktree) and create a feature — verify worktree is createdconfigure-worktree.shwith special characters in--path— verify no jq injection🤖 Generated with Claude Code