Repository navigation
Fix Claude fork/resume failing when session changed directories - #5154
Conversation
A Claude session can start in one directory and cd into another (repo root then a worktree). Claude keeps the transcript in the start directory's project folder, but the hook-reported cwd drifts to the later directory. cmux builds the restore snapshot's working directory from that drifted cwd, so fork/resume cd into the wrong directory and claude --resume fails with "No conversation found". These tests assert the restored snapshot resolves to the directory that actually holds the transcript. They fail against current behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restore snapshots took their working directory from the hook-reported cwd, which drifts when the agent cds away from where it started. Claude --resume and --fork-session only find a session from the directory it was created in, so a drifted cwd makes them fail with "No conversation found" even though resuming from the start directory works. For Claude, resolve the snapshot working directory to the candidate directory whose project folder actually holds the transcript: prefer the launch cwd, matched first against the transcript's known storage path and then against the config directory on disk, before falling back to the recorded cwd. Also fix encodeClaudeProjectDir to replace "." with "-" (matching Claude's real project directory naming, e.g. "/x/.claude" -> "-x--claude"), which the match relies on. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughSession restore now computes snapshot workingDirectory via a new helper that prefers the transcript’s project directory for Claude sessions (matching encoded project-dir or probing config roots). The Claude project-dir encoder now replaces both '/' and '.' with '-'. Tests cover drifted-cwd scenarios with and without explicit transcriptPath. ChangesClaude Session Working Directory Resolution
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related issues
Poem
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (16 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryThis PR fixes Claude fork/resume failing with "No conversation found" when an agent session drifts its working directory mid-session. The hook-reported cwd no longer blindly drives the
Confidence Score: 5/5Safe to merge; scoped to Claude snapshot restoration with a fallback chain that preserves pre-PR behavior when no transcript match is found. The restore path has a reliable fallback to the recorded cwd whenever transcript-path matching or config-scan probing produces no match, so the worst case for any uncovered edge is unchanged behavior rather than a new failure. No actor-isolation or concurrency concerns are introduced. The decodeClaudeProjectDir comment in SessionIndexStore.swift is now inconsistent with the updated encoding and should be updated. Reviews (3): Last reviewed commit: "Single-source Claude project-dir encodin..." | Re-trigger Greptile |
| static func encodeClaudeProjectDir(_ path: String) -> String { | ||
| // Claude derives a project directory name by replacing both "/" and "." with "-" | ||
| // (e.g. "/Users/x/repo/.claude" -> "-Users-x-repo--claude"). Missing the "." case | ||
| // sent dotted paths to the wrong project directory. | ||
| path.replacingOccurrences(of: "/", with: "-") | ||
| .replacingOccurrences(of: ".", with: "-") | ||
| } |
There was a problem hiding this comment.
Duplicate
encodeClaudeProjectDir in SessionIndexStore.swift not updated
SessionIndexStore.swift line 877 has its own private copy of encodeClaudeProjectDir that still only replaces / with - and does not replace . with -. That copy is used as the cwdFilter fast path in enumerateClaudeJSONLCandidates: when a filter is set and the encoded name doesn't match Claude's actual directory on disk, the function returns an empty candidate list immediately (no full-enumeration fallback). For any session whose working directory contains a dot (e.g. /Users/foo.bar/project or /Users/foo/node_modules/.bin), the fast path would look for -Users-foo.bar-project while Claude stores the transcript under -Users-foo-bar-project, returning zero results. The fix in this file is correct but incomplete.
There was a problem hiding this comment.
Fixed in d4c5021. Removed the duplicate encodeClaudeProjectDir in SessionIndexStore and routed the cwd-filter fast path to RestorableAgentSessionIndex.encodeClaudeProjectDir, so both discovery paths now encode "." -> "-" identically.
— Claude Code
|
|
||
| let configDir = root.appendingPathComponent("claude-config", isDirectory: true) | ||
| let projectsDir = configDir.appendingPathComponent("projects", isDirectory: true) | ||
| let launchCwd = root.appendingPathComponent("repo", isDirectory: true) | ||
| let driftedCwd = root.appendingPathComponent("worktree", isDirectory: true) | ||
| try fm.createDirectory(at: launchCwd, withIntermediateDirectories: true) | ||
| try fm.createDirectory(at: driftedCwd, withIntermediateDirectories: true) | ||
| try fm.createDirectory( | ||
| at: projectsDir.appendingPathComponent( | ||
| RestorableAgentSessionIndex.encodeClaudeProjectDir(launchCwd.path), | ||
| isDirectory: true | ||
| ), | ||
| withIntermediateDirectories: true | ||
| ) | ||
|
|
||
| let sessionId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" | ||
| let workspaceId = UUID() | ||
| let panelId = UUID() | ||
| try writeClaudeTranscript(sessionId: sessionId, cwd: launchCwd, projectsDir: projectsDir) | ||
| let transcriptPath = projectsDir | ||
| .appendingPathComponent( | ||
| RestorableAgentSessionIndex.encodeClaudeProjectDir(launchCwd.path), | ||
| isDirectory: true | ||
| ) | ||
| .appendingPathComponent("\(sessionId).jsonl", isDirectory: false) | ||
| .path | ||
|
|
||
| try writeClaudeHookStore( | ||
| root: root, | ||
| sessions: [ | ||
| sessionId: driftedHookRecord( | ||
| sessionId: sessionId, | ||
| workspaceId: workspaceId, | ||
| panelId: panelId, | ||
| recordedCwd: driftedCwd.path, | ||
| launchCwd: launchCwd.path, |
There was a problem hiding this comment.
Tests don't exercise the dot-encoding fix in
encodeClaudeProjectDir
Both new tests use path components that contain no dots ("repo", "worktree", "claude-config"), so encodeClaudeProjectDir's new . → - replacement is never exercised by the test suite. If the encoding assumption is wrong for user directories that do contain dots (e.g. /Users/alice.smith/code), the transcript-path matching and config-scan branches would silently fail to match, falling back to the drifted cwd and reproducing the original bug. A test case with dots in the launch-cwd path would directly verify the fix.
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!
There was a problem hiding this comment.
Fixed in d4c5021. The launch path now contains a "." (repo.main), so the "." -> "-" replacement is exercised by both tests.
— Claude Code
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit bd1e119. Configure here.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@cmuxTests/RestorableAgentSessionIndexTests.swift`:
- Around line 214-235: The test currently uses
RestorableAgentSessionIndex.encodeClaudeProjectDir(...) to create the fixture,
which masks regressions; instead hard-code the expected directory name in the
fixture by deriving it from the launchCwd path using the same external rule
(replace "." with "-" in the cwd component(s)) and use that value when creating
and referencing the on-disk Claude project directory and transcriptPath; update
the setup in RestorableAgentSessionIndexTests (variables launchCwd, driftedCwd,
projectsDir, sessionId and the calls to createDirectory and transcriptPath) and
leave the call to writeClaudeTranscript(sessionId:cwd:projectsDir:) unchanged so
the test asserts against an independently-computed expected project dir name
rather than the production helper encodeClaudeProjectDir.
In `@Sources/RestorableAgentSession.swift`:
- Around line 1344-1349: The duplicate project-dir encoding logic diverges:
update Sources/SessionIndexStore.swift to use the same encodeClaudeProjectDir
behavior (replace both "/" and ".") by extracting the logic into a single shared
implementation (e.g., a new static helper or extension) and have both
RestorableAgentSession.encodeClaudeProjectDir and the code in SessionIndexStore
call that shared function so dotted cwd paths resolve identically across both
discovery paths.
🪄 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: bca8745b-2041-4975-aec8-9e40cf9cba98
📒 Files selected for processing (2)
Sources/RestorableAgentSession.swiftcmuxTests/RestorableAgentSessionIndexTests.swift
There was a problem hiding this comment.
2 issues found across 2 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="Sources/RestorableAgentSession.swift">
<violation number="1" location="Sources/RestorableAgentSession.swift:1349">
P1: This updates Claude project-dir encoding in only one code path. If transcript discovery still uses a slash-only encoder elsewhere, dotted cwd values will resolve to different project directory names and valid Claude transcripts can be missed. Please single-source this encoding logic (or update the other encoder) so lookup paths stay consistent.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| // (e.g. "/Users/x/repo/.claude" -> "-Users-x-repo--claude"). Missing the "." case | ||
| // sent dotted paths to the wrong project directory. | ||
| path.replacingOccurrences(of: "/", with: "-") | ||
| .replacingOccurrences(of: ".", with: "-") |
There was a problem hiding this comment.
P1: This updates Claude project-dir encoding in only one code path. If transcript discovery still uses a slash-only encoder elsewhere, dotted cwd values will resolve to different project directory names and valid Claude transcripts can be missed. Please single-source this encoding logic (or update the other encoder) so lookup paths stay consistent.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/RestorableAgentSession.swift, line 1349:
<comment>This updates Claude project-dir encoding in only one code path. If transcript discovery still uses a slash-only encoder elsewhere, dotted cwd values will resolve to different project directory names and valid Claude transcripts can be missed. Please single-source this encoding logic (or update the other encoder) so lookup paths stay consistent.</comment>
<file context>
@@ -1279,7 +1342,11 @@ struct RestorableAgentSessionIndex: Sendable {
+ // (e.g. "/Users/x/repo/.claude" -> "-Users-x-repo--claude"). Missing the "." case
+ // sent dotted paths to the wrong project directory.
path.replacingOccurrences(of: "/", with: "-")
+ .replacingOccurrences(of: ".", with: "-")
}
</file context>
There was a problem hiding this comment.
Fixed in d4c5021. Encoding is now single-sourced across both discovery paths, so dotted cwd values resolve to the same project directory name everywhere.
— Claude Code
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: af00287d34
ℹ️ 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".
| // (e.g. "/Users/x/repo/.claude" -> "-Users-x-repo--claude"). Missing the "." case | ||
| // sent dotted paths to the wrong project directory. | ||
| path.replacingOccurrences(of: "/", with: "-") | ||
| .replacingOccurrences(of: ".", with: "-") |
There was a problem hiding this comment.
Preserve dots when deriving Claude project dirs
For Claude sessions launched from a path containing ., this rewrite makes the transcript lookup use repo-v2 instead of Claude's existing repo.v2 project directory format (the other Claude locator in Sources/SessionIndexStore.swift still documents and implements slash-to-dash only). In the drifted-cwd case this means neither the transcriptPath match nor the config scan finds the launch cwd, so the code falls back to the drifted record.cwd and claude --resume/--fork-session still fails for dotted project paths.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Not applicable — this is contradicted by Claude's actual on-disk behavior. Claude encodes project directories by replacing BOTH / and . with -, including middle dots. Verified against 10 real project dirs under this machine's CLAUDE_CONFIG_DIR; e.g. cwd /private/tmp/cmux-claude-subagent-probe.LIwVJX/project is stored at -private-tmp-cmux-claude-subagent-probe-LIwVJX-project (the middle . became -), and /Users/.../cmuxterm-hq/.claude/worktrees/... is stored at -Users-...-hq--claude-worktrees-.... For every dotted-cwd case, slash-only encoding does NOT match the real dir and slash+dot encoding does. The slash-only encoder this comment references was the pre-existing bug; this PR fixes it and single-sources the encoder so both discovery paths agree.
— Claude Code
Address review feedback (CodeRabbit, Greptile, Cursor, cubic): - SessionIndexStore had its own private encodeClaudeProjectDir that only replaced "/", so the cwd-filter fast path in enumerateClaudeJSONLCandidates (which early- returns) looked in the wrong project directory for any dotted cwd and could miss every transcript for that cwd. Removed the duplicate and route the one call site to RestorableAgentSessionIndex.encodeClaudeProjectDir so both discovery paths encode "." -> "-" identically. - The new regression tests built their on-disk fixtures with the production encodeClaudeProjectDir, so a regression in that helper would have been masked, and used dot-free paths so the "." -> "-" contract was never exercised. They now place fixtures using an independently-computed project-dir name and a launch path containing a ".", so they fail if the encoder regresses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Agent session stores are namespaced by the directory the agent was launched in; cmux was resuming from the agent's drifted runtime cwd (a worktree it cd'd into mid-session), so `<agent> --resume <id>` looked in the wrong namespace and failed with "No conversation found with session ID". Two independent paths carried the drift, fixed via one shared engine: New shared package logic (Packages/CMUXAgentLaunch/AgentResumeWorkingDirectory): classifies each agent kind (cwd-in-file vs directory-namespaced) and resolves the resume working directory, preferring the launch cwd for directory- namespaced agents (Claude, Gemini, Cursor, Grok, Pi, Qoder) and keeping the runtime cwd for id-keyed agents (Codex, OpenCode, Amp, ...). Linked by both the app and the standalone cmux-cli, so there is one source of truth. 1. CLI surface resume binding (publishAgentSurfaceResumeBinding) — the path that re-runs the agent on a restored surface — now pins the binding to the launch cwd via the shared resolver instead of baking the drifted runtime cwd. This is the path that actually failed on a build that already had the Claude-only #5154 fix. 2. App-side resolver (restorableWorkingDirectory) generalized beyond Claude via RestorableAgentKind.cwdNamespacing, which now delegates to the shared classifier. Claude still verifies against the transcript first. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Summary
Forking (and restoring) a Claude conversation could fail with
No conversation found with session ID: ...when the session had moved directories since it started.Claude stores each transcript under the project directory derived from the cwd the session was created in, and
claude --resume/--fork-sessiononly locate it from that same directory. cmux's Claude hook reports the agent's current cwd, which drifts when the agentcds elsewhere mid-session (a very common pattern: start in a repo root, then move into a worktree for the rest of the session). The restore snapshot took its working directory from that drifted cwd, so fork/resumecdinto the wrong directory and Claude can't find the transcript there.Observed concretely: session
a1fcdb44started in/Users/lawrence/fun/cmuxterm-hq, so its transcript lives atprojects/-Users-lawrence-fun-cmuxterm-hq/a1fcdb44-….jsonl. 27 seconds in itcd'd intoworktrees/feat-ios-swift-mobile-coreand worked there for ~2 days. A fork launched into the worktree failed; a fork launched from the start directory succeeded. Same session, only the directory differed.Fix: for Claude, resolve the restore snapshot's working directory to the candidate directory whose project folder actually holds the transcript (prefer the launch cwd, matched first against the transcript's known storage path and then against the config directory on disk), before falling back to the recorded cwd. No transcript is copied; cmux just
cds into where it already lives. Also fixesencodeClaudeProjectDirto replace.with-, matching Claude's real project-directory naming (e.g./x/.claude->-x--claude), which the match relies on.This restores consistency: resume already used the launch directory and worked; fork now does the same.
Testing
RestorableAgentSessionIndexTests.testClaudeForkResolvesDriftedCwdViaTranscriptPathand...ViaConfigScanWhenTranscriptPathMissing. Two-commit red/green: the first commit adds the tests (red), the second adds the fix (green). They build a hook store whose recorded cwd is a drifted sibling directory while the transcript lives under the launch cwd, then assert the restored snapshot's working directory andforkCommandcdinto the launch cwd, not the drift.cmux-unit/testsjob); local test runs are disabled in this environment.Issues
Scope and known follow-up
This PR fixes the reported symptom: forking a Claude conversation (a snapshot-driven action) that had changed directories. Verified empirically: the same session forks successfully from its start directory but failed from the drifted directory.
The same root-cause drift also reaches the auto-resume binding path (
source: agent-hook,--resume), whose cwd originates in the CLI hook (CLI/cmux.swift) recording Claude's reported cwd and is re-applied at restore viareplacingRequiredChangeDirectoryPrefixusing the binding's own cwd, bypassing the snapshot fixed here. A persisted drifted--resumebinding can therefore still fail auto-resume. Fixing that cleanly spans a second target (the CLI hook) with broader blast radius on the shared auto-resume path, so it is intentionally left as a focused follow-up rather than rushed into this PR.Note
Medium Risk
Changes how Claude restore snapshots choose working directories and unifies project-dir encoding used for transcript lookup and session-index cwd filters; behavior is well covered by new tests but affects fork/resume shell commands.
Overview
Fixes Claude fork and restore when the agent’s hook-reported
cwdhas drifted after mid-sessioncd(e.g. repo root → worktree). Restore snapshots now setworkingDirectoryviarestorableWorkingDirectory: for Claude only, pick launch vs recorded cwd by matching the transcript’s on-disk project folder (fromtranscriptPathwhen present, else scanCLAUDE_CONFIG_DIR), so generatedforkCommand/ resume shellscdwhere Claude actually stores the session.Also corrects
encodeClaudeProjectDirto map.→-like Claude’s project dirs, and single-sources that helper inSessionIndexStorefor cwd-scoped session listing so dotted paths stay consistent with discovery.Adds regression tests for drifted-cwd fork resolution (with and without
transcriptPath).Reviewed by Cursor Bugbot for commit d4c5021. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
Bug Fixes
Tests