Skip to content

Pin Claude auto-resume binding to the launch cwd (#4256) - #6741

Open
endmoseung wants to merge 3 commits into
manaflow-ai:mainfrom
endmoseung:fix/claude-resume-binding-cwd-drift
Open

endmoseung wants to merge 3 commits into
manaflow-ai:mainfrom
endmoseung:fix/claude-resume-binding-cwd-drift

Conversation

@endmoseung

@endmoseung endmoseung commented Jun 24, 2026 •

Copy link
Copy Markdown

Summary

Fixes the Claude auto-resume binding path (source: agent-hook) so a restored session cds into the directory Claude filed the transcript under, not a runtime cwd the agent drifted into mid-session. Resolves #4256.

Claude namespaces each transcript under <config>/projects/<encoded launch cwd>/<id>.jsonl, and claude --resume <id> only locates it from that same cwd. When a session starts in one dir and the agent later cds elsewhere (repo root → worktree, or $HOME → a subdir), the persisted binding pinned the drifted cwd, so auto-resume ran cd '<drift>' && claude --resume <id> and failed with No conversation found.

This is the follow-up that #5154 explicitly deferred:

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) … intentionally left as a focused follow-up.

#5154 fixed the snapshot fork/restore path (Sources/RestorableAgentSession.swift); the binding published by publishAgentSurfaceResumeBinding in CLI/cmux.swift was untouched.

Why a candidate match isn't enough

In the real failure (captured from a live 0.64.9 session, see #4256) both inputs to AgentResumeWorkingDirectory().resolve(...) are the drift by the time the binding is published: the runtime cwd is the drift, and the captured launchCommand.workingDirectory also collapsed to the drift (SessionStart fired after the agent moved, and the hook process had no trusted CMUX_AGENT_LAUNCH_CWD, so agentLaunchCommandFromEnvironment fell back to parsedInput.cwd). The true launch cwd is therefore not among the candidates.

What is reliable: every Claude transcript record carries the launch cwd in a top-level "cwd", and transcript_path is in the hook payload. So the launch cwd is recovered from the transcript content and accepted only when it re-encodes to the transcript's project dir (no lossy decode).

Changes

  • New shared type ClaudeResumeWorkingDirectory + ClaudeProjectDirEncoding in CMUXAgentLaunch (single source of truth for both targets). verifiedWorkingDirectory(...):
    1. matches a candidate dir's encoding to the transcript path's project-dir segment;
    2. else recovers the launch cwd from the transcript file's top-level "cwd", accepted only if it re-encodes to that same project dir;
    3. else probes the Claude config roots on disk.
  • CLI binding builder publishAgentSurfaceResumeBinding resolves the Claude resume cwd through it before the existing namespacing fallback, gated to kind == "claude" (other agents unchanged). transcriptPath is threaded through all 8 call sites.
  • App snapshot resolver delegates to the same shared type, removing the duplicated transcript-verification logic (no behavior change; the existing Fix Claude fork/resume failing when session changed directories #5154 tests cover it).

Testing

  • CMUXAgentLaunch package: swift test green (added ClaudeResumeWorkingDirectoryTests — candidate match, transcript-content recovery, round-trip guard rejection, config scan, empty inputs; full suite 161 tests pass).
  • Added a CLI hook integration test (cmuxTests/CLIGenericHookPersistenceTests.swift) that reproduces the drift (both candidates = drift, transcript under the launch dir) and asserts the published surface.resume.set cwd/command pin the launch dir.

⚠️ Disclosure: I could not run the full Xcode app/CLI build or the cmuxTests target locally (this machine is missing CoreSimulator.framework, so xcodebuild fails to load IDESimulatorFoundation before compiling). The shared-package logic and its unit tests are verified via swift test; the CLI/app integration compiles against the same shared API but its full build + the new integration test should be confirmed by CI.

Residual notes for reviewers

  • If Claude ever changes its project-dir encoding, ClaudeProjectDirEncoding is now the one place to update (used by both app and CLI).
  • The round-trip guard is intentionally strict: it only accepts a recovered cwd that re-encodes to the transcript's project dir, so a foreign/spoofed cwd in transcript content can't be honored.
  • The transcript read is a bounded 64 KB head read with per-line JSON parsing; a mid-codepoint UTF-8 cut yields nil and falls through safely.

🤖 Generated with Claude Code


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


Summary by cubic

Pins Claude auto-resume to the original launch directory and verifies it against the transcript to prevent “No conversation found” after mid-session cd. Resolves #4256.

  • Bug Fixes

    • Verifies the resume cwd with ClaudeResumeWorkingDirectory: match the transcript’s project dir, else recover from transcript content with a round-trip check, else scan config roots; supports nested <id>/messages/<id>.jsonl.
    • Updates CLI publishAgentSurfaceResumeBinding (Claude only) to resolve via transcript before namespacing and threads transcriptPath; fixes non-optional sessionId handling.
  • Refactors

    • Extracts ClaudeProjectDirEncoding and centralizes logic in @macOS/CMUXAgentLaunch.
    • App snapshot resolver now delegates to one shared resolver per index-load pass with memoized config-root/transcript lookups, removing duplicate verification code.

Written for commit daa09dd. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Improved Claude resume handling by verifying and resolving the correct working directory from the session transcript, even when the current CWD has drifted.
    • Resume/session flows now better preserve and propagate transcript path context for more consistent continuation behavior.
  • Bug Fixes

    • Fixed resume targeting the wrong folder by validating the expected project-dir encoding and, when needed, recovering the launch CWD from transcript JSONL records.
    • Added more reliable fallback behavior (including honoring CLAUDE_CONFIG_DIR) when transcript lookup isn’t provided.
  • Tests

    • Added coverage for transcript-derived CWD recovery and Claude project directory encoding rules.

endmoseung and others added 2 commits June 24, 2026 18:57
…anscript

Claude files each session transcript under the project directory derived from
the cwd the session was *created* in, and `claude --resume <id>` only finds it
from that same directory. A resume that `cd`s into a directory the agent later
drifted into (e.g. repo root -> worktree) fails with "No conversation found".

Add `ClaudeResumeWorkingDirectory` to the shared CMUXAgentLaunch package as the
single source of truth for resolving a Claude resume cwd:
- match a candidate dir's encoding to the transcript path's project-dir segment,
- else recover the launch cwd from the transcript file's top-level `cwd` field,
  accepting it only when it re-encodes to that same project dir (no lossy decode),
- else probe the Claude config roots on disk.

Also extract `ClaudeProjectDirEncoding` (// and . both map to -) so the app and
CLI agree on Claude's project-dir naming. Covered by unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Claude auto-resume binding (source: agent-hook) pinned the agent's runtime
cwd, so after a mid-session `cd` (e.g. home -> a subdirectory) cmux's automatic
restore ran `cd '<runtime-cwd>' && claude --resume <id>` and failed with
"No conversation found" — the transcript lives under the launch cwd.

PR manaflow-ai#5154 fixed the snapshot fork/restore path for this same drift and explicitly
left the auto-resume *binding* path (the CLI hook) as a follow-up. This is that
follow-up.

- `publishAgentSurfaceResumeBinding` (CLI/cmux.swift) now resolves the Claude
  resume cwd through the shared `ClaudeResumeWorkingDirectory` before the
  existing namespacing fallback, gated to `kind == "claude"` so other agents are
  untouched. `transcriptPath` is threaded through all 8 call sites.
- The app snapshot resolver delegates to the same shared type, removing the
  duplicated transcript-verification logic.

Reproduces in the real failure where the launch capture itself collapsed to the
drift (SessionStart fired after the agent moved, no trusted CMUX_AGENT_LAUNCH_CWD):
both candidates are the drift, so the launch cwd is recovered from the transcript
content. Covered by a CLI hook integration test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 24, 2026

Copy link
Copy Markdown

@endmoseung is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds shared Claude transcript resume helpers, wires them into restorable-session and CLI resume paths, propagates transcript paths through session records, and adds tests for transcript-based working-directory recovery and resume binding.

Changes

Claude Resume Working-Directory Recovery

Layer / File(s) Summary
Claude transcript resume helper
Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptResume.swift
Adds ClaudeProjectDirEncoding.projectDirName(forPath:), ClaudeResumeWorkingDirectory.verifiedWorkingDirectory(...), and TranscriptLookupCache for config-root discovery, transcript lookup, and transcript-head cwd recovery.
Restorable session wiring
Sources/RestorableAgentSession.swift
Creates and reuses a shared ClaudeResumeWorkingDirectory, passes it into restorable working-directory resolution, and replaces the prior Claude-specific lookup path with the shared verifier and shared encoding.
CLI transcriptPath propagation and Claude resume check
CLI/cmux.swift
Adds transcriptPath to session/resume records across construction sites and uses ClaudeResumeWorkingDirectory().verifiedWorkingDirectory(...) for Claude resume working-directory selection before falling back to the generic resolver.
Claude resume tests
Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/ClaudeResumeWorkingDirectoryTests.swift, cmuxTests/CLIGenericHookPersistenceTests.swift
Covers transcript-path resolution, config-dir fallback, transcript-content recovery, nested transcript layouts, invalid inputs, encoding behavior, and the end-to-end resume binding flow with drifted cwd.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • manaflow-ai/cmux#5154: Updates Claude resume working-directory recovery using transcript-derived project-dir encoding and drifted-cwd handling.
  • manaflow-ai/cmux#5242: Also changes Claude restorable-session loading in Sources/RestorableAgentSession.swift by altering how transcript-related session data is resolved.
  • manaflow-ai/cmux#5300: Also adjusts resume working-directory selection in CLI/cmux.swift and Sources/RestorableAgentSession.swift to avoid using a drifted cwd.

Poem

🐇 I sniffed the trail in JSONL light,
And found the launch-day cwd just right.
With dashes danced from dots and slashes too,
The resume hop now lands where sessions grew.
No drifted burrow can mislead my nose —
The transcript knows the path it chose.


Important

Pre-merge checks failed

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

❌ Failed checks (3 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Expensive Synchronous Load ❌ Error CLI hook path now synchronously calls verifiedWorkingDirectory, which scans config roots and reads transcripts on resume binding; no off-main cache/background hop is used. Move Claude transcript verification into an off-main cached loader (e.g. SharedLiveAgentIndex.shared/Task.detached) and return only the resolved cwd to the hook path.
Cmux Cache Substitution Correctness ❌ Error On-disk Claude transcript/config-root probes were memoized in snapshot/binding paths, but stale cache hits/misses aren’t freshness-checked or invalidated. Make the cache event-driven or freshness-checked, or drop memoization for these correctness-sensitive restore paths and read disk fresh each time.
Cmux No Ambient Global State ❌ Error Packages/macOS/CMUXAgentLaunch/.../ClaudeTranscriptResume.swift adds public caseless enum ClaudeProjectDirEncoding with only a static helper, matching the banned static-namespace shape. Move projectDirName(forPath:) onto an owning injectable type (e.g. ClaudeResumeWorkingDirectory or a dedicated encoder instance) and keep helpers private/fileprivate.
Docstring Coverage ⚠️ Warning Docstring coverage is 19.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (21 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately highlights the main change: pinning Claude auto-resume to the launch cwd.
Description check ✅ Passed The description covers summary, why, testing, review trigger, and checklist; only the demo-video field is left empty.
Linked Issues check ✅ Passed The changes match #4256 by restoring Claude resume to the original launch cwd and handling transcript-based recovery.
Out of Scope Changes check ✅ Passed The new shared resolver, CLI wiring, and tests all support the Claude resume fix; no unrelated changes stand out.
Cmux Swift Actor Isolation ✅ Passed No new actor-isolation debt: the added helpers are plain package types, with no @MainActor/Observable misuse; the mutable cache is private and non-Sendable.
Cmux Swift Blocking Runtime ✅ Passed Touched Swift code adds file-based transcript verification/caching only; no new waits, sleeps, semaphores, syncs, or locks were introduced.
Cmux Browser Automation Off-Main ✅ Passed PR only updates submodule pointers; the changed submodule files are unrelated to browser automation, and no browser/WebKit/socket-worker command routing was touched.
Cmux No Hacky Sleeps ✅ Passed New runtime code is deterministic file/probe logic; no added sleeps/timers/polling. Only XCTest timeout waits appear in tests, which are exempt.
Cmux Algorithmic Complexity ✅ Passed The new filesystem scans are cached per resolver/load pass, and the only per-call candidate iteration is tiny-bounded or linear; no new nested rescans on scalable collections.
Cmux Swift Concurrency ✅ Passed Changed Swift code is synchronous path-resolution logic plus XCTest coverage; no new DispatchQueue, Combine, completion-handler, or fire-and-forget Task patterns were introduced.
Cmux Swift @Concurrent ✅ Passed No new @concurrent/nonisolated async misuse was introduced; the added Claude resolver is synchronous and used from sync code paths.
Cmux Swift File And Package Boundaries ✅ Passed New resolver logic lives in a 326-line CMUXAgentLaunch package file with tests; CLI/app changes are thin glue, and the 1870-line app file was only lightly touched.
Cmux Swiftpm Lockfiles ✅ Passed Diff only bumps vendored submodule gitlinks (ghostty, vendor/bonsplit); no cmux-owned Package.swift/.gitignore/Package.resolved or Xcode package-reference files changed.
Cmux Swift Logging ✅ Passed No forbidden logging was added or changed in the touched runtime Swift files; existing prints are user-facing CLI output, and tests are exempt.
Cmux User-Facing Error Privacy ✅ Passed No user-facing error/alert/output copy changed; the diff only adjusts internal Claude resume cwd resolution and adds tests/comments.
Cmux Full Internationalization ✅ Passed The PR only changes resume-resolution logic and tests; it adds no new user-facing copy or locale assets, and existing CLI text stays routed through localized APIs.
Cmux Swiftui State Layout ✅ Passed No touched SwiftUI view/state code; the diff is CLI/package/test plumbing, so the state-layout rule doesn’t apply.
Cmux Architecture Rethink ✅ Passed Diff centralizes Claude resume cwd resolution in one shared resolver, with no new timing hacks or split UI ownership; the cache is local and ephemeral.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Only submodule pointers changed; the updated submodule commits touch Zig/C files, not Swift window code or cmuxAuxiliaryWindowIdentifiers.
Cmux Source Artifacts ✅ Passed Only changed paths are submodule gitlinks (ghostty, vendor/bonsplit); no logs, caches, temp dirs, or build artifacts were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No new test/debug seam in production: the new Claude helpers are production APIs, with call sites in CLI and app code, and no added DEBUG/test-only exposure.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@greptile-apps

greptile-apps Bot commented Jun 24, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes claude --resume failing with "No conversation found" when an agent drifts to a different working directory mid-session. The bug root was that publishAgentSurfaceResumeBinding pinned the auto-resume cd path to the drifted runtime cwd rather than the launch cwd Claude namespaced the transcript under.

  • Introduces ClaudeResumeWorkingDirectory + ClaudeProjectDirEncoding in the CMUXAgentLaunch package as the shared source of truth, implementing a three-step resolution: candidate-encoding match against the transcript's project-dir segment → transcript-content cwd recovery with round-trip guard → config-root probe.
  • Threads transcriptPath through all 8 CLI call sites of publishAgentSurfaceResumeBinding and routes Claude sessions through the shared resolver before the existing AgentResumeWorkingDirectory namespacing fallback; deletes the duplicated claudeVerifiedRestorableWorkingDirectory from RestorableAgentSession.swift and delegates to the same type.
  • Coverage added: ClaudeResumeWorkingDirectoryTests (unit, 5 cases) and a CLI integration test in CLIGenericHookPersistenceTests reproducing the exact failure scenario (both candidates = drift, cwd recovered from transcript).

Confidence Score: 5/5

Safe to merge — the fix is well-scoped, tested end-to-end, and the shared resolver is created once outside the session loop so no new per-record disk scans are introduced on the app's index load path.

The three-step resolution logic (encoding match → transcript-content recovery → config-root probe) is correct and guarded by a strict round-trip check that prevents spoofed paths from being accepted. The shared ClaudeResumeWorkingDirectory instance in RestorableAgentSessionIndex.load() preserves the single config-root scan per load pass. The integration test reproduces the exact drift scenario from the bug report and verifies the correct cwd is pinned.

No files require special attention.

Important Files Changed

Filename Overview
Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptResume.swift New shared type ClaudeResumeWorkingDirectory with three-step resolution logic; TranscriptLookupCache reference-type caching is correct; the 64 KB bounded file read, round-trip guard, and nested-layout path inference all look correct.
Sources/RestorableAgentSession.swift Correctly creates one shared claudeResumeResolver before the session loop and replaces the duplicated claudeVerifiedRestorableWorkingDirectory with a delegation call; claudeTranscriptLookup is still used for resolvedClaudeWorkflowRecord/hookRecordIsRestorable (separate existing cache, no regression).
CLI/cmux.swift transcriptPath threaded through all 8 call sites; publishAgentSurfaceResumeBinding now routes Claude sessions through ClaudeResumeWorkingDirectory before the AgentResumeWorkingDirectory fallback, scoped strictly to kind == "claude".
Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/ClaudeResumeWorkingDirectoryTests.swift Good coverage: candidate match, transcript-content recovery (the exact failure case), nested layout, round-trip guard rejection, config-dir scan, and nil inputs all tested.
cmuxTests/CLIGenericHookPersistenceTests.swift Integration test correctly reproduces the real failure (CMUX_AGENT_LAUNCH_CWD = drift, both candidates = drift) and asserts surface.resume.set pins the launch cwd recovered from transcript content.

Reviews (2): Last reviewed commit: "Address review: fix sessionId optional, ..." | Re-trigger Greptile

Comment thread Sources/RestorableAgentSession.swift Outdated
Comment on lines +1511 to +1517
ClaudeResumeWorkingDirectory(fileManager: fileManager, homeDirectory: homeDirectory)
.verifiedWorkingDirectory(
sessionId: record.sessionId ?? "",
transcriptPath: record.transcriptPath,
claudeConfigDir: record.launchCommand?.environment?["CLAUDE_CONFIG_DIR"],
candidateWorkingDirectories: [launchCwd, recordedCwd].compactMap { $0 }
)

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 Per-session directory scan and 64 KB file read inside the session-loop

claudeVerifiedRestorableWorkingDirectory now constructs a fresh ClaudeResumeWorkingDirectory (and therefore a fresh TranscriptLookupCache) for every Claude session record in the for record in state.sessions.values loop inside RestorableAgentSessionIndex.load(). The old code received the shared claudeTranscriptLookup (created once at line 1078, still used at lines 1106 and 1124), so configRoots() — which calls contentsOfDirectory(atPath: accountRoot) on ~/.codex-accounts/claude — ran once per index load. The new code re-runs that scan per session. On top of that, the new step (b) recordedCwd(inTranscriptAtPath:) adds a synchronous FileHandle.read(upToCount: 64*1024) + JSONSerialization.jsonObject per session whenever no candidate matches the expected project-dir name. The fix is to thread the existing shared claudeTranscriptLookup into restorableWorkingDirectory / claudeVerifiedRestorableWorkingDirectory (as the old signature did) and push the new transcript-content read into the same shared cache or an off-main Task.

Rule Used: Flag production Swift that reads, decodes, or scan... (source)

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 on lines +10 to +15
public enum ClaudeProjectDirEncoding {
public static func projectDirName(forPath path: String) -> String {
path.replacingOccurrences(of: "/", with: "-")
.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 Caseless enum as static-func namespace

ClaudeProjectDirEncoding is a caseless enum whose only member is a public static func — precisely the pattern the cmux-no-ambient-global-state rule flags as a prohibited static-only namespace type. The encoding belongs naturally as a static func on ClaudeResumeWorkingDirectory (or as a private helper), which would keep the API surface coherent and remove the standalone namespace type.

Rule Used: Flag new ambient global state in production Swift:... (source)

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

🤖 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 `@CLI/cmux.swift`:
- Around line 26749-26754: The resume working directory selection for Claude is
too permissive because `verifiedClaudeWorkingDirectory` falling back to
`AgentResumeWorkingDirectory.resolve(...)` can reuse an untrusted hook cwd.
Update the logic around `resumeWorkingDirectory` so Claude only resumes when the
launch working directory is explicitly verified/trusted, and otherwise skip or
clear the resume binding instead of calling the generic fallback. Keep the fix
localized to the `verifiedClaudeWorkingDirectory` /
`AgentResumeWorkingDirectory.resolve` flow.

In `@cmuxTests/CLIGenericHookPersistenceTests.swift`:
- Around line 3745-3750: The setup call in runClaudeHook for the hooks claude
session-start path is currently ignored, which can hide an earlier failure and
cause misleading later assertions. Update the test in
CLIGenericHookPersistenceTests to capture the result of the session-start
invocation, assert that it succeeds before proceeding, and keep the resume
binding validation separate so failures point to the correct step.

In
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptResume.swift`:
- Around line 62-63: Make config-root discovery reusable across the load pass:
`verifiedWorkingDirectory` is creating a new `TranscriptLookupCache` for every
record, which causes repeated synchronous scans of Claude account roots during
the session-index load loop. Hoist the `TranscriptLookupCache`/`configRoots`
resolver out of the per-record path in `ClaudeTranscriptResume` and reuse it
across the loop, or make the cache injectable so `verifiedWorkingDirectory` can
share the same lookup state for all records.
- Around line 74-90: The cwd recovery logic in ClaudeTranscriptResume should not
accept a best-effort encoded candidate when it can be ambiguous between paths
like /a.b and /a/b. Update the candidate selection in the function that uses
candidates.first(where:) and recordedCwd(inTranscriptAtPath:) to prefer the
transcript’s top-level cwd when it round-trips cleanly, and otherwise fail
closed instead of returning a lossy/drifted match. Keep the
expectedProjectDirName check, but ensure
ClaudeProjectDirEncoding.projectDirName(forPath:) is only used to validate an
authoritative cwd source rather than selecting among ambiguous candidates.
- Around line 65-72: The transcript path fallback in
ClaudeTranscriptResume.swift is too shallow and uses only the immediate parent
directory, which breaks recovery for nested Claude layouts like
<project>/<sessionId>/messages/<sessionId>.jsonl when
projectDirName(containingTranscriptPath:configRoots:) cannot resolve a root.
Update the logic around normalizedNonEmptyValue and expectedProjectDirName to
infer the project directory from the known transcript path shape before falling
back to the parent, so nested paths recover the project name correctly even when
the config root is unknown.

In
`@Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/ClaudeResumeWorkingDirectoryTests.swift`:
- Around line 17-24: The transcript fixture only covers the direct
<project>/<sessionId>.jsonl layout, while the production resolver also handles
the nested <project>/<sessionId>/messages/<sessionId>.jsonl shape. Update the
fixture setup in ClaudeResumeWorkingDirectoryTests to create an additional
nested-layout variant alongside the existing projectDir/transcriptPath setup,
using ClaudeProjectDirEncoding.projectDirName(forPath:) and the
sessionId/messages path so both storage shapes are covered.

In `@Sources/RestorableAgentSession.swift`:
- Around line 1511-1517: The `verifiedWorkingDirectory` call in
`RestorableAgentSession` is passing `record.sessionId ?? ""`, but
`RestorableAgentHookSessionRecord.sessionId` is already a non-optional `String`,
so remove the nil-coalescing fallback and pass `record.sessionId` directly. Keep
the rest of the `ClaudeResumeWorkingDirectory` invocation unchanged.
🪄 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: 9c7633e6-bd25-4979-ba5e-71d3747ccb35

📥 Commits

Reviewing files that changed from the base of the PR and between e4b590a and 541f5f9.

📒 Files selected for processing (5)
  • CLI/cmux.swift
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptResume.swift
  • Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/ClaudeResumeWorkingDirectoryTests.swift
  • Sources/RestorableAgentSession.swift
  • cmuxTests/CLIGenericHookPersistenceTests.swift

Comment thread CLI/cmux.swift
Comment on lines +26749 to +26754
let resumeWorkingDirectory = verifiedClaudeWorkingDirectory
?? AgentResumeWorkingDirectory().resolve(
kind: kind,
runtimeCwd: cwd,
launchWorkingDirectory: launchCommand?.workingDirectory
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fail closed when Claude cwd verification cannot prove the launch directory.

If verifiedWorkingDirectory returns nil, this falls back to AgentResumeWorkingDirectory.resolve(...), which can reuse the drifted hook cwd and bind claude --resume to the wrong project directory. For Claude, skip/clear the resume binding unless the launch cwd is explicitly trusted, or avoid the generic fallback after verification fails.

As per path instructions, correctness-critical session identity/lifecycle should use one authoritative source and fail closed rather than guessing when the reliable signal is missing.

🤖 Prompt for 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.

In `@CLI/cmux.swift` around lines 26749 - 26754, The resume working directory
selection for Claude is too permissive because `verifiedClaudeWorkingDirectory`
falling back to `AgentResumeWorkingDirectory.resolve(...)` can reuse an
untrusted hook cwd. Update the logic around `resumeWorkingDirectory` so Claude
only resumes when the launch working directory is explicitly verified/trusted,
and otherwise skip or clear the resume binding instead of calling the generic
fallback. Keep the fix localized to the `verifiedClaudeWorkingDirectory` /
`AgentResumeWorkingDirectory.resolve` flow.

Source: Path instructions

Comment thread cmuxTests/CLIGenericHookPersistenceTests.swift Outdated
Comment thread Sources/RestorableAgentSession.swift Outdated
- Sources/RestorableAgentSession.swift: `record.sessionId` is non-optional —
  drop the `?? ""` that wouldn't compile (caught by review). Build one
  `ClaudeResumeWorkingDirectory` per index-load pass and reuse it across records
  so the config-root scan and transcript probes aren't redone per session; remove
  the now-inlined wrapper and the dead `claudeProjectDirName(containingTranscriptPath:)`.
- ClaudeTranscriptResume.swift: infer the project dir from the transcript shape
  (including nested `<id>/messages/<id>.jsonl`) when the config root is unknown,
  instead of using only the immediate parent. Memoize config-root discovery in the
  shared cache so reusing one resolver across a load loop scans account roots once.
- Tests: assert the session-start hook succeeds before validating the binding;
  add a nested-layout recovery case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@endmoseung

Copy link
Copy Markdown
Author

Thanks for the reviews — pushed fixes for the actionable ones (sessionId ?? "" that wouldn't compile, nested <id>/messages/<id>.jsonl project-dir inference, shared per-load resolver/cache so account roots are scanned once, and a session-start success assertion + nested-layout test).

A few I deliberately skipped, with reasoning — happy to change if you disagree:

  • Fail closed (skip/clear the binding) when Claude verification returns nil — I kept the fallback to AgentResumeWorkingDirectory().resolve(...). For the common, non-drifted session both candidates already equal the launch cwd, so resolve (which prefers the launch dir for .byDirectory) returns the correct dir; failing closed there would regress the happy path by dropping a valid binding. verifiedWorkingDirectory only overrides when it can prove a better dir against the transcript on disk, so the fallback is strictly the prior behavior, not a new guess.

  • Move ClaudeProjectDirEncoding off the caseless-enum namespace — kept it as is to match the existing ClaudeConfigDirectoryPath / AgentLaunchEnvironmentPolicy enums already in this package. Glad to fold it into ClaudeResumeWorkingDirectory if you'd prefer the static-namespace rule enforced here.

  • Lossy /a.b vs /a/b ambiguity in recovery — the recovery path never decodes; it reads the launch cwd from the transcript's own cwd field and accepts it only when projectDirName(forPath:) re-encodes to the transcript's project dir, so an ambiguous/foreign path fails closed rather than being chosen.

Note: I couldn't run the full Xcode build locally (this machine lacks CoreSimulator.framework, so xcodebuild fails to load before compiling). The CMUXAgentLaunch package + its tests pass via swift test; the app/CLI compile and the integration test should be confirmed by CI.

@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
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptResume.swift`:
- Around line 68-82: The transcript-path shortcut in ClaudeTranscriptResume
should only be trusted when it actually belongs to the current sessionId. Update
the logic around normalizedNonEmptyValue(transcriptPath),
projectDirName(containingTranscriptPath:sessionId:configRoots:), and the
candidate match in ClaudeProjectDirEncoding.projectDirName(forPath:) so it
accepts only the supported direct/nested transcript shapes whose file stem
matches sessionId; otherwise return nil and fall back to the verified
config-root disk probe.
🪄 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: 8a1d2677-e6bc-4a2b-8040-bab83a4cdc41

📥 Commits

Reviewing files that changed from the base of the PR and between 541f5f9 and daa09dd.

📒 Files selected for processing (4)
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptResume.swift
  • Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/ClaudeResumeWorkingDirectoryTests.swift
  • Sources/RestorableAgentSession.swift
  • cmuxTests/CLIGenericHookPersistenceTests.swift

Comment on lines +68 to +82
if let transcriptPath = normalizedNonEmptyValue(transcriptPath) {
let expandedTranscriptPath = (transcriptPath as NSString).expandingTildeInPath
// The transcript's own storage path names the project directory Claude looks in.
let expectedProjectDirName = projectDirName(
containingTranscriptPath: expandedTranscriptPath,
sessionId: sessionId,
configRoots: roots
)

if let expectedProjectDirName, !expectedProjectDirName.isEmpty {
// (a) Prefer a candidate whose encoding matches that project directory.
if let matched = candidates.first(where: {
ClaudeProjectDirEncoding.projectDirName(forPath: $0) == expectedProjectDirName
}) {
return matched

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate transcriptPath belongs to sessionId before trusting its project dir.

Right now any path under projects/<project>/... can select <project> and return a candidate, even if the file stem is not \(sessionId).jsonl. A stale/mismatched transcript path can therefore bind resume to the wrong Claude project and skip the verified config-root probe. Parse only the supported direct and nested shapes for the current session, otherwise return nil and let the disk probe/fallback handle it.

🐛 Proposed fix
     private func projectDirName(
         containingTranscriptPath path: String,
         sessionId: String,
         configRoots: [String]
     ) -> String? {
         let standardizedPath = (path as NSString).standardizingPath
+        let expectedFileName = "\(sessionId).jsonl"
+        guard (standardizedPath as NSString).lastPathComponent == expectedFileName else {
+            return nil
+        }
         for root in configRoots {
             let projectsRoot = ((root as NSString).appendingPathComponent("projects") as NSString)
                 .standardizingPath
             let prefix = projectsRoot.hasSuffix("/") ? projectsRoot : projectsRoot + "/"
             guard standardizedPath.hasPrefix(prefix) else { continue }
             let relativePath = String(standardizedPath.dropFirst(prefix.count))
-            guard let projectDirName = relativePath.split(separator: "/", maxSplits: 1).first,
-                  !projectDirName.isEmpty else {
-                continue
-            }
-            return String(projectDirName)
+            let parts = relativePath.split(separator: "/", omittingEmptySubsequences: false).map(String.init)
+            if parts.count == 2, parts[1] == expectedFileName, !parts[0].isEmpty {
+                return parts[0]
+            }
+            if parts.count == 4,
+               parts[1] == sessionId,
+               parts[2] == "messages",
+               parts[3] == expectedFileName,
+               !parts[0].isEmpty {
+                return parts[0]
+            }
+            continue
         }
 
         // Config root unknown — infer from the transcript shape. Walk up from the file: the nested

Also applies to: 145-174

🤖 Prompt for 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.

In
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/ClaudeTranscriptResume.swift`
around lines 68 - 82, The transcript-path shortcut in ClaudeTranscriptResume
should only be trusted when it actually belongs to the current sessionId. Update
the logic around normalizedNonEmptyValue(transcriptPath),
projectDirName(containingTranscriptPath:sessionId:configRoots:), and the
candidate match in ClaudeProjectDirEncoding.projectDirName(forPath:) so it
accepts only the supported direct/nested transcript shapes whose file stem
matches sessionId; otherwise return nil and fall back to the verified
config-root disk probe.

Source: Path instructions

@teamleaderleo teamleaderleo added bug Something isn't working area: agents Agent integrations (Claude Code, Codex, ACP), agent chat, hooks, status area: workspaces Workspaces, sessions, restore after relaunch, worktrees S2: major A crash, hang, lost state, broken connection, or a regression on a path people use labels Sep 30, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator

Claude resume bindings on main still lack transcript-verified launch-directory recovery; keeping this open for a current-main port and review.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: agents Agent integrations (Claude Code, Codex, ACP), agent chat, hooks, status area: workspaces Workspaces, sessions, restore after relaunch, worktrees bug Something isn't working S2: major A crash, hang, lost state, broken connection, or a regression on a path people use

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Resume launch command uses panel's current cwd, but session may have been created in a different cwd → session-not-found after restart

2 participants