Skip to content

Preserve sr Claude profile config on resume - #7121

Closed
azooz2003-bit wants to merge 3 commits into
mainfrom
fix-sr-claude-resume-config
Closed

azooz2003-bit wants to merge 3 commits into
mainfrom
fix-sr-claude-resume-config

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jul 1, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • keep cmux Claude resume self-heal from overriding an explicitly preserved CLAUDE_CONFIG_DIR
  • add a regression test for resume+fork launched from a Subrouter profile while the old transcript lives under ~/.claude

Test

  • env -u CMUX_CUSTOM_CLAUDE_PATH -u CMUX_CLAUDE_WRAPPER_SHIM -u CMUX_CLAUDE_WRAPPER_SHIM_ROOT python3 tests/test_claude_wrapper_hooks.py

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


Summary by cubic

Preserves the selected Claude profile config on resume by skipping self-heal when CLAUDE_CONFIG_DIR is explicitly preserved, preventing auth root switches and "No conversation found" errors. Applies this behavior across all wrapper paths (including hooks-disabled and socket passthrough) and adds a resume+fork test to confirm the pinned config dir and argv remain unchanged for live and stale sockets.

Written for commit 309a417. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved resume behavior to avoid overwriting a preserved Claude configuration setting during resumed sessions.
    • Kept the selected configuration directory intact when resume/auth preservation settings request it.
  • Tests
    • Added a regression test covering --resume + --fork-session, ensuring the preserved configuration remains unchanged and the resume arguments are forwarded correctly.

@vercel

vercel Bot commented Jul 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 Jul 1, 2026 7:47pm
cmux-staging Building Building Preview, Comment Jul 1, 2026 7:47pm

@coderabbitai

coderabbitai Bot commented Jul 1, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

cmux-claude-wrapper now skips resume-time CLAUDE_CONFIG_DIR reconciliation when auth-selection preservation is requested, and the test suite adds coverage for that preserved resume path.

Changes

Preserve CLAUDE_CONFIG_DIR on Resume

Layer / File(s) Summary
Conditional resume reconciliation logic
Resources/bin/cmux-claude-wrapper
Checks should_preserve_claude_auth_selection_key "CLAUDE_CONFIG_DIR" before extracting the resume session id; skips CLAUDE_CONFIG_DIR relocation when preservation is requested and updates the resume handling call sites accordingly.
Regression test and runner wiring
tests/test_claude_wrapper_hooks.py
Adds test_preserved_claude_config_dir_skips_resume_self_heal for --resume --fork-session across live and stale socket states, and wires the test into main().

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 24 | ❌ 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 (24 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change by describing preserved Claude config behavior on resume.
Description check ✅ Passed The description includes a summary and testing section, though it omits the demo video, review trigger, and checklist template sections.
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 The PR only changes the Claude wrapper shell script and Python tests; no production Swift files or actor-isolation-sensitive Swift code are introduced.
Cmux Swift Blocking Runtime ✅ Passed No production Swift files changed; the PR only updates a bash wrapper and a Python test, so the Swift blocking-runtime rule doesn’t apply.
Cmux Browser Automation Off-Main ✅ Passed PR only changes the Claude wrapper and its tests; no browser.* routing or policy files under the rule’s scope were touched, so the off-main check is not applicable.
Cmux Expensive Synchronous Load ✅ Passed Only transcript scrolling/debug Swift files changed; no new history/transcript load, parsing, or MainActor sync load was added.
Cmux Cache Substitution Correctness ✅ Passed PASS: resume self-heal still uses on-disk session lookup; the new helper only skips it when CLAUDE_CONFIG_DIR is explicitly preserved, not substituting a cache.
Cmux No Hacky Sleeps ✅ Passed No fixed sleeps, timers, polling, or wall-clock waits were added; the wrapper change is conditional resume handling and the new test is deterministic.
Cmux Algorithmic Complexity ✅ Passed The PR only adds a preserve guard around existing resume lookup; it removes work when CLAUDE_CONFIG_DIR is pinned and adds only test scaffolding.
Cmux Swift Concurrency ✅ Passed PR changes only a bash wrapper and Python tests; no cmux-owned Swift code or async-pattern additions were introduced.
Cmux Swift @Concurrent ✅ Passed PR changes are bash/Python wrapper tests only; no Swift async/concurrency code was touched, so the Swift @concurrent rule is not applicable.
Cmux Swift File And Package Boundaries ✅ Passed No production Swift files were touched; the PR changes only a shell wrapper and Python tests, so the Swift boundary rule is inapplicable.
Cmux Swiftpm Lockfiles ✅ Passed No cmux-owned .gitignore ignores Package.resolved; every external-url cmux package has a matching Package.resolved, and the root Xcode lockfile is present.
Cmux Swift Logging ✅ Passed PR changes only a shell wrapper and a Python test; no production Swift logging was added or modified.
Cmux User-Facing Error Privacy ✅ Passed The PR only changes resume/config-dir control flow and adds a developer-only regression test; no new user-facing error, alert, or recovery text exposes restricted details.
Cmux Full Internationalization ✅ Passed Only an operational shell wrapper and tests changed; no user-facing Swift/web text or locale catalogs were touched, just comments/config tokens.
Cmux Swiftui State Layout ✅ Passed PR only changes a bash wrapper and a Python test; no SwiftUI views, state, GeometryReader, or render-time state mutations are introduced.
Cmux Architecture Rethink ✅ Passed PR only changes a bash wrapper and Python tests; no Swift architectural changes or lifecycle/state-ownership issues are introduced.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR changes are a shell wrapper and Python test only; no Swift window code is added or materially changed, so the auxiliary-window shortcut rule doesn't apply.
Cmux Source Artifacts ✅ Passed Only hand-written source/test files changed; no artifact, cache, temp, or generated-output paths appear in the diff.
Cmux No Test Or Debug Seam In Production Source ✅ Passed The PR touches two production Swift files but only removes isUserScrollMomentumActive and trims debug accessibility output; it adds no new test/debug seam.
Cmux No Ambient Global State ✅ Passed PR changes only the bash wrapper and Python tests; no production Swift additions or new global/static-state APIs are introduced.
✨ 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 fix-sr-claude-resume-config

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 Jul 1, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a resume self-heal regression where reconcile_claude_config_dir_for_resume could override an explicitly preserved CLAUDE_CONFIG_DIR (e.g., set by sr claude via CMUX_PRESERVE_CLAUDE_AUTH_SELECTION_ENV). It introduces reconcile_claude_config_dir_for_resume_unless_preserved and applies it at all three early-exit and main call sites, and adds a regression test covering both the live-socket and stale-socket code paths.

  • Wrapper fix: Wraps the resume self-heal in a preservation guard at all three call sites (hooks-disabled, stale-socket, and main-socket paths), so a pinned CLAUDE_CONFIG_DIR is never overwritten.
  • Regression test: Adds test_preserved_claude_config_dir_skips_resume_self_heal, which iterates over both socket states with a fabricated home directory where the transcript lives under a different config root than the preserved one.

Confidence Score: 5/5

Safe to merge — the preservation guard is correctly applied at all three call sites and the new wrapper function handles the no-resume-flag exit code cleanly.

All three code paths that previously called reconcile_claude_config_dir_for_resume directly now go through the new preservation-aware wrapper. The fix is minimal and targeted, and the new regression test confirms the intended behavior for both live and stale socket states.

tests/test_claude_wrapper_hooks.py — the hooks-disabled code path is not yet covered by the new test.

Important Files Changed

Filename Overview
Resources/bin/cmux-claude-wrapper Introduces reconcile_claude_config_dir_for_resume_unless_preserved and applies it at all three call sites; logic is correct and the
tests/test_claude_wrapper_hooks.py Adds test_preserved_claude_config_dir_skips_resume_self_heal covering live and stale socket states; the hooks-disabled path (run_wrapper_auth_env hooks_disabled=True) is not exercised, leaving a small coverage gap.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[cmux-claude-wrapper invoked] --> B{CMUX_CLAUDE_HOOKS_DISABLED=1?}
    B -- yes --> C[clear_inherited_claude_auth_selection_env]
    C --> D[reconcile_claude_config_dir_for_resume_unless_preserved]
    B -- no --> E{IN_CMUX=0 OR stale socket?}
    E -- yes --> F[clear_inherited_claude_auth_selection_env]
    F --> G[reconcile_claude_config_dir_for_resume_unless_preserved]
    E -- no --> H[main socket path]
    H --> I[reconcile_claude_config_dir_for_resume_unless_preserved]

    D --> P{should_preserve_claude_auth_selection_key CLAUDE_CONFIG_DIR?}
    G --> P
    I --> P

    P -- yes, PRESERVED --> Q[return 0 - keep pinned CLAUDE_CONFIG_DIR]
    P -- no --> R[extract_claude_resume_session_id from argv]
    R --> S[reconcile_claude_config_dir_for_resume with session id]
    S --> T[scan known config roots for transcript]
    T --> U{transcript found in different root?}
    U -- yes --> V[export CLAUDE_CONFIG_DIR = found root]
    U -- no --> W[keep existing CLAUDE_CONFIG_DIR]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[cmux-claude-wrapper invoked] --> B{CMUX_CLAUDE_HOOKS_DISABLED=1?}
    B -- yes --> C[clear_inherited_claude_auth_selection_env]
    C --> D[reconcile_claude_config_dir_for_resume_unless_preserved]
    B -- no --> E{IN_CMUX=0 OR stale socket?}
    E -- yes --> F[clear_inherited_claude_auth_selection_env]
    F --> G[reconcile_claude_config_dir_for_resume_unless_preserved]
    E -- no --> H[main socket path]
    H --> I[reconcile_claude_config_dir_for_resume_unless_preserved]

    D --> P{should_preserve_claude_auth_selection_key CLAUDE_CONFIG_DIR?}
    G --> P
    I --> P

    P -- yes, PRESERVED --> Q[return 0 - keep pinned CLAUDE_CONFIG_DIR]
    P -- no --> R[extract_claude_resume_session_id from argv]
    R --> S[reconcile_claude_config_dir_for_resume with session id]
    S --> T[scan known config roots for transcript]
    T --> U{transcript found in different root?}
    U -- yes --> V[export CLAUDE_CONFIG_DIR = found root]
    U -- no --> W[keep existing CLAUDE_CONFIG_DIR]
Loading

Reviews (2): Last reviewed commit: "Honor preserved Claude config in all wra..." | Re-trigger Greptile

@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.

No issues found across 2 files

Re-trigger cubic

@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 `@tests/test_claude_wrapper_hooks.py`:
- Around line 1413-1449: The setup_env closure in
test_preserved_claude_config_dir_skips_resume_self_heal captures the loop
variable expected by reference, triggering Ruff B023. Bind the current expected
value inside the loop before defining setup_env, and have the closure use that
bound local instead so each iteration uses its own dictionary consistently. Keep
the rest of the test logic the same, including the auth_env assertion against
the selected_root path.
🪄 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: 47afc344-853e-4c85-a118-4dad59475311

📥 Commits

Reviewing files that changed from the base of the PR and between 70f666a and 309a417.

📒 Files selected for processing (2)
  • Resources/bin/cmux-claude-wrapper
  • tests/test_claude_wrapper_hooks.py

Comment on lines +1413 to +1449
def test_preserved_claude_config_dir_skips_resume_self_heal(failures: list[str]) -> None:
session_id = "582054f9-2f87-4e90-ad8d-2f532ed8c61b"

for socket_state in ("live", "stale"):
expected: dict[str, str] = {}

def setup_env(tmp: Path) -> dict[str, str]:
home = tmp / "home"
default_root = home / ".claude"
(default_root / "projects" / "-work").mkdir(parents=True)
(default_root / "projects" / "-work" / f"{session_id}.jsonl").write_text(
"{}\n", encoding="utf-8"
)
selected_root = home / ".subrouter" / "codex" / "claude" / "aziz-claude-1"
(selected_root / "projects").mkdir(parents=True)
expected["path"] = str(selected_root)
return {
"HOME": str(home),
"CLAUDE_CONFIG_DIR": str(selected_root),
"CMUX_PRESERVE_CLAUDE_AUTH_SELECTION_ENV": "1",
"CMUX_PRESERVE_CLAUDE_AUTH_SELECTION_ENV_KEYS": "CLAUDE_CONFIG_DIR",
}

code, auth_env, real_argv, stderr = run_wrapper_auth_env(
argv=["--resume", session_id, "--fork-session"],
inherited_env={},
socket_state=socket_state,
setup_env=setup_env,
)
expect(code == 0, f"preserved resume config dir {socket_state}: wrapper exited {code}: {stderr}", failures)
expect(
auth_env.get("CLAUDE_CONFIG_DIR") == expected["path"],
f"preserved resume config dir {socket_state}: expected CLAUDE_CONFIG_DIR to remain on the selected profile root "
f"{expected['path']!r}, got {auth_env.get('CLAUDE_CONFIG_DIR')!r}",
failures,
)
expect(real_argv[-3:] == ["--resume", session_id, "--fork-session"], f"preserved resume config dir {socket_state}: expected resume+fork argv, got {real_argv}", failures)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Bind expected in the setup_env closure to satisfy Ruff B023.

Static analysis flags the closure at Line 1428 capturing the loop variable expected by reference (B023). It's functionally safe here since setup_env is invoked synchronously within the same loop iteration before expected is reassigned, but binding it explicitly avoids relying on that ordering and silences the lint warning.

🧹 Proposed fix to bind the loop variable
-        def setup_env(tmp: Path) -> dict[str, str]:
+        def setup_env(tmp: Path, expected: dict[str, str] = expected) -> dict[str, str]:
             home = tmp / "home"
📝 Committable suggestion

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

Suggested change
def test_preserved_claude_config_dir_skips_resume_self_heal(failures: list[str]) -> None:
session_id = "582054f9-2f87-4e90-ad8d-2f532ed8c61b"
for socket_state in ("live", "stale"):
expected: dict[str, str] = {}
def setup_env(tmp: Path) -> dict[str, str]:
home = tmp / "home"
default_root = home / ".claude"
(default_root / "projects" / "-work").mkdir(parents=True)
(default_root / "projects" / "-work" / f"{session_id}.jsonl").write_text(
"{}\n", encoding="utf-8"
)
selected_root = home / ".subrouter" / "codex" / "claude" / "aziz-claude-1"
(selected_root / "projects").mkdir(parents=True)
expected["path"] = str(selected_root)
return {
"HOME": str(home),
"CLAUDE_CONFIG_DIR": str(selected_root),
"CMUX_PRESERVE_CLAUDE_AUTH_SELECTION_ENV": "1",
"CMUX_PRESERVE_CLAUDE_AUTH_SELECTION_ENV_KEYS": "CLAUDE_CONFIG_DIR",
}
code, auth_env, real_argv, stderr = run_wrapper_auth_env(
argv=["--resume", session_id, "--fork-session"],
inherited_env={},
socket_state=socket_state,
setup_env=setup_env,
)
expect(code == 0, f"preserved resume config dir {socket_state}: wrapper exited {code}: {stderr}", failures)
expect(
auth_env.get("CLAUDE_CONFIG_DIR") == expected["path"],
f"preserved resume config dir {socket_state}: expected CLAUDE_CONFIG_DIR to remain on the selected profile root "
f"{expected['path']!r}, got {auth_env.get('CLAUDE_CONFIG_DIR')!r}",
failures,
)
expect(real_argv[-3:] == ["--resume", session_id, "--fork-session"], f"preserved resume config dir {socket_state}: expected resume+fork argv, got {real_argv}", failures)
def test_preserved_claude_config_dir_skips_resume_self_heal(failures: list[str]) -> None:
session_id = "582054f9-2f87-4e90-ad8d-2f532ed8c61b"
for socket_state in ("live", "stale"):
expected: dict[str, str] = {}
def setup_env(tmp: Path, expected: dict[str, str] = expected) -> dict[str, str]:
home = tmp / "home"
default_root = home / ".claude"
(default_root / "projects" / "-work").mkdir(parents=True)
(default_root / "projects" / "-work" / f"{session_id}.jsonl").write_text(
"{}\n", encoding="utf-8"
)
selected_root = home / ".subrouter" / "codex" / "claude" / "aziz-claude-1"
(selected_root / "projects").mkdir(parents=True)
expected["path"] = str(selected_root)
return {
"HOME": str(home),
"CLAUDE_CONFIG_DIR": str(selected_root),
"CMUX_PRESERVE_CLAUDE_AUTH_SELECTION_ENV": "1",
"CMUX_PRESERVE_CLAUDE_AUTH_SELECTION_ENV_KEYS": "CLAUDE_CONFIG_DIR",
}
code, auth_env, real_argv, stderr = run_wrapper_auth_env(
argv=["--resume", session_id, "--fork-session"],
inherited_env={},
socket_state=socket_state,
setup_env=setup_env,
)
expect(code == 0, f"preserved resume config dir {socket_state}: wrapper exited {code}: {stderr}", failures)
expect(
auth_env.get("CLAUDE_CONFIG_DIR") == expected["path"],
f"preserved resume config dir {socket_state}: expected CLAUDE_CONFIG_DIR to remain on the selected profile root "
f"{expected['path']!r}, got {auth_env.get('CLAUDE_CONFIG_DIR')!r}",
failures,
)
expect(real_argv[-3:] == ["--resume", session_id, "--fork-session"], f"preserved resume config dir {socket_state}: expected resume+fork argv, got {real_argv}", failures)
🧰 Tools
🪛 Ruff (0.15.20)

[warning] 1428-1428: Function definition does not bind loop variable expected

(B023)

🤖 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 `@tests/test_claude_wrapper_hooks.py` around lines 1413 - 1449, The setup_env
closure in test_preserved_claude_config_dir_skips_resume_self_heal captures the
loop variable expected by reference, triggering Ruff B023. Bind the current
expected value inside the loop before defining setup_env, and have the closure
use that bound local instead so each iteration uses its own dictionary
consistently. Keep the rest of the test logic the same, including the auth_env
assertion against the selected_root path.

Source: Linters/SAST tools

@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 — 309a417e Deployed Jul 1, 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.

3 participants