Skip to content

Fix Claude fork cwd drift - #5149

Closed
lawrencecchen wants to merge 4 commits into
mainfrom
task-claude-fork-session-resume
Closed

lawrencecchen wants to merge 4 commits into
mainfrom
task-claude-fork-session-resume

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Preserve Claude hook transcriptPath on restorable snapshots.
  • Use the transcript project directory for Claude resume/fork cd prefixes when hook cwd has drifted.

Testing

  • ./scripts/reload.sh --tag cldfork passed.
  • AWS targeted xcodebuild test was attempted, but the runner's xcodebuild crashed during Swift package resolution before compiling.

Issues

  • Related: Claude Code fork command failed with No conversation found with session ID when the transcript lived under the original project cwd but the hook cwd had moved to a worktree.

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.


Note

Medium Risk
Changes how Claude resume/fork shell commands choose the working directory; incorrect matching could still run Claude from the wrong folder, though scope is limited to restorable Claude sessions and regression tests were added.

Overview
Fixes Claude resume/fork when the hook cwd has moved (e.g. into a worktree) but the conversation transcript still lives under the original project folder.

Restorable snapshots now carry Claude transcriptPath from hook records into SessionRestorableAgentSnapshot, and AgentResumeCommandBuilder uses it when building the shell cd prefix for Claude resume/fork commands.

For Claude only, the builder reads the encoded project directory name from the transcript path (…/projects/<encoded>/session.jsonl) and picks a working directory by matching that name against encodeClaudeProjectDir on hook/launch cwd candidates (with tilde expansion)—not by reversing hyphens to slashes. If nothing matches, behavior falls back to the previous cwd logic.

SessionIndexStore drops its local encodeClaudeProjectDir duplicate in favor of RestorableAgentSessionIndex.encodeClaudeProjectDir. New tests cover cwd drift and avoiding lossy decode on hyphenated project dir names.

Reviewed by Cursor Bugbot for commit 170a021. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Fixes Claude resume/fork when the hook cwd drifts by deriving the cd prefix from the transcript’s project directory using a shared encoder for consistent matching. Also avoids lossy decoding of encoded transcript project folders, preventing bad cd paths and the “No conversation found with session ID” error.

  • Bug Fixes
    • Preserve Claude transcriptPath on restorable snapshots so resume/fork can locate the original project.
    • Compute the cd prefix by matching the transcript’s encoded project folder to candidate cwds with a shared encoder; if no match, fall back to the previous cwd (no hyphen→slash decoding).
    • Add regression tests for transcript project cwd selection and for avoiding lossy decoding.

Written for commit 170a021. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Resume and fork now carry optional transcript information so sessions can better restore context; snapshots persist transcript info.
  • Bug Fixes

    • Improved working-directory resolution to prefer transcript-derived project directories where applicable and avoid embedding drifted hook paths.
  • Tests

    • Added coverage for transcript-based working-directory behavior and hardened transcript writing in tests.

@vercel

vercel Bot commented Jun 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 2, 2026 12:33am
cmux-staging Building Building Preview, Comment Jun 2, 2026 12:33am

@coderabbitai

coderabbitai Bot commented Jun 1, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Threads optional transcriptPath through agent resume/fork command builders, adds Claude transcript-based project-directory resolution for working-directory selection, stores transcriptPath in snapshots and hydrations, updates SessionIndexStore to use a centralized encoder, and adds tests for cwd-drift behavior.

Changes

Transcript-based working directory resolution

Layer / File(s) Summary
Command builder API—add transcriptPath parameter
Sources/RestorableAgentSession.swift
resumeShellCommand and forkShellCommand accept an optional transcriptPath and forward it into the internal shellCommand helper; shellCommand signature updated.
Working directory resolution with Claude transcript mapping
Sources/RestorableAgentSession.swift
New shellWorkingDirectory() and claudeTranscriptProjectDirName() compute cwd: when prefixing is enabled and registration allows, a Claude project dir is derived from transcriptPath+sessionId and matched (including tilde-expanded variants) against candidates; otherwise fallback to normalized candidate.
Snapshot integration—store and propagate transcriptPath
Sources/RestorableAgentSession.swift
SessionRestorableAgentSnapshot adds stored transcriptPath: String? and passes it from resumeCommand/forkCommand into the updated command builders; hydration reads record.transcriptPath.
SessionIndexStore—use centralized Claude encoder
Sources/SessionIndexStore.swift
Removed local encodeClaudeProjectDir and switched candidate encoding to RestorableAgentSessionIndex.encodeClaudeProjectDir(...).
Test validation and helper updates
cmuxTests/RestorableAgentSessionIndexTests.swift
Adds testClaudeForkCommandUsesTranscriptProjectWorkingDirectoryWhenHookCwdDrifts; extends hookRecord to accept launchWorkingDirectory; ensures transcript parent directory is created before writing JSONL.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~35 minutes

Possibly related issues

Possibly related PRs

  • manaflow-ai/cmux#4859: Both PRs modify RestorableAgentSession.swift working-directory prefixing logic in AgentResumeCommandBuilder.shellCommand, with this PR adding transcriptPath-based directory selection.
  • manaflow-ai/cmux#4683: Overlaps at restorable-agent shell-command construction and quoting/tokenization changes; related to command-generation code.

Poem

🐰 I threaded a path through scripts and guides,
A transcript's whisper where the project hides.
Claude's project-name decoded with care,
The shell steps home — no drift to spare.
Hooray for tidy resuming, hop and share!


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (2 errors, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error Introduces Task.sleep(60 seconds) in production code (SessionDragRegistry.register) for auto-expiry, violating the rule prohibiting sleep-based synchronization without a real signal. Replace Task.sleep with a Timer or cancellation token; implement proper cleanup without blocking delays, as the rule requires a real signal/callback/notification.
Cmux Swift @Concurrent ❌ Error Multiple nonisolated async functions use Task.detached without @concurrent: defaultAgentOrder and vaultAgentRegistry in SessionIndexStore, plus three load functions in RestorableAgentSession. Add @concurrent annotation to nonisolated async functions that use Task.detached to leave the caller's actor context in both new files.
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.
Description check ❓ Inconclusive PR description covers the main changes and testing approach, but lacks some template sections and details on manual verification. Add more details on what was manually verified, include a demo video link if applicable, and confirm all checklist items (testing locally, bot reviews, comment resolutions).
✅ Passed checks (14 passed)
Check name Status Explanation
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.
Cmux Swift Actor Isolation ✅ Passed PR adds transcriptPath property to existing struct without introducing new actor isolation issues. Struct's missing @nonisolated is existing debt, not worsened by these changes.
Cmux No Hacky Sleeps ✅ Passed Custom check applies only to TypeScript, JavaScript, shell, or build/runtime scripts; all PR changes are in Swift files (.swift), which are explicitly out of scope for this check.
Cmux Algorithmic Complexity ✅ Passed Fixed-size 2-element loop in shellWorkingDirectory; encodeClaudeProjectDir used once per cwdFilter as optimization, not in scalable-collection loop.
Cmux Swift Concurrency ✅ Passed PR introduces no legacy async patterns: only synchronous parameter threading and helpers in RestorableAgentSession.swift, function refactoring in SessionIndexStore.swift, and test-only code in XCTest.
Cmux Swift File And Package Boundaries ✅ Passed PR adds +75 lines to RestorableAgentSession.swift (below 250-line threshold for oversized files), reduces SessionIndexStore via refactoring, focuses on Claude session restoration logic.
Cmux Swift Logging ✅ Passed No Swift logging violations found. All code uses Apple's unified Logger with proper nonisolated isolation and privacy redaction; no print/debugPrint/dump/NSLog in production code.
Cmux User-Facing Error Privacy ✅ Passed PR contains no user-facing error messages, alerts, or API responses that expose vendor names, session IDs, credentials, or other sensitive information. All changes are internal implementation details.
Cmux Full Internationalization ✅ Passed PR adds only internal parameters and properties (transcriptPath) to RestorableAgentSession. No user-facing strings, UI text, or localization requirements in the changes.
Cmux Swiftui State Layout ✅ Passed No new SwiftUI state patterns added. Changes are to non-UI data models in RestorableAgentSession.swift and SessionIndexStore.swift; neither file imports SwiftUI or adds @Published/@observable state.
Cmux Architecture Rethink ✅ Passed Small correctness fix with clear ownership and invariants: threads transcript path through command builders to resolve working directory drift without timing/dispatch patches.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR modifies session restoration code with no NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup changes. The window shortcuts check does not apply.
Title check ✅ Passed The title 'Fix Claude fork cwd drift' directly and clearly summarizes the main change: addressing a working directory drift issue in Claude fork/resume functionality.
✨ 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 task-claude-fork-session-resume

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d9e28257ff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

let transcriptURL = projectsDir
.appendingPathComponent(RestorableAgentSessionIndex.encodeClaudeProjectDir(projectCwd.path), isDirectory: true)
.appendingPathComponent("\(sessionId).jsonl", isDirectory: false)
try writeClaudeTranscript(sessionId: sessionId, transcriptURL: transcriptURL, cwd: projectCwd)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Create the Claude projects directory before writing

This new test builds transcriptURL under claude-config/projects/<encoded cwd>/..., but the only directory created beforehand is hookCwd under the project tree. Unlike the neighboring tests, nothing creates projectsDir or the encoded project directory, so writeClaudeTranscript(..., transcriptURL: ...) fails with a missing-parent-directory error before any assertions run, making the test suite fail whenever this test is executed.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Jun 1, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes Claude resume/fork failing with "No conversation found with session ID" when the hook cwd has drifted (e.g. into a git worktree) while the transcript still lives under the original project directory.

  • transcriptPath is now stored on SessionRestorableAgentSnapshot and threaded through resumeShellCommand/forkShellCommand so the cd prefix can be derived from the transcript's encoded project directory name instead of the drifted hook cwd.
  • shellWorkingDirectory encodes each candidate cwd (hook cwd, then launch cwd) and compares against the project folder name parsed from the transcript path; if neither matches it falls back to the previous behavior, avoiding any lossy hyphen→slash decoding.
  • The duplicate encodeClaudeProjectDir in SessionIndexStore is removed, leaving a single canonical implementation on RestorableAgentSessionIndex.

Confidence Score: 5/5

Safe to merge; the cwd selection logic is strictly additive and falls back to prior behavior when the transcript path is absent or unrecognized.

All three previously flagged issues (lossy hyphen decoding, hardcoded FileManager, duplicate encode helper) are resolved in this revision. The new shellWorkingDirectory path only activates for Claude sessions with a valid transcriptPath; all other agents and all nil-transcript Claude sessions follow the original fallback. Regression tests cover the drift scenario and the hyphenated-name edge case. No new blocking issues found.

No files require special attention.

Important Files Changed

Filename Overview
Sources/RestorableAgentSession.swift Core fix: adds transcriptPath field to snapshot and shellWorkingDirectory helper that encodes candidate cwds and matches against the transcript's project dir name; no decoding, safe fallback to pre-fix behavior when no match is found.
Sources/SessionIndexStore.swift Removes duplicate encodeClaudeProjectDir and delegates to the now-public RestorableAgentSessionIndex.encodeClaudeProjectDir; straightforward deduplication.
cmuxTests/RestorableAgentSessionIndexTests.swift Adds two regression tests: cwd-drift selects transcript project dir over drifted hook cwd, and hyphenated project path is never lossily decoded; writeClaudeTranscript now creates parent directories.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[resumeShellCommand / forkShellCommand] --> B[shellWorkingDirectory]
    B --> C{includeWorkingDirectoryPrefix AND cwd != .ignore?}
    C -- No --> D[return nil]
    C -- Yes --> E[fallback = normalized workingDirectory ?? launchCmd.workingDirectory]
    E --> F{kind == .claude AND transcriptPath extractable?}
    F -- No --> G[return fallback]
    F -- Yes --> H[Extract projectDirName from transcriptPath]
    H --> I[Encode workingDirectory compare to projectDirName]
    I -- Match --> J[return workingDirectory]
    I -- No match --> K[Encode launchCmd.workingDirectory compare to projectDirName]
    K -- Match --> L[return launchCmd.workingDirectory]
    K -- No match --> M[return fallback]
    J --> N[cd prefix in shell command]
    L --> N
    M --> N
    G --> N
Loading

Reviews (2): Last reviewed commit: "Share Claude project directory encoder" | Re-trigger Greptile

Comment thread Sources/RestorableAgentSession.swift Outdated
Comment on lines +451 to +461
private static func decodeClaudeProjectDir(_ raw: String) -> String? {
guard !raw.isEmpty else { return nil }
let stripped = raw.hasPrefix("-") ? String(raw.dropFirst()) : raw
let candidate = "/" + stripped.replacingOccurrences(of: "-", with: "/")
var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: candidate, isDirectory: &isDirectory),
isDirectory.boolValue else {
return nil
}
return candidate
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 decodeClaudeProjectDir inverts encoding incorrectly for hyphenated paths

The encoding replaces / → -, but hyphens already present in directory names are also stored as -. Decoding by replacing ALL - → / conflates both, producing a wrong candidate path. For a project at /Users/alice/my-project the encoded name is -Users-alice-my-project; decoding yields /Users/alice/my/project, which almost certainly doesn't exist, so fileExists returns false and the function returns nil. The outer caller then falls back to the hook cwd — exactly the drifted cwd the fix is meant to avoid. Any session whose launchCommand?.workingDirectory is absent or nil will silently use the wrong prefix when the project root contains a hyphen.

Comment thread Sources/RestorableAgentSession.swift Outdated
Comment on lines +456 to +458
guard FileManager.default.fileExists(atPath: candidate, isDirectory: &isDirectory),
isDirectory.boolValue else {
return nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 FileManager.default hardcoded in a static helper

Every other filesystem access in the loading pipeline accepts a FileManager parameter for testability and sandbox correctness. This helper hardcodes FileManager.default, making it impossible to test with a mock filesystem in isolation and inconsistent with the surrounding codebase style.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread Sources/RestorableAgentSession.swift Outdated
Comment on lines +447 to +449
private static func encodeClaudeProjectDir(_ path: String) -> String {
path.replacingOccurrences(of: "/", with: "-")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Duplicate encodeClaudeProjectDir implementations

AgentResumeCommandBuilder.encodeClaudeProjectDir (added here) is identical to RestorableAgentSessionIndex.encodeClaudeProjectDir (line 1372) and SessionIndexStore.encodeClaudeProjectDir (SessionIndexStore.swift line 877). Three copies of the same one-liner with no shared source of truth means any future fix or change to the encoding scheme must be applied in three places.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/RestorableAgentSession.swift`:
- Around line 451-461: The decodeClaudeProjectDir function performs a lossy
decode of encoded paths containing literal hyphens (e.g., "my-project" ->
"my/project"), so add a brief inline comment above the private static func
decodeClaudeProjectDir(_ raw: String) noting that the encode→decode
transformation is not invertible for paths containing hyphens, that this is an
intentional last-resort fallback used only after other matching logic and
guarded by FileManager.fileExists, and that callers should not rely on this
function for exact reversible encoding/decoding if hyphens may be present.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d2dacd4f-f552-48ab-b234-3a33ef4bcc30

📥 Commits

Reviewing files that changed from the base of the PR and between 8ba0ee5 and d9e2825.

📒 Files selected for processing (2)
  • Sources/RestorableAgentSession.swift
  • cmuxTests/RestorableAgentSessionIndexTests.swift

Comment thread Sources/RestorableAgentSession.swift Outdated

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

3 issues found across 2 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread cmuxTests/RestorableAgentSessionIndexTests.swift
Comment thread Sources/RestorableAgentSession.swift Outdated
Comment thread Sources/RestorableAgentSession.swift Outdated
@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — 170a0213 Deployed Jun 2, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants