Skip to content

fix: avoid restoring empty Claude Keychain snapshots - #26205

Closed
innocarpe wants to merge 1 commit into
stablyai:mainfrom
innocarpe:claude-runtime-auth-snapshot-restore-logout
Closed

innocarpe wants to merge 1 commit into
stablyai:mainfrom
innocarpe:claude-runtime-auth-snapshot-restore-logout

Conversation

@innocarpe

Copy link
Copy Markdown
Contributor

Summary

Do not restore an empty Claude OAuth Keychain snapshot while a managed Claude account remains selected. Empty scoped and legacy values are recorded as metadata-only markers, so rollback keeps the current managed runtime auth and the next sync can retry. Deselecting managed auth still clears empty Keychain entries.

Screenshots

No visual change.

Testing

  • Added a regression test for partial Keychain write failure with empty scoped and legacy snapshots.
  • Focused Vitest file: 19 tests passed.
  • Oxlint on all five changed files: passed.
  • Oxfmt check on all five changed files: passed.
  • The regression test failed when the rollback guard was temporarily disabled, then passed after restoration.
  • pnpm lint
  • pnpm typecheck
  • pnpm test
  • pnpm build

AI Review Report

Reviewed snapshot capture, compatibility with older persisted snapshots, and both restoration paths. The regression test proves the rollback guard prevents an empty snapshot from replacing the selected managed auth. The change adds no platform-specific paths, shell commands, shortcuts, or Electron behavior; Keychain access remains in the existing macOS-only path, and Linux and Windows behavior is unchanged.

Security Audit

Empty OAuth Keychain JSON is omitted from the snapshot; only empty-state flags are persisted. Non-empty snapshots retain their existing handling. No commands, paths, dependencies, or IPC were added. The Keychain write failure cause remains unknown and is not attributed to this change.

Notes

CI and maintainer review have not been run.

@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Walkthrough

Walkthrough

Snapshot handling now distinguishes empty Claude OAuth credentials from captured and unknown Keychain values. Capture records empty scoped and legacy credentials, and restore paths write null for empty values. A new test covers selection after a partial legacy Keychain write failure, retry, and deselection.

Priority: ⬇️ Low

Merge Risk: 🟡 Moderate · up to 5cfcc

If a Keychain write partially fails when the original entries are absent, rollback can remove managed credentials while the managed account remains selected. Fix that path before merging.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: preventing restoration of empty Claude Keychain snapshots.
Description check ✅ Passed The description explains the change, its purpose, the regression test, and reported checks. It notes there is no visual change, but does not use the template’s required “N/A” wording or provide a reas…
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 5…
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.
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8d0c72c2-ced6-429c-ab4c-2d67f6b68621
📥 Commits

Reviewing files that changed from the base of the PR and between 2dc98c9 and 5cfcc85.

📒 Files selected for processing (5)
  • src/main/claude-accounts/runtime-auth-service-keychain-snapshots.test.ts
  • src/main/claude-accounts/runtime-auth/runtime-auth-keychain-snapshots.ts
  • src/main/claude-accounts/runtime-auth/runtime-auth-snapshot-capture.ts
  • src/main/claude-accounts/runtime-auth/runtime-auth-snapshot-restore.ts
  • src/main/claude-accounts/runtime-auth/runtime-auth-types.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.

Comment on lines +74 to +79
const scopedKeychainSnapshot = this.snapshotKeychainCredentials(
scopedKeychainCredentials.credentialsJson,
options.previousSnapshot,
'scoped',
options.managedCredentialsJson
)

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

set -eu
base=2dc98c93de675947958960061a335bcfbfb3f838
head=5cfcc85508ae37f9ee09a813a4219b42f53d9263
file=src/main/claude-accounts/runtime-auth/runtime-auth-snapshot-capture.ts
printf '%s\n' '--- diff ---'
git diff --no-ext-diff --unified=50 "$base" "$head" -- "$file"
printf '%s\n' '--- current file ---'
nl -ba "$file"
printf '%s\n' '--- relevant symbols and test references ---'
rg -n -F -e 'hasEmptyClaudeKeychainSnapshot' -e 'snapshotKeychainCredentials' -e 'partial' src

Repository: stablyai/orca

Length of output: 45130


🏁 Script executed:

set -eu
base=2dc98c93de675947958960061a335bcfbfb3f838
head=5cfcc85508ae37f9ee09a813a4219b42f53d9263
file=src/main/claude-accounts/runtime-auth/runtime-auth-snapshot-capture.ts
printf '%s\n' '--- diff ---'
git diff --no-ext-diff --unified=50 "$base" "$head" -- "$file"
printf '%s\n' '--- current file ---'
nl -ba "$file"
printf '%s\n' '--- relevant symbols and test references ---'
rg -n -F -e 'hasEmptyClaudeKeychainSnapshot' -e 'snapshotKeychainCredentials' -e 'partial' src || test "$?" -eq 1

Repository: stablyai/orca

Length of output: 45278


🏁 Script executed:

set -eu
dir=src/main/claude-accounts/runtime-auth
printf '%s\n' '--- runtime-auth files ---'
rg --files "$dir"
printf '%s\n' '--- exact symbol references ---'
rg -n -C 24 -F -e 'snapshotKeychainCredentials' -e 'hasEmptyClaudeKeychainSnapshot' -e 'scopedKeychainCredentialsEmpty' -e 'legacyKeychainCredentialsEmpty' "$dir" || test "$?" -eq 1
printf '%s\n' '--- related test declarations ---'
rg -n -C 8 'partial|rollback|managed.*selected|Keychain.*null|initial.*Keychain' "$dir" -g '*test*' || test "$?" -eq 1

Repository: stablyai/orca

Length of output: 36529


🏁 Script executed:

set -eu
dir=src/main/claude-accounts
printf '%s\n' '--- claude-account test files ---'
rg --files "$dir" | rg '(^|/)[^/]*(test|spec)\.'
printf '%s\n' '--- restore callers and rollback references ---'
rg -n -C 10 -F -e 'restoreSystemDefaultSnapshot' -e 'hasUnchangedActiveClaudeKeychainCredentials' -e 'writeActiveClaudeKeychainCredentials' -e 'removeActiveClaudeKeychainCredentials' "$dir" || test "$?" -eq 1
printf '%s\n' '--- complete restore implementation ---'
nl -ba "$dir/runtime-auth/runtime-auth-snapshot-restore.ts"
printf '%s\n' '--- keychain snapshot operations ---'
nl -ba "$dir/runtime-auth/runtime-auth-keychain-snapshots.ts" | sed -n '35,180p'

Repository: stablyai/orca

Length of output: 42616


🏁 Script executed:

set -eu
dir=src/main/claude-accounts
printf '%s\n' '--- sync write and rollback blocks ---'
rg -n -C 28 'writeActiveClaudeKeychainCredentials|restoreSystemDefaultSnapshot|catch \(error\)' "$dir/runtime-auth/runtime-auth-sync.ts" || test "$?" -eq 1
printf '%s\n' '--- ownership and file restoration helpers ---'
rg -n -C 20 'hasUnchangedRuntimeCredentials|hasUnchangedActiveClaudeKeychainCredentials|restoreRuntimeCredentials' "$dir/runtime-auth"
printf '%s\n' '--- keychain snapshot test cases ---'
rg -n -C 24 'partial|legacy|rollback|empty token|empty OAuth|Keychain' "$dir/runtime-auth-service-keychain-snapshots.test.ts" || test "$?" -eq 1

Repository: stablyai/orca

Length of output: 42647


🏁 Script executed:

set -eu
dir=src/main/claude-accounts
printf '%s\n' '--- Keychain read/write implementations ---'
rg -n -C 16 -F -e 'readActiveClaudeKeychainCredentialsStrict' -e 'writeActiveClaudeKeychainCredentialsForRuntime' "$dir/keychain.ts"
printf '%s\n' '--- ownership helpers ---'
rg -n -C 18 -F -e 'hasUnchangedRuntimeCredentials' -e 'hasUnchangedActiveClaudeKeychainCredentials' "$dir/runtime-auth"
printf '%s\n' '--- empty predicate call sites ---'
rg -n -F 'isEmptyClaudeOAuthCredentials(' "$dir"
printf '%s\n' '--- focused test setup and relevant test ---'
sed -n '1,115p' "$dir/runtime-auth-service-keychain-snapshots.test.ts"
sed -n '542,635p' "$dir/runtime-auth-service-keychain-snapshots.test.ts"

Repository: stablyai/orca

Length of output: 26799


🏁 Script executed:

set -eu
dir=src/main/claude-accounts
printf '%s\n' '--- shared credential merge definition ---'
rg -n -C 18 -F 'mergeSharedClaudeCredentialFields' "$dir"
printf '%s\n' '--- runtime credential restore definition ---'
rg -n -C 18 -F 'restoreRuntimeCredentials(' "$dir/runtime-auth"

Repository: stablyai/orca

Length of output: 41590


Treat null Keychain captures as empty rollback targets.

A successful Keychain read with no entry is recorded as captured with null, so the selected-account rollback guard is skipped. If the scoped write succeeds and the legacy write fails, rollback can restore the system credentials file and clear the managed OAuth credentials from the scoped Keychain while the account remains selected.

Classify null as empty. Also keep the pending rollback eligible for restoration if the account is deselected before a retry. Add a partial-write test with both initial Keychain entries set to null; check that managed auth survives the failure and that deselection restores the system file and clears the entries.

Suggested fix
diff --git a/src/main/claude-accounts/runtime-auth/runtime-auth-keychain-snapshots.ts b/src/main/claude-accounts/runtime-auth/runtime-auth-keychain-snapshots.ts
@@
-    if (this.isEmptyClaudeOAuthCredentials(credentialsJson)) {
+    if (credentialsJson === null || this.isEmptyClaudeOAuthCredentials(credentialsJson)) {
       return { status: 'empty' }
     }

diff --git a/src/main/claude-accounts/runtime-auth/runtime-auth-sync.ts b/src/main/claude-accounts/runtime-auth/runtime-auth-sync.ts
@@
-      if (this.lastSyncedAccountId !== null) {
+      if (this.lastSyncedAccountId !== null || this.lastWrittenCredentialsJson !== null) {
         await (previousAccount
           ? this.restoreSystemDefaultSnapshot(

@innocarpe

Copy link
Copy Markdown
Contributor Author

Closing as obsolete. Current main no longer has the Claude runtime-auth Keychain snapshot restore path this PR changes.

src/main/claude-accounts/runtime-auth/runtime-auth-keychain-snapshots.ts, runtime-auth-snapshot-capture.ts, and runtime-auth-snapshot-restore.ts are gone after the per-account folder work in #24434 and #26405 was re-landed in #26801 (8b2a5e45f1). Only runtime-auth-types.ts remains in that directory. Rebasing onto current main is a modify/delete conflict because the code under review was deleted upstream, not because of a textual overlap.

The CodeRabbit note about treating a null Keychain capture as an empty rollback target applied to that removed restore path. I am not porting it onto the new account-folder model in this PR.

@innocarpe innocarpe closed this Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant