Skip to content

fix(server): switching between Claude accounts that share ~/.claude/projects resumes the same session - #14340

Open
belliel wants to merge 3 commits into
pingdotgg:mainfrom
belliel:fix/claude-overlay-continuation
Open

belliel wants to merge 3 commits into
pingdotgg:mainfrom
belliel:fix/claude-overlay-continuation

Conversation

@belliel

@belliel belliel commented Sep 30, 2026 •

Copy link
Copy Markdown

Problem

A common way to run two Claude subscriptions side by side is an auth overlay: a second CLAUDE_CONFIG_DIR with its own .credentials.json, whose projects is a symlink to ~/.claude/projects. Both accounts read and write the same transcripts, so either one can --resume the other's session.

T3 builds Claude continuation keys from the config dir path, so the two instances never match, and switching a thread between them (say, when one account hits its usage limit) goes through the context handoff. The handoff replays the thread as text under a 16k-token budget (T3CODE_CONTEXT_HANDOFF_TOKEN_CAP) and drops items that do not fit. It carries messages and command output, but not other tool results: whatever the agent learned from files it read is gone, even though the transcript that holds it is on disk and readable by the target account.

Change

makeClaudeContinuationGroupKey keys an instance by its transcript store, the realpath of <config dir>/projects, as claude:projects:<dir>. When projects is missing or a dangling symlink, it uses the unresolved <config dir>/projects path, so homes that share nothing keep distinct keys.

Equal keys make decideProviderSessionTransition pick restart_and_resume. On that path the orchestrator already hands the native thread ref to the target instance ("Account overlays share native history" in Orchestrator.ts), including after the session has stopped, which is what #11908 fixed on the old orchestrator. Instances with separate stores still get the handoff.

The first version keyed on the parent of the resolved projects. CodeRabbit pointed out that two homes whose projects link to sibling directories (/data/a, /data/b) would then share the key /data, and a switch would try to resume a transcript the target cannot read. 6b563ef keys on the directory itself and adds that case to the tests.

Risk: one realpath when a Claude instance is built. The key format changes from claude:home: to claude:projects:. Keys are computed at runtime and only compared for equality (the transition policy on the server, the model picker on the web), so nothing stored depends on the old format.

Scope and approval

No prior issue. This is a small fix for the same defect class as #12616 (accepted and closed): a continuation key has to identify where the transcripts live, and here two instances with one transcript store get different keys. The change stays inside the function that derives the key. The transition policy, the native-resume path, and the handoff are untouched, and no setting, default, or UI changes. Codex already applies the same rule: with shadowHomePath, auth.json stays private while sessions is shared, and the key follows the shared home. I keyed on the realpath instead of adding a Claude shadow-home setting, so existing overlay setups work without new configuration.

Verification

vp test run apps/server/src/provider/Drivers/ClaudeHome.test.ts: 8/8. An overlay, a chained overlay, a symlinked home, and an overlay reached through an inherited CLAUDE_CONFIG_DIR share one key. A separate home, a home without projects, a dangling projects symlink, and two homes linked to sibling directories each keep their own. With provider/Drivers, ProviderInstanceRegistryLive, ProviderSwitchService and ProviderSessionTransitionPolicy: 147/147. apps/server typecheck and lint on the changed files: clean.

Live, in the web client against an isolated vp run dev server (one dev state, a new thread per run). Linux 6.12 (Debian 13), Claude Code 2.1.288, Claude Sonnet 5. "Claude" uses ~/.claude. "Claude Work" is a second config dir with its own credentials and projects -> ~/.claude/projects, signed in to a different subscription.

Claude reads a file holding a code word and replies only DONE-READING. The file is deleted, the thread is switched to Claude Work, and Claude Work is asked for the code word without tools, so the word exists only in the Read tool result.

Build Session at switch What Claude Work does Answer
main 2a45557 live Context handoff, query.open without resume I don't know
this PR 6b563ef live resume = the Claude session id, init reports the same session_id PAPAYA-42
this PR 6b563ef stopped (dev server restarted after the Claude turn) resumes the stopped session's id PAPAYA-42

Each screenshot is the conversation column of the thread. "1 changed file -1" is the test deleting the code-word file. Full-size versions: https://lj16g2p4ui4l.postplan.dev

main 2a45557: a "Context handoff" row before the question, then "I don't know". The timestamp line under it comes from that account's own CLAUDE.md.

main: context handoff, Claude Work answers I don't know

This PR 6b563ef, live session: no handoff row, PAPAYA-42.

This PR, live session: Claude Work answers PAPAYA-42

This PR 6b563ef, after a server restart stopped the session: PAPAYA-42.

This PR, stopped session: Claude Work answers PAPAYA-42

Not checked: macOS, Windows, the desktop and mobile clients, and a switch triggered by a real usage limit.

…shared home

A Claude config dir that symlinks `projects` into another home resumes that
home's transcripts, but its continuation key was built from its own path, so
T3 locked the model picker and rejected switching a thread between the two
instances. Key Claude continuation by the home that owns `projects`
(resolved through symlinks), which is unchanged for ordinary config dirs.
@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Sep 30, 2026
macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Sep 30, 2026
@macroscopeapp

macroscopeapp Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 6b563ef

Macroscope's review found this PR approvable — The change is a narrowly scoped Claude continuation-key fix with one filesystem lookup and comprehensive symlink/fallback tests. The remaining modified file only updates registry expectations, with no schema, deployment, default, or static-analysis configuration impact.

You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: pingdotgg/t3code/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a25a9cc7-8222-47ad-815e-1e9e8b411f8e
📥 Commits

Reviewing files that changed from the base of the PR and between 5e456e6 and 6b563ef.

📒 Files selected for processing (3)
  • apps/server/src/provider/Drivers/ClaudeHome.test.ts
  • apps/server/src/provider/Drivers/ClaudeHome.ts
  • apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

Claude continuation group keys now use the real path of the projects directory. If real-path resolution fails, the key uses the joined projects path. Tests cover linked, missing, and distinct project directories.

Changes

Claude continuation group keys

Layer / File(s) Summary
Resolve projects directory for continuation keys
apps/server/src/provider/Drivers/ClaudeHome.ts, apps/server/src/provider/Drivers/ClaudeHome.test.ts, apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts
The key function uses the real path of the projects directory and falls back to its joined path if resolution fails. Tests cover symlinked, missing, and distinct project directories. The provider registry test expects a key based on the resolved projects path.

Priority: ➖ Normal

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

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 6b563

This change lets Claude instances that share a transcripts directory switch within one thread. Ordinary config directories keep their existing behavior apart from the key prefix. No blocking merge risk is evident from the supplied evidence.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 6b563

Instances sharing transcript storage can now switch accounts without abandoning their native thread. The selected account’s configuration and credentials remain separate. No concrete security regression was established, but deployment-specific account-sharing policy and behavior during filesystem changes remain unverified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The demonstrated exposure is continuation compatibility among configured Claude instances whose projects paths resolve to one transcript store. Influencing this grouping requires control over the configured home, inherited configuration environment, or relevant filesystem links. Repository evidence does not establish whether different deployment tenants can exercise that control.

Trust Boundaries and Controls

  • observed — Continuation equality does not select credentials. Session opening resolves the selected instance’s adapter, and existing credential-binding controls reject startup during sign-in changes and wrap startup in credential-access guards. These controls predate the PR and remain separate from transcript grouping.

Resilience and Maintainability Implications

  • observed — Existing session opening is serialized by provider-session ID, not continuation key. Its inspected failure handling closes the session scope, drops the tool-credential reservation, and revokes only freshly issued credentials; attachment-persistence failure triggers session release. Shared transcript grouping therefore does not itself merge live session or credential ownership.

Hardening Proposals

  • proposed — Document that sharing a projects directory permits native-history continuation across configured accounts, and that its symlink targets belong to trusted configuration rather than an account-isolation boundary.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 3…
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.
Title check ✅ Passed The title clearly identifies the fix: Claude instances that share ~/.claude/projects can resume the same session. It is specific and relevant, though somewhat long.
Description check ✅ Passed The description covers the problem, change, scope and approval rationale, and verification. It includes focused test results, live-test evidence, and limitations.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

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

Copy link
Copy Markdown
Member

Note

This comment is posted by Julius' dot

Which checks were actually run on this head, and what were the results? The "How to check" section gives a test command and manual recipe, while the two images illustrate the design rather than an observed run. Please report the focused test result and whether a live switch between the symlinked homes was exercised, including platform and limits. That will establish the evidence required by the verification rule. Leaving this open for clarification.

@belliel

belliel commented Oct 1, 2026 •

Copy link
Copy Markdown
Author

I ran it on the head and put the results in the description under Verification: the focused test (7/7), the related suites, and a live switch between two symlinked Claude homes on Linux with before and after screenshots. In short, the switch is now allowed and keeps context while the session is live. After a stop it still starts blank, which is #4766 and not changed by this PR.

@belliel

belliel commented Oct 1, 2026

Copy link
Copy Markdown
Author

Also checked this together with #11908. Merging its head (f68d312) onto this one (0cd75a9) has no conflicts, and the provider suites pass (1682/1682, apps/server typecheck clean).

Live, same two-subscription setup: one turn on Claude, server restart so the session is stopped, then switch the thread to Claude Work. It answered with the code word set under Claude, and its session.started carries the stopped session's resume cursor. On this PR alone the same flow answered "I don't know". So the two together cover both the live and the stopped switch for symlinked Claude homes.

Screenshots: https://dsbta0isuh6u.postplan.dev

@juliusmarminge juliusmarminge added the macroscope-review Opt PRs made by unvouched contributors in for Macroscope review. Vouched contributors auto-reviews label Oct 1, 2026 — with ChatGPT Codex Connector
@macroscopeapp
macroscopeapp Bot dismissed their stale review October 1, 2026 22:13

Dismissing prior approval to re-evaluate 0cd75a9

macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Oct 1, 2026
Keying on the parent of the resolved projects directory gave two homes the
same key when their projects links pointed at sibling directories, so a
switch between them chose native resume against a transcript store the
target cannot read. The key is now the projects directory itself.
@macroscopeapp
macroscopeapp Bot dismissed their stale review October 5, 2026 17:46

Dismissing prior approval to re-evaluate 6b563ef

@belliel belliel changed the title fix(server): Claude instances that share ~/.claude/projects can switch mid-thread fix(server): switching between Claude accounts that share ~/.claude/projects resumes the same session Oct 5, 2026
@belliel

belliel commented Oct 5, 2026 •

Copy link
Copy Markdown
Author

Rechecked this on the new orchestrator. The context handoff does work for this switch on main, and this PR doesn't change it. What the PR covers is the narrower case where both Claude instances already share one transcript store and differ only in credentials: one projects directory, two .credentials.json. The target can then resume the original session itself, so a handoff throws away context that is already on disk. The handoff carries messages and command output under a 16k-token budget, but not file reads or other tool results.

It is the same rule Codex follows with shadowHomePath, where the key follows the shared sessions. Once the keys match, the orchestrator's existing restart_and_resume path does the rest, including for a stopped session, so #11908 is no longer needed for this.

On head 6b563ef: Claude reads a file holding a code word, the file is deleted, the thread is switched to Claude Work, and Claude Work is asked for the word without tools.

  • main: context handoff, answers "I don't know"
  • this PR, live session: resumes the same Claude session, answers PAPAYA-42
  • this PR, after a server restart stopped the session: resumes it, answers PAPAYA-42

6b563ef also fixes a key collision CodeRabbit flagged (two homes whose projects link to sibling directories). The description has the full results and screenshots of all three runs.

@Neonsy

Neonsy commented Oct 9, 2026 •

Copy link
Copy Markdown

Windows data point, since the live runs in the description are on Linux

Setup: Windows 10 with two Claude instances. The first uses the default %USERPROFILE%\.claude. The second sets CLAUDE_CONFIG_DIR to %USERPROFILE%\.claude-2, which has its own .credentials.json and .claude.json. Its projects and most other directories are NTFS junctions (not symlinks) to the matching directories in .claude

Key derivation: With Node 24.21 outside T3, fs.realpath and fs.realpathSync.native both resolve .claude\projects and .claude-2\projects to the identical string C:\Users\<user>\.claude\projects, drive-letter case included. So this branch should give both instances the same claude:projects: key, while on main they get different claude:home: keys

Impact on main: Switching a thread from one account to the other (target model Opus 5.5) goes through the context handoff. One switch on a long thread carried a summary plus 14 history items and omitted 944. Another carried a summary plus 12 items and omitted 90. The transcripts are reachable through both homes via the junction

Not verified: I haven't run this branch live on Windows, so end-to-end resume evidence is still Linux only

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

macroscope-review Opt PRs made by unvouched contributors in for Macroscope review. Vouched contributors auto-reviews size:S 10-29 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants