Skip to content

feat: launch Claude session as git worktree (⌘+Shift+Enter)#119

Open
grimmerk wants to merge 4 commits intomainfrom
feat/worktree-launch
Open

feat: launch Claude session as git worktree (⌘+Shift+Enter)#119
grimmerk wants to merge 4 commits intomainfrom
feat/worktree-launch

Conversation

@grimmerk
Copy link
Copy Markdown
Owner

@grimmerk grimmerk commented Apr 27, 2026

Summary

Add ⌘+Shift+Enter on the Projects tab to launch a Claude Code session as a git worktree. Plus polish: worktree-aware naming.

What's new

Launch a worktree session — ⌘+Shift+Enter

A small dialog asks for an optional branch name:

  • Empty → behaves like ⌘+Enter (normal session)
  • With a name → runs claude -w "<name>" -n "<name>" in the configured terminal. Claude CLI creates a fresh worktree at <repo>/.claude/worktrees/<name> and starts the session inside it.

Esc cancels, click outside cancels, Enter launches. Modal traps Tab/click events so background shortcuts/inputs don't fire while the dialog is open.

Worktree session display

Sessions whose path is <repo>/.claude/worktrees/<name> now display:

  • The parent repo name (e.g., codev) — not the worktree folder name (e.g., test-worktree-4)
  • A small purple WT badge

New parseWorktreePath() helper + isWorktree / parentRepo fields on ClaudeSession. The project field still points at the worktree path (resume / cwd matching unchanged). Detection is path-based: also recognizes worktree sessions launched manually via claude -w or by other tools.

Why -n <name> is required

Without -n, Ghostty session switching breaks for worktree sessions:

  • Worktree terminal cwd = parent repo (shell stays in parent; only the claude process internally cd's into the worktree)
  • Main terminal cwd = parent repo too
  • Ghostty has no per-tab TTY exposure → only cwd / title match available
  • Same cwd → AppleScript first-match-wins → wrong tab

Passing -n <name> makes Claude CLI write a custom-title JSONL entry. Codev's existing Layer-1 title match finds the correct tab. iTerm2 / Terminal.app / cmux were already unaffected (they use TTY match).

Future: once Ghostty exposes per-tab TTY (ghostty-org/ghostty#11354), the -n flag becomes optional.

Shell-injection guard

worktreeName is interpolated into a shell command that ends up in AppleScript. Validated at three layers (dialog input, IPC handler, launchNewClaudeSession) via isValidWorktreeName() — whitelist of branch-name-safe characters; rejects shell metacharacters, leading dash/dot/slash, double slash, ...

Why we use claude -w instead of git worktree add ourselves

Two approaches were considered. We picked B but A remains a viable future direction.

Approach Worktree path Worktree creation Trade-off
A. We own it (claude-control's choice) <parent>/<repo>-<branch> (sibling) git worktree add ourselves Full control, sibling visibility, no IDE/git-GUI clutter inside the parent repo. But codev owns lifecycle: cleanup UI, branch conflict handling, error paths.
B. claude -w <name> -n <name> (this PR) <repo>/.claude/worktrees/<name> (nested) Claude CLI does it Minimal code, leverages Claude CLI features (auto-cleanup, --tmux). Downside: the nested worktree folder is visible to VS Code / git GUI, so users may see worktree files in their workspace, accidentally commit them, or include them in cross-repo searches.

Why B for now: codev is Claude-Code-focused (claude-control supports multiple AI CLIs, so they need their own implementation). Auto-cleanup is the right default for codev's typical use case. Detection of <repo>/.claude/worktrees/ paths is path-based, so even if we ever add A as an option, existing nested worktrees keep showing the WT badge.

See the design doc update for the full discussion.

Files

  • src/switcher-ui.tsx — new dialog component, ⌘+Shift+Enter keybinding, WT badge, hint text, modal event isolation, name validation
  • src/claude-session-utility.tsparseWorktreePath(), isValidWorktreeName(), worktree fields in 3 session sources, -n in launch
  • src/main.ts, src/preload.ts, src/electron-api.d.ts — new launch-new-claude-session-worktree IPC with validation
  • docs/claude-session-integration-design.md — added "Git Worktree Sessions" section
  • README.md, CHANGELOG.md, package.json — feature doc + version 1.0.75

Test plan

  • ⌘+Shift+Enter opens dialog
  • Empty branch name → normal session (same as ⌘+Enter)
  • With branch name → claude -w … -n … launches in external terminal
  • Esc / click-outside cancel
  • Worktree session shows parent repo name + WT badge in Sessions tab
  • Switching back to a worktree session focuses the correct Ghostty tab (when no same-cwd collision)
  • Resume worktree session works
  • Invalid worktree names rejected (shell metachars, leading dash, etc.)
  • Tab/click inside dialog don't fire background shortcuts
  • Non-Ghostty terminals (iTerm2/Terminal.app/cmux) still work as before
  • yarn make build OK (no debug logs in production)

Known limitations / future work

These are pre-existing issues that became more visible with worktree sessions but are not specific to this PR. Documented for follow-up:

  • Same-cwd switch ambiguity (Ghostty). When multiple Ghostty windows report the same cwd (e.g., a yarn start window + the active conversation window + an old worktree window all at ~/git/codev), Layer-2 cwd match returns the first window in iteration order — often the top-most. Workaround: use -n / /rename to disambiguate via title match. Real fix waits on Ghostty TTY exposure.

  • Status indicator occasionally orange when expected green. Worktree sessions that should be idle sometimes show the working/needs-attention orange dot. Cause TBD — may be hook timing or status-file race. Tracked separately.

  • Old worktree sessions launched without -n (before this PR) won't have a custom title and will still hit the cwd-collision issue when switching. Workaround: exit + re-launch.

  • Non-alive worktree sessions don't show a branch name. Claude CLI may auto-cleanup the worktree directory on clean exit, breaking the git -C <worktree-path> branch lookup. Branch info for cleaned-up worktrees is a future enhancement.

  • Nested worktree visibility in IDE/git GUI. Because claude -w places worktrees at <repo>/.claude/worktrees/<name>, VS Code's file explorer and git GUI tools see those folders inside the workspace. Mitigation: add .claude/worktrees/ to .gitignore, or revisit Approach A in a future PR.

  • AppleScript activate timing investigated, kept top-of-block. Moving activate into match-success branches showed a perceptible slowdown in switch latency in local testing. Reverted; the visual glitch on match-miss is documented as a manifestation of the same-cwd ambiguity above, not a separate bug.

  • Resume with worktree-aware context. Codev resumes worktree sessions via cd <path> && claude --resume <uuid>. Claude CLI also supports claude --worktree <name> --resume <uuid> which preserves keep/remove prompts on next exit. Codev could detect isWorktree and use the worktree-aware form — future enhancement.

  • Drop -n once Ghostty exposes TTY (ghostty#11354).

🤖 On behalf of @grimmerk — generated with Claude Code

- New ⌘+Shift+Enter shortcut on a project → opens a small dialog
  for an optional branch name. With a name we run
  `claude -w "<name>" -n "<name>"` in the configured terminal.
- The `-n` flag is critical: it sets a Claude session custom
  title so the Sessions tab can switch back to the right tab in
  Ghostty (which has no per-tab TTY exposure). Without it,
  worktree and main-repo terminals share the same cwd and
  AppleScript first-match-wins picks the wrong tab.
- Added parseWorktreePath() helper + isWorktree / parentRepo
  fields on ClaudeSession. Worktree sessions display as the
  parent repo name with a small WT badge, instead of the
  worktree folder name (e.g. "codev WT" not "test-worktree-4").
- Fixed Ghostty switch AppleScript: `activate` is now inside
  the match success branch, so we no longer surface the wrong
  Ghostty window when no tab matches. This benefits all
  session switches, not only worktrees.
- Updated README + design doc + changelog. Version 1.0.75.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

📝 Walkthrough

Walkthrough

Adds git worktree session launching (optional name via dialog, shortcut ⌘+Shift+Enter) and worktree-aware session detection/display (parent repo + WT badge). Extends IPC/preload/main to accept worktree names and updates Ghostty AppleScript to activate only after a successful tab match. Bumps version to 1.0.75 and adds docs.

Changes

Cohort / File(s) Summary
Release & Docs
CHANGELOG.md, README.md, docs/claude-session-integration-design.md
Documented 1.0.75 release, new "Launch as git worktree" feature, worktree session design, AppleScript activation timing fix, and UI display notes (WT badge).
Version
package.json
Bumped app version from 1.0.741.0.75.
Session utility & model
src/claude-session-utility.ts
Added parseWorktreePath, isValidWorktreeName, getProjectDisplayName; extended ClaudeSession with isWorktree & parentRepo; added worktreeName param to launchNewClaudeSession; improved AppleScript quote escaping and deferred activate until on-match.
Electron API types
src/electron-api.d.ts
Added launchNewClaudeSessionWorktree(projectPath: string, worktreeName: string): void to IElectronAPI.
Main & preload IPC
src/main.ts, src/preload.ts
Added launch-new-claude-session-worktree IPC handler in main and exposed launchNewClaudeSessionWorktree on preload; validates worktree name before launching.
Switcher UI
src/switcher-ui.tsx
Added 'worktree' launch mode, Cmd+Shift+Enter handling, modal for optional worktree name with validation, fallback to normal launch on empty input, and WT badge rendering in sessions list.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant UI as Switcher UI
    participant Dialog as Worktree Dialog
    participant Preload as Preload (window.electronAPI)
    participant Main as Main Process
    participant Util as Session Utility
    participant Terminal as Terminal App

    User->>UI: Select project + Cmd+Shift+Enter
    UI->>Dialog: Open worktree name dialog
    Dialog->>User: Prompt for worktree name
    User->>Dialog: Enter name (or leave empty)

    alt Worktree name provided
        Dialog->>Preload: launchNewClaudeSessionWorktree(projectPath, worktreeName)
        Preload->>Main: IPC: launch-new-claude-session-worktree
        Main->>Util: launchNewClaudeSession(projectPath, ..., worktreeName)
        Util->>Terminal: Execute: claude -w "<name>" -n "<name>"
    else Empty input
        Dialog->>Preload: launchNewClaudeSession(projectPath)
        Preload->>Main: IPC: launch-new-claude-session
        Main->>Util: launchNewClaudeSession(projectPath)
        Util->>Terminal: Execute: claude (normal)
    end

    Terminal-->>User: New session / worktree created
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 I nibble code and hop with glee,
Worktrees sprout from a single key,
A dialog name, a tab takes flight,
Badge of WT gleams in morning light,
Hooray — sessions now branch just right!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 clearly and specifically summarizes the main change: introducing a new worktree-launch feature triggered by ⌘+Shift+Enter.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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 feat/worktree-launch

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

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

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: 4

🧹 Nitpick comments (1)
src/claude-session-utility.ts (1)

161-176: DRY: use getProjectDisplayName instead of re-inlining the worktree/basename logic.

The new getProjectDisplayName helper (lines 46‑50) implements exactly this fallback, but the same expression is reproduced here and at lines 863‑865 and 947‑949. Routing all three call sites through the helper avoids drift if the display rule ever changes (e.g., showing <repo>/<worktree> later).

♻️ Proposed refactor
       .map((s) => {
         const worktree = parseWorktreePath(s.project);
         return {
           sessionId: s.sessionId,
           project: s.project,
-          projectName: worktree
-            ? path.basename(worktree.parentRepo) || worktree.parentRepo
-            : (path.basename(s.project) || s.project),
+          projectName: getProjectDisplayName(s.project),
           firstUserMessage: s.firstDisplay,
           lastUserMessage: s.lastDisplay,
           lastTimestamp: s.lastTimestamp,
           messageCount: s.promptCount,
           isActive: false,
           ...(worktree && { isWorktree: true, parentRepo: worktree.parentRepo }),
         };
       });

Apply the same swap at lines 863‑865 and 947‑949.

As per coding guidelines: src/**/*.{ts,tsx}: Use TypeScript for all components with strict typing.

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

In `@src/claude-session-utility.ts` around lines 161 - 176, The map callback in
src/claude-session-utility.ts duplicates the worktree/basename fallback logic to
compute projectName; replace the inline expression with a call to the existing
helper getProjectDisplayName (e.g., const projectName =
getProjectDisplayName(s.project, worktree)) and use that for the projectName
property, keeping the existing isWorktree/parentRepo spread; apply the same
replacement at the other two places in this file where the same
basename/worktree expression is duplicated so all three call sites route through
getProjectDisplayName.
🤖 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/claude-session-utility.ts`:
- Around line 1389-1396: The code interpolates unvalidated worktreeName into the
shell command built in claude-session-utility (shortCmd/fullCmd) and launches it
via runCommandInTerminal, which allows shell expansion/command injection; to
fix, add validation in the UI and sanitize/quote before constructing fullCmd:
either enforce a whitelist/regex in the switcher UI (e.g.,
/^[A-Za-z0-9._\/\-]+$/) in launchNewClaudeSessionWorktree so invalid names are
rejected with user feedback, or apply robust shell-quoting/escaping when
building shortCmd/fullCmd (escape all special chars including ", $, `, \\, ;, &,
|, $( )) so the argument is passed literally to claude, and ensure both the
dialog (switcher-ui.tsx) and the claude-session-utility path perform the same
check/escape to prevent bypass.

In `@src/main.ts`:
- Around line 2152-2159: The ipcMain handler passes raw worktreeName into
launchNewClaudeSession which constructs a shell string like claude -w
"${worktreeName}" -n "${worktreeName}", allowing injection via
quotes/backticks/$(); either validate/sanitize worktreeName in the ipc handler
(reject or strip characters like " ' ` $ ( ) and newlines, enforce a safe regex
e.g. /^[A-Za-z0-9._-]+$/) or change launchNewClaudeSession to avoid shell
interpolation by using child_process.spawn/execFile with an args array (e.g.
['-w', worktreeName, '-n', worktreeName]) so the value is passed as an argument
not parsed by the shell; update the ipcMain.on callback to perform the
validation or pass the raw value to the new spawn-style API accordingly.

In `@src/switcher-ui.tsx`:
- Around line 1520-1523: The dropdown hint in the DropdownIndicator component
currently only advertises worktree and external-launch shortcuts; update the
hint string inside DropdownIndicator to also include the existing Shift+Enter
CodeV (embedded terminal) shortcut so it matches the actual supported shortcuts
(the same shortcut referenced around lines 1491-1492). Locate the
DropdownIndicator function/component and append or restore the "Shift+Enter"
text into the displayed string (keeping existing symbols like '\u2318' and
formatting) so users see the embedded terminal shortcut alongside the worktree
and New Claude hints.
- Around line 1682-1797: The modal's clicks and key events are bubbling to
global document handlers (causing focus steal); update the modal container and
inner dialog to stop propagation and trap focus: add onClick={(e) => {
e.stopPropagation(); if (e.target === e.currentTarget) setWorktreeDialog(null);
}} and add onKeyDown={(e) => e.stopPropagation()} on the outer fixed container
(and/or the inner dialog wrapper), and when showing the dialog call
worktreeInputRef.current?.focus() so the branch input receives focus; target the
existing worktreeDialog rendering, the outer fixed div and the inner dialog div
and the worktreeInputRef / input onKeyDown handlers to implement these changes.

---

Nitpick comments:
In `@src/claude-session-utility.ts`:
- Around line 161-176: The map callback in src/claude-session-utility.ts
duplicates the worktree/basename fallback logic to compute projectName; replace
the inline expression with a call to the existing helper getProjectDisplayName
(e.g., const projectName = getProjectDisplayName(s.project, worktree)) and use
that for the projectName property, keeping the existing isWorktree/parentRepo
spread; apply the same replacement at the other two places in this file where
the same basename/worktree expression is duplicated so all three call sites
route through getProjectDisplayName.
🪄 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: 31b3c16a-2a46-41b4-be08-ee9bd2d78a07

📥 Commits

Reviewing files that changed from the base of the PR and between 06014bd and 7dc2e63.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • README.md
  • docs/claude-session-integration-design.md
  • package.json
  • src/claude-session-utility.ts
  • src/electron-api.d.ts
  • src/main.ts
  • src/preload.ts
  • src/switcher-ui.tsx

Comment thread src/claude-session-utility.ts
Comment thread src/switcher-ui.tsx
Comment thread src/switcher-ui.tsx Outdated
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 9 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/claude-session-utility.ts">

<violation number="1" location="src/claude-session-utility.ts:1393">
P1: Unsanitized `worktreeName` is interpolated into a shell command, enabling command injection in terminal launch paths.</violation>
</file>

<file name="src/main.ts">

<violation number="1" location="src/main.ts:2159">
P1: Validate or sanitize `worktreeName` before passing it to the launcher; currently it can break out of the quoted shell argument and inject commands.</violation>
</file>

<file name="src/switcher-ui.tsx">

<violation number="1" location="src/switcher-ui.tsx:1694">
P2: The worktree dialog does not isolate keyboard events, so pressing Tab in the dialog triggers the app-level tab switch shortcut instead of moving focus within the modal.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src/claude-session-utility.ts
Comment thread src/main.ts
Comment thread src/switcher-ui.tsx Outdated
grimmerk and others added 3 commits April 28, 2026 00:20
- Critical: validate worktreeName before shell interpolation.
  isValidWorktreeName() rejects shell metacharacters and bad
  branch-name patterns. Validated at three layers:
    * dialog input (visual error + Launch button disabled)
    * IPC handler (defense in depth)
    * launchNewClaudeSession (defense in depth)
- Major: dialog now traps keydown + click events so background
  shortcuts (Tab, Esc-document, etc.) and project-input refocus
  can't fire while the modal is open. Added role="dialog" +
  aria-modal for a11y.
- Minor: hint text now includes Shift+Enter (Codev embedded
  terminal) so it's discoverable next to the worktree shortcut.
- Docs: noted the IDE/git-GUI clutter trade-off of nested
  worktrees, that A and B aren't mutually exclusive, and that
  -n becomes optional once Ghostty exposes per-tab TTY.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/claude-session-utility.ts (1)

1380-1429: ⚠️ Potential issue | 🟠 Major

Worktree launches are silently downgraded for vscode and codev.

worktreeName is only used after the early vscode/codev returns, so users with either terminal configured still get a plain session even though they went through the new worktree dialog. src/main.ts now forwards the configured terminal into this function, so this path is reachable in normal use. Please either plumb worktreeName through those backends or fail fast instead of ignoring it.

Safe stop-gap
 export const launchNewClaudeSession = (
   projectPath: string,
   terminalApp: string = 'iterm2',
   terminalMode: string = 'tab',
   worktreeName?: string,
 ): void => {
+  if (worktreeName && (terminalApp === 'vscode' || terminalApp === 'codev')) {
+    console.error(
+      '[launchNewClaudeSession] worktree launch is not implemented for terminalApp:',
+      terminalApp,
+    );
+    return;
+  }
   if (terminalApp === 'vscode') {
🤖 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/claude-session-utility.ts`:
- Around line 35-40: parseWorktreePath currently uses a regex that only captures
a single path segment for the worktree name, breaking names containing slashes
(e.g., fix/login-bug); update parseWorktreePath to use a regex that captures the
entire remainder of the path as the worktreeName while stopping at the
`/.claude/worktrees/` boundary for parentRepo (for example change the pattern to
something like /^(.+?)\/\.claude\/worktrees\/(.+?)\/?$/), then return parentRepo
and worktreeName accordingly (no code formatting changes beyond replacing the
regex), and keep in mind isValidWorktreeName allows slash-delimited names.

In `@src/main.ts`:
- Around line 2153-2165: The handler registered via
ipcMain.on('launch-new-claude-session-worktree') currently only checks
existsSync(projectPath) and can accept files (.code-workspace); change the
validation to reject non-directory paths and non-git repositories before calling
launchNewClaudeSession: use fs.statSync(projectPath).isDirectory() (or
lstatSync) to ensure it's a directory, and then verify it's a git
repo/worktree-capable repo (for example by running a quick git check such as
executing "git rev-parse --is-inside-work-tree" or checking for a .git
directory) and return with an error log (similar to the existing
isValidWorktreeName handling) if either check fails; keep the
isValidWorktreeName check and only call launchNewClaudeSession when all
validations pass.
🪄 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: 8f56ddd9-aa0c-4c15-bcc3-8e6b2ec771a6

📥 Commits

Reviewing files that changed from the base of the PR and between 7dc2e63 and 5545f31.

📒 Files selected for processing (4)
  • docs/claude-session-integration-design.md
  • src/claude-session-utility.ts
  • src/main.ts
  • src/switcher-ui.tsx
✅ Files skipped from review due to trivial changes (1)
  • docs/claude-session-integration-design.md

Comment on lines +35 to +40
export const parseWorktreePath = (
p: string,
): { parentRepo: string; worktreeName: string } | null => {
if (!p) return null;
const match = p.match(/^(.+)\/\.claude\/worktrees\/([^/]+)\/?$/);
return match ? { parentRepo: match[1], worktreeName: match[2] } : null;
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

parseWorktreePath() breaks on slash-delimited worktree names.

isValidWorktreeName() explicitly allows names like fix/login-bug, but this regex only captures a single segment after /.claude/worktrees/. For a path like /repo/.claude/worktrees/fix/login-bug, it misidentifies the parent repo as /repo/.claude/worktrees/fix, so the new projectName/isWorktree metadata is wrong across history reads and active-session detection.

Suggested fix
 export const parseWorktreePath = (
   p: string,
 ): { parentRepo: string; worktreeName: string } | null => {
   if (!p) return null;
-  const match = p.match(/^(.+)\/\.claude\/worktrees\/([^/]+)\/?$/);
-  return match ? { parentRepo: match[1], worktreeName: match[2] } : null;
+  const marker = `${path.sep}.claude${path.sep}worktrees${path.sep}`;
+  const idx = p.indexOf(marker);
+  if (idx === -1) return null;
+  const parentRepo = p.slice(0, idx);
+  const worktreeName = p.slice(idx + marker.length).replace(/\/$/, '');
+  if (!parentRepo || !worktreeName) return null;
+  return { parentRepo, worktreeName };
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/claude-session-utility.ts` around lines 35 - 40, parseWorktreePath
currently uses a regex that only captures a single path segment for the worktree
name, breaking names containing slashes (e.g., fix/login-bug); update
parseWorktreePath to use a regex that captures the entire remainder of the path
as the worktreeName while stopping at the `/.claude/worktrees/` boundary for
parentRepo (for example change the pattern to something like
/^(.+?)\/\.claude\/worktrees\/(.+?)\/?$/), then return parentRepo and
worktreeName accordingly (no code formatting changes beyond replacing the
regex), and keep in mind isValidWorktreeName allows slash-delimited names.

Comment thread src/main.ts
Comment on lines +2153 to +2165
ipcMain.on('launch-new-claude-session-worktree', async (_event, projectPath: string, worktreeName: string) => {
if (!existsSync(projectPath)) {
console.log('[launch-new-claude-session-worktree] path does not exist:', projectPath);
return;
}
// Validate at IPC boundary (defense in depth) — the renderer also validates.
if (!isValidWorktreeName(worktreeName)) {
console.error('[launch-new-claude-session-worktree] invalid worktreeName:', JSON.stringify(worktreeName));
return;
}
const terminalApp = ((await settings.get('session-terminal-app')) || 'iterm2') as string;
const terminalMode = ((await settings.get('session-terminal-mode')) || 'tab') as string;
launchNewClaudeSession(projectPath, terminalApp, terminalMode, worktreeName);
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

Reject non-directory targets before starting a worktree session.

This handler only checks existsSync(projectPath), so .code-workspace entries from the Projects list still pass through. The downstream launcher then ends up doing cd "<workspace-file>" && claude -w ..., which fails before Claude starts. Please require a directory here, and ideally a git worktree-capable repo, before forwarding the request.

Suggested hardening
-import { existsSync, readdirSync } from 'fs';
+import { existsSync, readdirSync, statSync } from 'fs';
...
 ipcMain.on('launch-new-claude-session-worktree', async (_event, projectPath: string, worktreeName: string) => {
   if (!existsSync(projectPath)) {
     console.log('[launch-new-claude-session-worktree] path does not exist:', projectPath);
     return;
   }
+  if (!statSync(projectPath).isDirectory()) {
+    console.error('[launch-new-claude-session-worktree] projectPath is not a directory:', projectPath);
+    return;
+  }
   // Validate at IPC boundary (defense in depth) — the renderer also validates.
   if (!isValidWorktreeName(worktreeName)) {
     console.error('[launch-new-claude-session-worktree] invalid worktreeName:', JSON.stringify(worktreeName));
     return;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.ts` around lines 2153 - 2165, The handler registered via
ipcMain.on('launch-new-claude-session-worktree') currently only checks
existsSync(projectPath) and can accept files (.code-workspace); change the
validation to reject non-directory paths and non-git repositories before calling
launchNewClaudeSession: use fs.statSync(projectPath).isDirectory() (or
lstatSync) to ensure it's a directory, and then verify it's a git
repo/worktree-capable repo (for example by running a quick git check such as
executing "git rev-parse --is-inside-work-tree" or checking for a .git
directory) and return with an error log (similar to the existing
isValidWorktreeName handling) if either check fails; keep the
isValidWorktreeName check and only call launchNewClaudeSession when all
validations pass.

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