Skip to content

Worktree mode#48

Merged
kenryu42 merged 64 commits intomainfrom
worktree-mode
May 1, 2026
Merged

Worktree mode#48
kenryu42 merged 64 commits intomainfrom
worktree-mode

Conversation

@kenryu42
Copy link
Copy Markdown
Owner

@kenryu42 kenryu42 commented Apr 30, 2026

Summary

  • Adds Worktree Mode (SAFETY_NET_WORKTREE=1): relaxes local-discard git rules (restore, checkout --, reset --hard, clean -f, switch --force) when the cwd is a verified linked worktree, since linked worktrees are designed as disposable, isolated workspaces.
  • Implements comprehensive safety hardening to prevent bypass: environment propagation through shell wrappers / interpreters / xargs / parallel / BusyBox, fail-closed on dynamic arguments, recursive submodules, forced branch resets, --git-dir/--work-tree overrides, and git config-affecting env vars.
  • New src/core/worktree.ts module for filesystem-based linked worktree detection with gitdir backlink verification, config.worktree validation, inode-based path comparison, and symlink-safe realpath resolution.

Changes

New Module: src/core/worktree.ts (+335 lines)

  • Linked worktree detection via .git file → gitdir:commondir chain
  • Gitdir backlink verification (rejects copied/symlinked .git files)
  • config.worktree validation against actual worktree root
  • git -C chained resolution with symlink-safe realpath
  • Inode-based path comparison with normalized realpath fallback
  • Windows native realpath prefix normalization (\\?\, \\?\UNC\)
  • Platform-aware path separator handling

Core Analysis Enhancements

  • src/core/rules-git.ts (+608 lines): Worktree relaxation logic with fail-closed hardening:

    • getGitWorktreeRelaxation() — determines if a blocked git command should be relaxed
    • isNonRelaxableLocalDiscard() — checks for dynamic arguments, recursive submodules, double-force clean, forced branch resets
    • hasRecursiveSubmoduleConfig() — reads local git config files before invoking git binary; checks GIT_CONFIG_COUNT/GIT_CONFIG_KEY_*/GIT_CONFIG_VALUE_* env vars and --config-env
    • effectiveGitConfigEnablesRecursiveSubmodules() — invokes trusted git binary (/usr/bin/git, /usr/local/bin/git) with sanitized env
    • Commands that reach beyond the local worktree remain blocked (push --force, branch -D, stash drop/clear)
  • src/core/analyze/analyze-command.ts (+445 lines): Shell git context environment tracking:

    • ShellGitContextEnvState — tracks exported names, set -a (allexport), keyword export mode
    • applyShellGitContextEnvSegment() — handles export, typeset, declare, readonly, builtin, command builtins
    • getSegmentGitContextEnvAssignments() — computes per-segment env assignments from accumulated shell state
    • Append assignments (NAME+=value) tracked for git config env names
    • AnalyzeNestedOverrides propagated through nested analysis calls
  • src/core/shell.ts (+261 lines): Wrapper/env stripping with cwd tracking:

    • stripWrappersWithInfo() now returns cwd (null = unknown, undefined = unchanged)
    • sudo --chdir/-D/--login cwd tracking
    • env -C/--chdir/-S/--split-string handling
    • Append env assignments (NAME+=value) preserved for git context
  • src/core/analyze/parallel.ts (+150 lines): Worktree-aware parallel analysis:

    • Detects remote execution (-S, --sshlogin) and dynamic stdin/placeholder combinations
    • Disables worktree relaxation for remote and dynamic-argument contexts
    • Improved placeholder detection: {1}, {.}, {/}, {//}, {/.}, {#}, {%}
    • Propagates envAssignments and cwd from wrapper stripping to child analysis
  • src/core/analyze/xargs.ts (+32 lines): Worktree-aware xargs analysis:

    • Uses wrapper-resolved cwd in analyzeRm calls
    • Disables worktree relaxation when dynamic replacement tokens are present
    • Propagates envAssignments through child analysis
  • src/core/analyze/segment.ts (+53 lines): Propagates envAssignments and wrapper cwd into nested analysis calls

  • src/types.ts (+11 lines):

    • worktreeMode option in AnalyzeOptions
    • AnalyzeNestedOverrides type for env/cwd/worktreeMode propagation
    • worktree-relaxation trace step type

CLI Surface & Explain

  • All hook entry points (claude-code.ts, copilot-cli.ts, gemini-cli.ts): Read SAFETY_NET_WORKTREE and pass worktreeMode to analysis
  • src/bin/statusline.ts: 🌳 emoji for worktree mode
  • src/bin/help.ts: Documents SAFETY_NET_WORKTREE=1 env var
  • src/bin/explain/segment.ts (+117 lines): Worktree relaxation trace steps, env/cwd propagation parity with guard analysis
  • src/bin/explain/format-helpers.ts: Format worktree-relaxation trace step
  • src/bin/explain/config.ts: Include worktreeMode in explain config
  • src/bin/doctor/environment.ts: Report SAFETY_NET_WORKTREE in doctor output

Documentation

  • README.md: New "Worktree Mode" section under Advanced Features with usage, allowed/blocked commands, and detection details

Tests (~1,600 lines added)

  • tests/core/worktree.test.ts (+276 lines): Unit tests for linked worktree detection — linked/main/non-repo/submodule/symlinked/malformed/copied/backlink/config.worktree/Windows path normalization
  • tests/core/rules-git.test.ts (+971 lines): Comprehensive worktree mode tests — bypass/hardening, env propagation (HOME, GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, GIT_CONFIG_*), sudo --chdir, export/typeset/declare/readonly, set -a, keyword export, recursive submodules, forced branch resets, dynamic arguments, xargs/parallel propagation, interpreter wrapping, shell wrapper nesting, explain parity
  • tests/bin/explain/command.test.ts (+156 lines): Explain command worktree trace steps
  • tests/core/analyze/edge-cases.test.ts (+30 lines): Platform-aware path formatting in xargs/parallel tests
  • tests/helpers.ts (+70 lines): createLinkedWorktreeFixture(), createSubmoduleLikeGitFileFixture(), toShellPath(), withEnv(), worktreeMode env support

Testing

# Run all checks (typecheck + knip + biome lint + tests)
bun run check

# Run worktree-specific tests
bun test tests/core/worktree.test.ts
bun test tests/core/rules-git.test.ts --test-name-pattern "worktree"

# Run explain parity tests
bun test tests/bin/explain/command.test.ts

# Manual test with a real linked worktree
cd /path/to/main-repo
git worktree add ../linked-worktree -b test-branch
cd ../linked-worktree
SAFETY_NET_WORKTREE=1 bun src/bin/cc-safety-net.ts explain "git checkout -- ."
SAFETY_NET_WORKTREE=1 bun src/bin/cc-safety-net.ts explain "git reset --hard"
SAFETY_NET_WORKTREE=1 bun src/bin/cc-safety-net.ts explain "git push --force"  # should still block

Related Issue

#44 #47

PR Checklist

  • I have read the CONTRIBUTING.md
  • Code follows project conventions (type hints, naming, etc.)
  • bun run check passes (lint, types, dead code, rules, tests)
  • Tests added for new rules (minimum 90% coverage required)
  • Tested locally with Claude Code, OpenCode, Gemini CLI or GitHub Copilot CLI
  • Updated documentation if needed (README, AGENTS.md)
  • No version changes in package.json

Summary by CodeRabbit

  • New Features

    • Worktree Mode (enable with SAFETY_NET_WORKTREE=1): allows a curated set of local-discard git operations in linked worktrees while preserving fail-closed safety checks. Explain output emits a "Worktree relaxation" block when applicable; statusline shows 🌳 and environment reporting includes SAFETY_NET_WORKTREE.
  • Documentation

    • README and help updated with Worktree Mode details, allowlist/blocklist, and fail-closed detection rules.
  • Tests

    • Added extensive tests covering worktree detection, relaxation behavior, formatting, CLI/status, nested/parallel, and edge cases.

kenryu42 added 30 commits April 27, 2026 05:02
Pure-filesystem detection for git linked worktrees via .git file and
commondir heuristics. Includes git execution context resolution that
honors -C, --git-dir, --work-tree flags and GIT_DIR/GIT_WORK_TREE/
GIT_COMMON_DIR env overrides.
Adds worktreeMode boolean to AnalyzeOptions and worktree-relaxation
discriminant to TraceStep union. Wires the option through the OpenCode
plugin entry point.
Refactors analyzeGit to classify rules as local-discard vs shared-state.
When SAFETY_NET_WORKTREE=1 and cwd is a linked worktree, local discard
commands (checkout --, restore, reset --hard/--merge, clean -f, switch
--discard-changes/-f) are allowed. Shared-state commands (push --force,
branch -D, stash drop/clear, worktree remove --force) remain blocked.

Wires worktreeMode, cwd, and envAssignments through the analysis
pipeline: segment, xargs, and parallel.
Wires worktreeMode through all hook entry points (claude-code,
copilot-cli, gemini-cli), help text, statusline emoji, doctor
environment diagnostics, and explain config. Adds worktree-relaxation
step formatting in explain output (both human and Style D).
Adds createLinkedWorktreeFixture and createSubmoduleLikeGitFileFixture
test helpers. Tests worktree detection (linked, main, submodule-like,
symlinked, malformed), git execution context resolution, env override
detection, and worktree relaxation rules (local discard allowed,
shared-state blocked, -C resolution, explicit context overrides).
Tests statusline emoji, doctor environment variable listing, help text,
explain --json worktree-relaxation step, explain human formatting, and
TraceStep type discriminant for worktree-relaxation.
Documents SAFETY_NET_WORKTREE environment variable, which commands are
relaxed (local discards) vs remain blocked (shared-state), linked
worktree detection heuristics, and statusline emoji.
… AnalyzeOptions

Add AnalyzeNestedOverrides interface for propagating env, cwd, and
worktreeMode overrides through nested command analysis. Add
envAssignments field to AnalyzeOptions so environment context can
flow through the analysis pipeline.
…pping

Extend stripWrappersWithInfo and stripEnvAssignmentsWithInfo to track
the effective cwd when env -C or --chdir is encountered. This allows
nested analysis to use the correct working directory for rule checks.
…ested

Update analyzeNested in analyzeCommandInternal to accept
AnalyzeNestedOverrides, passing effectiveCwd, envAssignments, and
worktreeMode to nested analysis so context is preserved across
recursion boundaries.
Use stripWrappersWithInfo cwd result to set nestedEffectiveCwd and
cwdForRm. Merge env assignments from wrappers into the inherited
envAssignments map. Pass overrides when analyzing shell wrapper
children and disable worktreeMode for SSH/remote embedded git
commands.
…pagation

Detect SSH login flags (-S/--sshlogin) to identify remote parallel
execution and disable worktreeMode for those commands. Use
stripWrappersWithInfo instead of stripWrappers to propagate env/cwd
context from child wrappers. Build and pass AnalyzeNestedOverrides
through all nested analysis calls.
Use stripWrappersWithInfo instead of stripWrappers to capture env
assignments and cwd from child wrappers. Pass the resolved childCwd
and merged childEnvAssignments to git analysis.
Change analyzeGitReset to return GitRuleMatch so it can distinguish
between ref-moving resets (shared state) and local-only discards.
Resets with a ref argument stay blocked even in worktree mode, while
bare --hard/--merge without a ref are treated as local discards.
Verify that SAFETY_NET_WORKTREE does not relax protections when:
- cwd-changing wrappers redirect to main worktree
- xargs/parallel/shell wrapper child commands use env overrides
  targeting the main worktree
- fallback embedded git commands run over SSH
- remote parallel execution uses SSH login flags
- git reset with ref arguments moves shared refs
…pping

Upgrade stripSudo to stripSudoWithInfo that tracks -D directory changes.
Add env -S/--split-string parsing that expands split-string tokens
before further processing, handling -S val, -Sval, and --split-string=val
forms. Fix env -C attached-arg parsing to handle -Cval (without =).
Export parseEnvAssignment and GIT_CONTEXT_ENV_OVERRIDES for use in
segment-level export tracking.
…ents

Track export statements that set GIT_DIR/GIT_WORK_TREE/GIT_COMMON_DIR
across semicolon-separated segments, propagating them into subsequent
segment analysis. Also propagate segment-scoped envAssignments through
analyzeNested so nested commands see the correct env context.
…ox, parallel, and xargs

Pass effectiveCwd and envAssignments through interpreter one-liners
and BusyBox recursive analysis. In parallel commands mode, forward
wrapper env/cwd context via buildCommandsModeOverrides. In xargs git
analysis, append a placeholder token when xargs input would be
appended as a git argument, enabling ref-moving reset detection.
In parallel template mode, expand git command sets with each arg
to detect ref-moving resets through appended input.
…e fallback worktree relaxation

Move effectiveCwd derivation before wrapper stripping so stripWrappersWithInfo
receives the correct base cwd. Use wrapperResult.cwd for downstream cwd
derivation to match guard behavior. Disable worktreeMode for fallback
embedded git analysis in explain, preventing spurious worktree relaxation
for commands like ssh host git clean -f.
… BusyBox, parallel, xargs, and interpreter propagation

Add comprehensive test coverage for worktree mode: env -C with terminators
and attached args, env -S split-string parsing, sudo -D chdir, export of
GIT_DIR/GIT_WORK_TREE across segments, BusyBox wrapper context, parallel
commands mode and template arg expansion, xargs appended input for git
ref-moving resets, and interpreter wrapper context propagation. Also add
explainCommand parity tests for wrapper cwd and fallback worktree relaxation.
…ards, sudo chdir, export/typeset, and explain parity
…th fallback

Replace direct realpathSync equality checks in worktreeGitdirBacklinkMatches
and worktreeConfigMatchesRoot with sameFilesystemPath, which compares by
device/inode first for reliability, then falls back to normalized realpath
comparison with platform-aware case handling and separator normalization.
…in xargs/parallel tests

Wrap otherDir paths with toShellPath to ensure correct path formatting
across platforms, particularly Windows where backslash separators differ.
…before invoking git binary

Parse .git/config and config.worktree files directly to determine submodule.recurse
setting without needing a git binary invocation. Resolves gitdir and commondir
from worktree .git files. Includes and includeIf directives conservatively
treat as enabling recursive submodules. Falls back to git binary only when local
config is absent or inconclusive; returns false instead of true when git binary
is unavailable but local config is readable.
…ed result

The linked worktree fixture now has its local git config read directly,
so git reset --hard is allowed when the local config is readable and the
git binary is not invoked (or unavailable).
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

📝 Walkthrough

Walkthrough

Adds SAFETY_NET_WORKTREE-controlled "Worktree Mode": per-segment shell/git context and env-assignment tracking, new worktree detection utilities, and optional "worktree relaxation" that permits certain local-discard git operations only when executed inside a verified linked worktree.

Changes

Cohort / File(s) Summary
Docs & CLI help
README.md, src/bin/help.ts
Documented Worktree Mode, added emoji/status, and help ENV listing for SAFETY_NET_WORKTREE.
Env & statusline integration
src/bin/doctor/environment.ts, src/index.ts, src/bin/statusline.ts, src/bin/hooks/claude-code.ts, src/bin/hooks/copilot-cli.ts, src/bin/hooks/gemini-cli.ts, src/bin/explain/config.ts
Expose SAFETY_NET_WORKTREE in environment reporting and thread worktreeMode option through startup/hooks/config/statusline.
Explain & formatting
src/bin/explain/analyze.ts, src/bin/explain/segment.ts, src/bin/explain/format-helpers.ts, src/types.ts
Track persistent shell-git context across segments, compute per-segment envAssignments, add worktreeMode flag and worktree-relaxation trace step + formatter path.
Shell parsing & path utilities
src/core/shell.ts, src/core/path.ts
CWD-aware wrapper/env stripping (-C, sudo/env flags), export env-assignment parsing, dynamic-substitution sentinel injection, and chdir target resolution helper.
Worktree detection utilities
src/core/worktree.ts
New module to resolve git execution context, detect linked worktrees, exported constants and helpers (explicit git-dir/work-tree detection, env override checks).
Git rules & analysis plumbing
src/core/rules-git.ts, src/core/analyze/analyze-command.ts, src/core/analyze/segment.ts, src/core/analyze/parallel.ts, src/core/analyze/xargs.ts
Reworked git analysis into rule + optional worktree relaxation, added GitAnalyzeOptions/GitWorktreeRelaxation, extensive plumbing for envAssignments/worktreeMode through nested/parallel/xargs analysis, and new non-relaxable detection logic.
Types
src/types.ts
Added envAssignments and worktreeMode to AnalyzeOptions, new AnalyzeNestedOverrides, and worktree-relaxation TraceStep variant.
Path & worktree tests & helpers
tests/helpers.ts, tests/core/worktree.test.ts, tests/core/rules-git.test.ts
New test fixtures for linked worktrees and submodule-like .git files; extensive unit/integration tests for worktree detection, config parsing, and fail-closed behavior.
Explain & CLI tests
tests/bin/explain/*, tests/bin/cli-statusline.test.ts, tests/bin/doctor/environment.test.ts, tests/bin/help.test.ts
Integration/unit tests validating worktree-relaxation traces, statusline emoji, env reporting, and formatting.
Misc tests (parsing/parallel/xargs/edge cases)
tests/core/analyze/*, tests/core/parsing-helpers.test.ts, tests/core/analyze/analyze-coverage.test.ts
Added/updated tests checking wrapper -C cwd propagation, placeholder handling, xargs/parallel behavior, and shell export/set propagation for git context.
Misc
.gitignore
Adjusted ignores (removed .sisyphus/ ignore; added coverage).

Sequence Diagram(s)

sequenceDiagram
%%{init: {"themeVariables": {"actorBackground":"rgba(200,220,255,0.5)","actorBorder":"rgba(0,0,0,0.5)"}} }%%
participant User as "User (shell)"
participant Parser as "Shell Parser / stripWrappers"
participant Analyzer as "Analyzer (analyzeCommand)"
participant Worktree as "Worktree Utils (isLinkedWorktree)"
participant FS as "Filesystem / git config"

User->>Parser: submit command tokens + cwd + env
Parser->>Analyzer: tokens, effectiveCwd, envAssignments (per‑segment)
Analyzer->>Analyzer: update shell‑git context across segments
Analyzer->>Worktree: getGitExecutionContext(tokens, cwd)
Worktree->>FS: resolve -C targets, read .git, read linked config
alt linked worktree verified and SAFETY_NET_WORKTREE=1
  Worktree-->>Analyzer: { gitCwd }
  Analyzer->>Analyzer: getGitWorktreeRelaxation(tokens, envAssignments, worktreeMode)
  Analyzer-->>User: emit worktree‑relaxation trace -> allow local discard
else explicit overrides / unknown / not linked
  Worktree-->>Analyzer: { hasExplicitGitContext:true } or { gitCwd:null }
  Analyzer-->>User: block destructive git operation (no relaxation)
end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

  • Issue #44: Implements worktree detection and the SAFETY_NET_WORKTREE toggle (isLinkedWorktree, getGitWorktreeRelaxation, and conditional relaxation), matching the issue objective.

Possibly related PRs

  • PR #34: Overlap in src/core/shell.ts wrapper/token handling — likely merge conflicts.
  • PR #32: Modifies src/core/rules-git.ts git-subcommand parsing — strong overlap with new worktree-aware git logic.
  • PR #31: Also touches env-assignment parsing in src/core/shell.ts — potential conflict with exported parse/assignment changes.

Poem

🐰
In linked worktrees I hop and sing,
A little 🌳 lets local discards swing.
Shell context kept, the roots run true,
I guard the branches — then nibble a clue.
Hooray — a rabbit's tiny review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.59% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title "Worktree mode" clearly and concisely describes the main feature addition. It directly corresponds to the primary change: introducing Worktree Mode via SAFETY_NET_WORKTREE=1 to relax local-discard git rules for linked worktrees.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering summary, changes, testing instructions, and related issues. It follows the template structure with all critical sections populated: Summary (bullet points), Changes (detailed file-by-file breakdown), Testing (executable commands), Related Issue, and completed PR Checklist.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-mode

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 30, 2026

Codecov Report

❌ Patch coverage is 95.31443% with 76 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.51%. Comparing base (33739bb) to head (96555cb).
⚠️ Report is 67 commits behind head on main.

Files with missing lines Patch % Lines
src/core/rules-git.ts 93.84% 28 Missing ⚠️
src/core/shell.ts 89.26% 19 Missing ⚠️
src/bin/explain/segment.ts 87.95% 10 Missing ⚠️
src/core/worktree.ts 96.65% 9 Missing ⚠️
src/core/analyze/analyze-command.ts 97.84% 8 Missing ⚠️
src/core/analyze/parallel.ts 98.19% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #48      +/-   ##
==========================================
- Coverage   99.51%   98.51%   -1.00%     
==========================================
  Files          50       52       +2     
  Lines        5362     6879    +1517     
==========================================
+ Hits         5336     6777    +1441     
- Misses         26      102      +76     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 30, 2026

Greptile Summary

This PR adds Worktree Mode (SAFETY_NET_WORKTREE=1) that relaxes local-discard git rules (restore, checkout --, reset --hard, clean -f, switch --force) when the current directory is a verified linked worktree. It includes deep safety hardening — environment variable propagation tracking through shell builtins, wrapper/interpreter chains, xargs, parallel, and sudo/env with cwd-changing flags — all fail-closed to prevent bypass.

The implementation is thorough and well-tested. The one remaining maintainability note is that isTrackedGitEnvName, isGitConfigEnvName, and GIT_CONTEXT_ENV_OVERRIDE_NAMES are defined identically in both src/core/shell.ts and src/core/analyze/analyze-command.ts, which the previously addressed GIT_CONFIG_AFFECTING_ENV_NAMES consolidation pattern could be extended to cover.

Confidence Score: 5/5

Safe to merge — all hardening paths are fail-closed and the only finding is a minor maintainability duplication with no correctness impact.

No P0 or P1 issues found. Previously flagged P1s (missing /usr/local/bin/git and /opt/homebrew/bin/git in TRUSTED_GIT_BINARIES, GIT_CONFIG_AFFECTING_ENV_NAMES triplication, path helper duplication, missing backslash escape handling) are all addressed. The single remaining finding is a P2 duplication of three small helpers across shell.ts and analyze-command.ts.

src/core/shell.ts and src/core/analyze/analyze-command.ts share identical isTrackedGitEnvName/isGitConfigEnvName/GIT_CONTEXT_ENV_OVERRIDE_NAMES definitions that should be consolidated.

Important Files Changed

Filename Overview
src/core/worktree.ts New module implementing linked-worktree detection with gitdir backlink verification, config.worktree validation, inode-based path comparison, and platform-aware realpath normalization. Logic is thorough and fail-closed.
src/core/rules-git.ts Adds worktree relaxation logic with extensive fail-closed hardening: dynamic args, recursive submodule detection (local file + trusted git binary), forced-branch-reset guard, double-force clean check, and git config env var tracking. TRUSTED_GIT_BINARIES now covers /usr/bin/git, /usr/local/bin/git, and /opt/homebrew/bin/git.
src/core/shell.ts Adds cwd tracking through sudo --chdir/-D and env -C/--chdir/-S/--split-string wrappers; preserves git-context append assignments. Introduces isTrackedGitEnvName/isGitConfigEnvName/GIT_CONTEXT_ENV_OVERRIDE_NAMES that are identically duplicated in analyze-command.ts — should be consolidated in worktree.ts.
src/core/analyze/analyze-command.ts Adds ShellGitContextEnvState to track export/typeset/declare/readonly/set -a/set -k env propagation across segments; correctly threads segmentEnvAssignments into nested analysis. Contains duplicated isTrackedGitEnvName and isGitConfigEnvName helpers (see shell.ts comment).
src/core/analyze/parallel.ts Adds remote-execution detection (-S/--sshlogin), improved placeholder regex covering {1}/{.}/{/}/{//}/{/.}/{#}/{%}, and cwd/envAssignments propagation; correctly disables worktreeMode for remote/dynamic-stdin contexts.
src/core/analyze/xargs.ts Propagates wrapper-resolved cwd and envAssignments to child analysis; correctly disables worktreeMode when stdin replacement tokens or dynamic placeholders are present.
src/core/path.ts New shared module for resolveChdirTarget with symlink-safe resolution; eliminates previous duplication between worktree.ts and shell.ts.
src/core/analyze/segment.ts Propagates wrapper-resolved cwd and accumulated envAssignments into git/xargs/parallel/shell-wrapper analysis; correctly disables worktreeMode for find-exec git patterns.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[analyzeGit called] --> B{worktreeMode enabled?}
    B -- No --> C[Return original reason / block]
    B -- Yes --> D{match.localDiscard?}
    D -- No sharedState --> C
    D -- Yes --> E{hasGitContextEnvOverride?}
    E -- Yes --> C
    E -- No --> F[getGitExecutionContext]
    F --> G{gitCwd resolved?}
    G -- null or explicit --> C
    G -- Valid cwd --> H[isLinkedWorktree]
    H -- Not linked --> C
    H -- Is linked --> I[isNonRelaxableLocalDiscard]
    I --> J{Dynamic args / recurse-submodules / double-force / branch-reset?}
    J -- Yes --> C
    J -- No --> K[hasRecursiveSubmoduleConfig]
    K -- Enabled --> C
    K -- Not enabled --> L[Allow relaxation]
Loading

Reviews (2): Last reviewed commit: "fix(worktree): handle double-quoted esca..." | Re-trigger Greptile

Comment thread src/core/rules-git.ts
Comment thread src/core/rules-git.ts Outdated
Comment thread src/core/worktree.ts Outdated
Comment thread src/core/worktree.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tests/helpers.ts (1)

100-157: ⚡ Quick win

Mark the new fixture exports as internal.

These interfaces/functions are test-only exports, so leaving them undecorated makes Knip treat them as public surface.

♻️ Suggested JSDoc additions
+/** `@internal` Exported for testing */
 export interface LinkedWorktreeFixture {
   rootDir: string;
   mainWorktree: string;
   linkedWorktree: string;
   cleanup: () => void;
 }

+/** `@internal` Exported for testing */
 export function createLinkedWorktreeFixture(): LinkedWorktreeFixture {
   // ...
 }

+/** `@internal` Exported for testing */
 export interface FakeGitFileFixture {
   rootDir: string;
   cwd: string;
   cleanup: () => void;
 }

+/** `@internal` Exported for testing */
 export function createSubmoduleLikeGitFileFixture(): FakeGitFileFixture {
   // ...
 }

Based on learnings: "For test-only exports, add /** @internal Exported for testing */ JSDoc to satisfy knip".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/helpers.ts` around lines 100 - 157, Add internal JSDoc to mark these
test-only exports as internal: annotate the LinkedWorktreeFixture and
FakeGitFileFixture interfaces and the createLinkedWorktreeFixture and
createSubmoduleLikeGitFileFixture functions with a comment like /** `@internal`
Exported for testing */ immediately above each declaration so knip treats them
as non-public; update the JSDoc for the symbols LinkedWorktreeFixture,
createLinkedWorktreeFixture, FakeGitFileFixture, and
createSubmoduleLikeGitFileFixture accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/helpers.ts`:
- Around line 100-157: Add internal JSDoc to mark these test-only exports as
internal: annotate the LinkedWorktreeFixture and FakeGitFileFixture interfaces
and the createLinkedWorktreeFixture and createSubmoduleLikeGitFileFixture
functions with a comment like /** `@internal` Exported for testing */ immediately
above each declaration so knip treats them as non-public; update the JSDoc for
the symbols LinkedWorktreeFixture, createLinkedWorktreeFixture,
FakeGitFileFixture, and createSubmoduleLikeGitFileFixture accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 46393664-7699-48ac-aa14-49817c404794

📥 Commits

Reviewing files that changed from the base of the PR and between a8acf4d and f492852.

⛔ Files ignored due to path filters (10)
  • dist/bin/cc-safety-net.js is excluded by !**/dist/**
  • dist/core/analyze/analyze-command.d.ts is excluded by !**/dist/**
  • dist/core/analyze/parallel.d.ts is excluded by !**/dist/**
  • dist/core/analyze/segment.d.ts is excluded by !**/dist/**
  • dist/core/analyze/xargs.d.ts is excluded by !**/dist/**
  • dist/core/rules-git.d.ts is excluded by !**/dist/**
  • dist/core/shell.d.ts is excluded by !**/dist/**
  • dist/core/worktree.d.ts is excluded by !**/dist/**
  • dist/index.js is excluded by !**/dist/**
  • dist/types.d.ts is excluded by !**/dist/**
📒 Files selected for processing (32)
  • README.md
  • src/bin/doctor/environment.ts
  • src/bin/explain/analyze.ts
  • src/bin/explain/config.ts
  • src/bin/explain/format-helpers.ts
  • src/bin/explain/segment.ts
  • src/bin/help.ts
  • src/bin/hooks/claude-code.ts
  • src/bin/hooks/copilot-cli.ts
  • src/bin/hooks/gemini-cli.ts
  • src/bin/statusline.ts
  • src/core/analyze/analyze-command.ts
  • src/core/analyze/parallel.ts
  • src/core/analyze/segment.ts
  • src/core/analyze/xargs.ts
  • src/core/rules-git.ts
  • src/core/shell.ts
  • src/core/worktree.ts
  • src/index.ts
  • src/types.ts
  • tests/bin/cli-statusline.test.ts
  • tests/bin/doctor/environment.test.ts
  • tests/bin/explain/cli.test.ts
  • tests/bin/explain/command.test.ts
  • tests/bin/explain/format.test.ts
  • tests/bin/explain/types.test.ts
  • tests/bin/help.test.ts
  • tests/core/analyze/edge-cases.test.ts
  • tests/core/analyze/parsing-helpers.test.ts
  • tests/core/rules-git.test.ts
  • tests/core/worktree.test.ts
  • tests/helpers.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/rules-git.ts`:
- Around line 609-618: The function effectiveGitConfigEnablesRecursiveSubmodules
currently treats a missing trusted git binary as safe; change its behavior in
effectiveGitConfigEnablesRecursiveSubmodules so that if getTrustedGitBinary()
returns null it returns true (fail-closed/assume unsafe) instead of false, and
make the same fail-closed change in any other branch that currently returns
false when gitBinary is null (see similar logic around where
getTrustedGitBinary() is called later in the file). Also update
getTrustedGitBinary to include standard Windows installation paths ("C:\\Program
Files\\Git\\cmd\\git.exe" and "C:\\Program Files\\Git\\bin\\git.exe") in its
candidate list so Windows-installed git is recognized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4f85784a-7a01-4c56-88fa-6223caae7780

📥 Commits

Reviewing files that changed from the base of the PR and between f492852 and f3abe29.

⛔ Files ignored due to path filters (5)
  • dist/bin/cc-safety-net.js is excluded by !**/dist/**
  • dist/core/path.d.ts is excluded by !**/dist/**
  • dist/core/rules-git.d.ts is excluded by !**/dist/**
  • dist/core/worktree.d.ts is excluded by !**/dist/**
  • dist/index.js is excluded by !**/dist/**
📒 Files selected for processing (7)
  • src/core/analyze/analyze-command.ts
  • src/core/path.ts
  • src/core/rules-git.ts
  • src/core/shell.ts
  • src/core/worktree.ts
  • tests/core/rules-git.test.ts
  • tests/core/worktree.test.ts

Comment thread src/core/rules-git.ts Outdated
@kenryu42
Copy link
Copy Markdown
Owner Author

@greptileai review

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/core/analyze/edge-cases.test.ts`:
- Around line 429-440: The test 'xargs rm uses wrapper cwd when checking
relative targets' currently uses `rm -rf build` which resides inside `otherDir`
and doesn't prove wrapper-cwd enforcement; update the command passed to
`assertBlocked` to target a path outside `otherDir` (e.g. `../build`) so the
removal would escape the wrapper cwd. Adjust the call to assertBlocked to use
the modified shell string `echo ok | xargs env -C ${toShellPath(otherDir)} rm
-rf ../build` (keeping `projectDir`, `otherDir`, `toShellPath`, and
`assertBlocked` as reference points) so the test correctly verifies escape
detection.
- Around line 548-559: The test 'parallel rm uses wrapper cwd when checking
relative replacements' is using the relative token "build" which lives under the
wrapper cwd so it does not prove the cwd-escape check; update the assertBlocked
command to target a path that actually escapes the wrapper cwd (for example use
toShellPath(join(otherDir, 'build')) or a '../other/build' relative path) and
create that target if needed (mkdirSync(join(otherDir, 'build'))). Modify the
string passed to assertBlocked (the command in the parallel env invocation) to
reference the escaping path and keep the rest of the test (projectDir/otherDir,
assertBlocked, toShellPath, tempDir) intact.

In `@tests/core/worktree.test.ts`:
- Around line 292-301: The test currently treats an unparsable config.worktree
as a valid linked worktree; update the test so malformed core.worktree is
rejected: in the test using createLinkedWorktreeFixture(), getLinkedGitDir(),
and writeFileSync(... 'config.worktree', '[core\n\tworktree ='), change the
assertion on isLinkedWorktree(fixture.linkedWorktree) from toBe(true) to
toBe(false) (and optionally rename the test to reflect rejection) while keeping
the try/finally cleanup intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: efdca466-a3b8-45d7-9210-7caef1ab0eda

📥 Commits

Reviewing files that changed from the base of the PR and between 0f3a06e and 96555cb.

📒 Files selected for processing (6)
  • .gitignore
  • tests/core/analyze/analyze-coverage.test.ts
  • tests/core/analyze/edge-cases.test.ts
  • tests/core/analyze/parsing-helpers.test.ts
  • tests/core/rules-git.test.ts
  • tests/core/worktree.test.ts
✅ Files skipped from review due to trivial changes (2)
  • .gitignore
  • tests/core/rules-git.test.ts

Comment on lines +429 to +440
test('xargs rm uses wrapper cwd when checking relative targets', () => {
const projectDir = join(tempDir, 'project');
const otherDir = join(tempDir, 'other');
mkdirSync(projectDir);
mkdirSync(otherDir);

assertBlocked(
`echo ok | xargs env -C ${toShellPath(otherDir)} rm -rf build`,
'rm -rf outside cwd',
projectDir,
);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use an escaping path if the goal is to prove wrapper-cwd enforcement.

build is still inside otherDir, so this blocked assertion does not actually demonstrate that env -C is being honored. If you want to verify escape detection, switch this to something like ../build.

Suggested adjustment
-        `echo ok | xargs env -C ${toShellPath(otherDir)} rm -rf build`,
+        `echo ok | xargs env -C ${toShellPath(otherDir)} rm -rf ../build`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test('xargs rm uses wrapper cwd when checking relative targets', () => {
const projectDir = join(tempDir, 'project');
const otherDir = join(tempDir, 'other');
mkdirSync(projectDir);
mkdirSync(otherDir);
assertBlocked(
`echo ok | xargs env -C ${toShellPath(otherDir)} rm -rf build`,
'rm -rf outside cwd',
projectDir,
);
});
test('xargs rm uses wrapper cwd when checking relative targets', () => {
const projectDir = join(tempDir, 'project');
const otherDir = join(tempDir, 'other');
mkdirSync(projectDir);
mkdirSync(otherDir);
assertBlocked(
`echo ok | xargs env -C ${toShellPath(otherDir)} rm -rf ../build`,
'rm -rf outside cwd',
projectDir,
);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/analyze/edge-cases.test.ts` around lines 429 - 440, The test
'xargs rm uses wrapper cwd when checking relative targets' currently uses `rm
-rf build` which resides inside `otherDir` and doesn't prove wrapper-cwd
enforcement; update the command passed to `assertBlocked` to target a path
outside `otherDir` (e.g. `../build`) so the removal would escape the wrapper
cwd. Adjust the call to assertBlocked to use the modified shell string `echo ok
| xargs env -C ${toShellPath(otherDir)} rm -rf ../build` (keeping `projectDir`,
`otherDir`, `toShellPath`, and `assertBlocked` as reference points) so the test
correctly verifies escape detection.

Comment on lines +548 to +559
test('parallel rm uses wrapper cwd when checking relative replacements', () => {
const projectDir = join(tempDir, 'project');
const otherDir = join(tempDir, 'other');
mkdirSync(projectDir);
mkdirSync(otherDir);

assertBlocked(
`parallel env -C ${toShellPath(otherDir)} rm -rf {} ::: build`,
'rm -rf outside cwd',
projectDir,
);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Same cwd-escape issue in the parallel case.

parallel ... env -C ${otherDir} ... build keeps build under the wrapper cwd, so blocking it doesn’t prove the wrapper-cwd check. Consider using a path that actually escapes the wrapper cwd.

Suggested adjustment
-        `parallel env -C ${toShellPath(otherDir)} rm -rf {} ::: build`,
+        `parallel env -C ${toShellPath(otherDir)} rm -rf {} ::: ../build`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test('parallel rm uses wrapper cwd when checking relative replacements', () => {
const projectDir = join(tempDir, 'project');
const otherDir = join(tempDir, 'other');
mkdirSync(projectDir);
mkdirSync(otherDir);
assertBlocked(
`parallel env -C ${toShellPath(otherDir)} rm -rf {} ::: build`,
'rm -rf outside cwd',
projectDir,
);
});
test('parallel rm uses wrapper cwd when checking relative replacements', () => {
const projectDir = join(tempDir, 'project');
const otherDir = join(tempDir, 'other');
mkdirSync(projectDir);
mkdirSync(otherDir);
assertBlocked(
`parallel env -C ${toShellPath(otherDir)} rm -rf {} ::: ../build`,
'rm -rf outside cwd',
projectDir,
);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/analyze/edge-cases.test.ts` around lines 548 - 559, The test
'parallel rm uses wrapper cwd when checking relative replacements' is using the
relative token "build" which lives under the wrapper cwd so it does not prove
the cwd-escape check; update the assertBlocked command to target a path that
actually escapes the wrapper cwd (for example use toShellPath(join(otherDir,
'build')) or a '../other/build' relative path) and create that target if needed
(mkdirSync(join(otherDir, 'build'))). Modify the string passed to assertBlocked
(the command in the parallel env invocation) to reference the escaping path and
keep the rest of the test (projectDir/otherDir, assertBlocked, toShellPath,
tempDir) intact.

Comment on lines +292 to +301
test('accepts unparsable core.worktree config as conservative fallback', () => {
const fixture = createLinkedWorktreeFixture();
const gitDir = getLinkedGitDir(fixture.linkedWorktree);
writeFileSync(join(gitDir, 'config.worktree'), '[core\n\tworktree =');
try {
expect(isLinkedWorktree(fixture.linkedWorktree)).toBe(true);
} finally {
fixture.cleanup();
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Fail closed on malformed core.worktree.

Accepting an unparsable config.worktree as a linked worktree keeps the relaxation path open even when the repo metadata is corrupted or tampered with. Worktree Mode should only apply to verified linked worktrees, so this should reject instead of falling back to true.

Suggested expectation
-    expect(isLinkedWorktree(fixture.linkedWorktree)).toBe(true);
+    expect(isLinkedWorktree(fixture.linkedWorktree)).toBe(false);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test('accepts unparsable core.worktree config as conservative fallback', () => {
const fixture = createLinkedWorktreeFixture();
const gitDir = getLinkedGitDir(fixture.linkedWorktree);
writeFileSync(join(gitDir, 'config.worktree'), '[core\n\tworktree =');
try {
expect(isLinkedWorktree(fixture.linkedWorktree)).toBe(true);
} finally {
fixture.cleanup();
}
});
test('accepts unparsable core.worktree config as conservative fallback', () => {
const fixture = createLinkedWorktreeFixture();
const gitDir = getLinkedGitDir(fixture.linkedWorktree);
writeFileSync(join(gitDir, 'config.worktree'), '[core\n\tworktree =');
try {
expect(isLinkedWorktree(fixture.linkedWorktree)).toBe(false);
} finally {
fixture.cleanup();
}
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/worktree.test.ts` around lines 292 - 301, The test currently
treats an unparsable config.worktree as a valid linked worktree; update the test
so malformed core.worktree is rejected: in the test using
createLinkedWorktreeFixture(), getLinkedGitDir(), and writeFileSync(...
'config.worktree', '[core\n\tworktree ='), change the assertion on
isLinkedWorktree(fixture.linkedWorktree) from toBe(true) to toBe(false) (and
optionally rename the test to reflect rejection) while keeping the try/finally
cleanup intact.

@kenryu42 kenryu42 merged commit d91a44c into main May 1, 2026
8 checks passed
@kenryu42 kenryu42 deleted the worktree-mode branch May 1, 2026 16:07
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.

1 participant