Skip to content

fix: Harden worktree feature with security and UX improvements#1

Merged
brianluby merged 11 commits intomainfrom
001-git-worktrees
Feb 5, 2026
Merged

fix: Harden worktree feature with security and UX improvements#1
brianluby merged 11 commits intomainfrom
001-git-worktrees

Conversation

@brianluby
Copy link
Copy Markdown
Owner

Summary

  • Security: Fix jq injection vulnerability in configure-worktree.sh by using --arg for user-supplied values; fix PowerShell temp file leak with try/finally cleanup
  • UX: Replace silent worktree-to-branch fallbacks with clear, actionable error messages; gate pre-flight warnings on worktree mode only so branch-mode users aren't confused
  • Performance: read_config_value now accepts optional config file path to avoid redundant get_repo_root() subprocess calls
  • Consistency: Restore HAS_GIT in PowerShell JSON output and add to bash for cross-platform parity
  • Housekeeping: Move WORKTREE_DESIGN.md to specs/001-git-worktrees/; bump to v0.0.23 with CHANGELOG

Test plan

  • Run create-new-feature.sh in branch mode — verify no worktree warnings appear
  • Run create-new-feature.sh --json in branch mode — verify HAS_GIT, FEATURE_ROOT, MODE all present
  • Configure worktree mode (configure-worktree.sh --mode worktree) and create a feature — verify worktree is created
  • Force a worktree failure (e.g., unwritable path) — verify script exits with error and actionable suggestions instead of silently falling back
  • Run configure-worktree.sh with special characters in --path — verify no jq injection
  • Run PowerShell equivalents of above on Windows/pwsh

🤖 Generated with Claude Code

brianluby and others added 7 commits January 15, 2026 17:54
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>
Copilot AI review requested due to automatic review settings February 5, 2026 16:49
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_ROOT and MODE fields to JSON output; restored HAS_GIT field for cross-platform parity
  • Performance optimization: bash read_config_value now 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.

Comment on lines +223 to +230
# Write JSON manually
cat > "$CONFIG_FILE" << EOF
{
"git_mode": "$CURRENT_MODE",
"worktree_strategy": "$CURRENT_STRATEGY",
"worktree_custom_path": "$CURRENT_PATH"
}
EOF
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
# 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"

Copilot uses AI. Check for mistakes.
Comment on lines +144 to +145
$strategy = Get-ConfigValue -Key "worktree_strategy" -Default "sibling"
$customPath = Get-ConfigValue -Key "worktree_custom_path" -Default ""
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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"
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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"

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot we removed the .specify directory from the repo. This was created by using speckit for this project. items in .specify should be ignored.

Comment on lines +202 to +230
# 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
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
# 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

Copilot uses AI. Check for mistakes.
….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>
Copy link
Copy Markdown

Copilot AI commented Feb 5, 2026

@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.

brianluby and others added 3 commits February 5, 2026 09:07
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>
@brianluby brianluby merged commit 4899828 into main Feb 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants