Skip to content

feat(claude): prepare account profiles and shared history (Step 1 of 4) - #24300

Merged
Jinwoo-H merged 16 commits into
mainfrom
brennanb2025/claude-profile-setup
Oct 7, 2026
Merged

Jinwoo-H merged 16 commits into
mainfrom
brennanb2025/claude-profile-setup

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 5 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1164 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1164
Prod 12 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1269 $\color{#cf222e}{\Huge{\mathbf{−}}}$​26 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1243

ELI5

Orca used to keep a spare copy of each Claude account's login and keep swapping it into the one place Claude reads its login from. Claude replaces its login every so often, so whenever Orca put back an older copy, Claude's server rejected it and you had to sign in again.

The fix gives each account its own Claude folder, where Claude signs in and looks after its own login. Orca never touches logins again. This PR builds those folders. Nothing calls it yet, so nothing changes for users.

What Changed

Step 1 of 4 for STA-3698 (#9582). Steps 2–4 are #24351, #24384 and #24434.

provisionClaudeAccountProfile prepares an account folder at <Orca data>/claude-profiles/<account>/home so it behaves like the user's normal Claude:

  • Setup is linked: skills, plugins, agents, commands, output styles, rules, themes and workflows link to the user's own folder.
  • Settings are copied: settings.json is merged key by key. A value changed inside an account is kept; login-related keys stay private. Orca's hooks and status line come along as ordinary settings.
  • CLAUDE.md is a one-line import of the user's file. Copying it made Claude load the user's instructions twice in projects under the home folder.
  • .claude.json: only MCP servers, theme, the onboarding flag and folder trust are merged, and only into a file Claude already created.
  • History (session transcripts, prompt history and the like) is moved aside, linked into the user's folder and merged without overwriting anything. This lets /resume and session IDs work across accounts. An interrupted run finishes on the next run.
  • Windows keeps history per account. Only setup folders are linked, using junctions.
  • "The user's folder" means their own CLAUDE_CONFIG_DIR when they set one, otherwise ~/.claude.

Safety checks:

  • The account folder must sit inside Orca's data folder, with no linked path parts, and outside the user's folder.
  • History is linked only when both sides are on the same drive.
  • Nothing in the user's folder is overwritten or deleted.

Why

A separate config folder per account (CLAUDE_CONFIG_DIR) is how Anthropic documents running several Claude accounts. Each folder gets its own login and its own Keychain entry, so no process ever replays an old login. The layout follows the common pattern other multi-account tools use.

We considered two alternatives:

  • Keep swapping one shared login, but never write an older one. It can't be made race-free: a running Claude keeps renewing its login without Orca seeing it.
  • Long-lived tokens. Claude.ai connectors and Remote Control stop working.

Linked Issue

Fixes STA-3698 (with #24434). Refs #9582.

Visual Proof

N/A. Nothing calls this code yet.

Testing

  • Unit tests cover setup, history merge and interrupted-merge recovery, settings merge, path refusal, the custom CLAUDE_CONFIG_DIR case and Windows behaviour (with process.platform pinned).

  • A sandbox probe against Claude 2.1.289 and 2.1.291 with a fake login showed:

    • Claude adds to transcripts and history.jsonl in place;
    • three Claudes writing to one shared history left every line intact;
    • /resume works across linked folders.
  • Not yet tested with real signed-in accounts; that's planned before fix(claude): activate account profiles and remove credential replay (Step 4 of 4) #24434.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

Review

Known and accepted: Claude's automatic cleanup (cleanupPeriodDays) and claude purge act through the history links, so a delete in one account applies to all. This is the same as one shared ~/.claude.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

Review round 1 of the dormant profile setup found that the pieces could
be called without their safety checks, that one failed write or an
unreadable bookkeeping file could silently stop sharing for good, and
that Windows prompt history could bring back history the user cleared.

- One entry, provisionClaudeAccountProfile: the profile gate (namespace,
  no linked components, outside ~/.claude and ~/.config/claude, and an
  ownership marker beside the home naming the account and target) runs
  first and refuses before creating anything; then history sharing,
  config provisioning, and the hook install after the settings merge.
  Results come back per surface with closed warning codes instead of
  message text.
- The sharing ledger is keyed by surface name, records a value only
  after its write succeeded, and an unreadable ledger starts empty and
  is rewritten instead of blocking every surface.
- The profile state file goes through the same locked writer as folder
  trust (Claude's <file>.lock plus the in-process queue), generalized as
  updateClaudeGlobalConfig. Onboarding and trust are still applied when
  the personal state file is unreadable.
- WSL descriptors build guest POSIX paths; the state-file path style
  follows the injected platform.
- Orca's managed statusLine has one owner in a profile: the settings
  merge never shares it, a user's own statusLine is shared over it, and
  the profile installer follows the default home's slot so a default
  opt-out reaches every profile. remove() takes the same destination;
  the remote installer cannot accept one.
- Prompt history compares file identity (bigint dev+ino) on every
  platform, never drains the shared file into itself, drains retained
  copies in generation order, never reuses a stale cursor, and on Windows
  keeps a replaced default's old copy aside instead of replaying it.
  Directory merges keep going past a failed entry.
A user's own Claude hooks in ~/.claude (notifications, formatters) did
not run under a managed account, because the whole hooks key stayed
private. They are now shared like any other settings key: Orca's own
hook entries and its managed statusLine are stripped from both the
personal value and the profile's current value before the per-key
ledger comparison, so they never travel through the merge and never make
the key look user-owned. Orca entries already in the profile are kept on
write, and the profile hook installer adds them on top as before.

Prompt history: merged bytes that lack a final newline are terminated,
so Claude's next record no longer fuses onto the last merged line. When
a CLI rewrote the profile's history file (old records plus new), only
the lines past the part it shares with the default history are added,
instead of the whole file again.
Hooks and statusLine sharing:
- When ~/.claude holds only Orca's hook entries, the user's shared hooks
  now read as an empty value instead of a missing key. Removing the
  user's last own hook in ~/.claude therefore reaches profiles that
  never edited it, and deleting the only shared hook inside a profile
  stays deleted.
- A custom statusLine Orca shared, and the profile never edited, goes
  away when the default home drops it. When a shared custom line
  replaced Orca's line in a profile, the profile's statusline marker is
  dropped so Orca's line comes back once the default returns to it; a
  profile that opted out stays opted out. No other key gains deletion.
- install/remove/getStatus with a profile directory refuse when it is
  the default home, or its settings.json resolves to the default one,
  instead of editing System Default's hooks and opt-out state.
- The profile statusline rule reads the default settings under the
  userHome passed to the setup entry, not os.homedir().

Profile state and ownership:
- A malformed `projects` value skips only folder trust (new warning
  code trust-refused); onboarding and shared keys still apply.
- The ownership marker stores only host-local facts (account, runtime,
  distro). The execution host id is the caller's view of the host, so
  it stays in the in-memory descriptor and is not compared.

Prompt history interruption paths:
- With no cursor yet, a retained copy starts past the bytes it shares
  with the default history, so an interrupted share no longer replays
  the whole history.
- A retained name for the shared file itself is removed with its cursor
  instead of lingering until a later scrub makes it look new.
- The Windows link record is read three-state: unreadable stops the
  share instead of reading as "no link". If the record cannot be
  written after linking, the fresh link is undone.
- An unreadable retained copy is reported and no longer blocks linking.
hook-service.ts and hook-settings.ts are compiled into the packaged CLI
project, which lists every file explicitly. The statusline policy and
profile destination modules they now import were missing, so the CLI
typecheck failed with TS6307. The CLI still loads hook-service through
the existing managed-agent-hook-controls build entry, which bundles
both modules; neither imports electron.
- A profile whose hooks hold only Orca's entries and that sharing never
  recorded is no longer treated as a user edit, so the user's first own
  hook in ~/.claude reaches it (for example when the profile was set up
  before ~/.claude had any hooks).
- A retained prompt-history file is removed as a second name for the
  shared file only when the default history does not itself link to it;
  otherwise it holds the only copy and is kept.
- Default-home checks compare file identity: the profile hook
  destination check uses device and inode, and the profile/default
  separation check resolves on-disk case, so a case-only alias of
  ~/.claude is refused on case-insensitive filesystems.
- A test pins that an unreadable leftover session tree no longer blocks
  linking.
QA found that removing a setting from ~/.claude never reached a managed
account: deleting the whole `hooks` block left the user's hook running
there. Only statusLine followed the default away.

Every shared key now follows the same rule through the existing per-key
ledger: when a key disappears from ~/.claude/settings.json (or
mcpServers/theme from the personal state file), it is removed from the
profile if the profile still holds exactly what Orca last shared. A
value changed inside the account is kept. Keys Orca never shared,
including denylisted ones, are never touched. Deleting the whole hooks
block removes the user's shared hooks and keeps Orca's own entries. A
missing source counts as empty; an unreadable source removes nothing.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status: Step 1 of 3 reviewed, fixed and QA'd; left as draft

The problem: users with several Orca-managed Claude accounts keep being forced back to /login, because Orca copies login tokens between folders and can overwrite Claude's newer login with an older copy (STA-3698, #9582).

What this PR changes for users today: nothing. It is dormant Step 1: it adds the per-account Claude folder setup that Steps 2 and 3 will switch on. The re-login bug is fixed only when Step 3 lands. Orca's existing hook/statusline install into ~/.claude writes the same bytes as main.

What it will mean once Steps 2-3 land: each account gets its own complete Claude folder that Claude signs into itself; your skills, plugins, settings, CLAUDE.md, MCP servers and your own hooks follow you into every account, a setting you delete from ~/.claude disappears from accounts that still had Orca's copy, and session and prompt history stay shared, so switching accounts never loses --resume or Up-arrow history.

Fixed during review (6 commits on this branch):

  • eb13d276321: one entry point (provisionClaudeAccountProfile) with an ownership check before anything is created, typed per-item report instead of free-text errors, settings ledger keyed by name and recorded only after a successful write (an unreadable ledger no longer blocks setup forever), state file written through Claude's existing lock, forced "onboarding done" even when the personal state file is unreadable, statusline with one owner, hook remove for an account folder, prompt-history fixes (no replay of a hard-linked history, no resurrection of history you cleared on Windows, ordered drains, no reused cursors).
  • 8ec7435d41e: your own hooks are shared into every account (Orca's entries kept out of the merge); merged prompt history always ends with a newline (QA reproduced fused records on APFS and NTFS); a rewritten history file no longer duplicates every line.
  • 39738c78379: deleting your last hook reaches accounts; a shared custom statusline follows ~/.claude back; hook install/remove refuse a folder that resolves to ~/.claude; the ownership marker stores only host-local facts; interrupted history runs never re-append or resurrect scrubbed lines.
  • c8662162bac: CI typecheck fix: the new hook modules are listed in the CLI project.
  • af69594f2d5: first user hook reaches an account provisioned before you had hooks; a history cleanup can no longer delete the only copy; case-only aliases of ~/.claude (e.g. ~/.CLAUDE on macOS) are refused.
  • d311c84198e: deleting a whole setting (e.g. the hooks block or model) or an MCP server from ~/.claude removes it from accounts that still hold Orca's copy; values you changed inside an account are kept.

Decisions taken in review (both labelled intended in the PR body): your own hooks are shared into accounts instead of kept private, and deletions in ~/.claude propagate to untouched accounts; both match what one shared ~/.claude gives you today.

Deferred (low severity, recorded):

  • Profiles get Orca's hooks but no Orca statusline unless Step 2 passes the probed Claude version into the hook-install callback.
  • First history merge of a very large history (~20,000 files) blocks ~2 s; Step 2 must run it off the main thread.
  • An unreadable older leftover history file merges after newer prompts (order only, nothing lost); a crash between append and cursor save can duplicate one prompt.
  • A user's dangling link is reported as failed rather than user-owned; a ~/.claude entry symlinked back into the account folder is not detected; custom-OAuth state file name not supported; modules could live in a subfolder.
  • Recommendation from the architecture review: merge this only together with Step 2, after Step 2's call sites are reviewed against these APIs.

Verified:

  • Three review rounds (provisioning, history, hooks, architecture) plus a narrow fourth check; every fix re-checked against the original repro; 51 guard ablations each turned a test red.
  • Default hook/statusline install with no account folder: byte-identical to main across 338 snapshot sequences (claude, openclaude, qoder, codebuddy; macOS/Linux/simulated Windows) and on real dev builds on a remote Mac (screenshots in the PR body).
  • Real filesystems at d311c84198e: macOS APFS 64/64 and Windows NTFS 66/66 checks pass, including junctions, the hardlinked history, two processes appending 5,000 lines each with no torn or lost lines, and the deletion cases.
  • Typecheck (node + CLI) passes locally on every pushed commit; 26 named test files, 340 passed; readiness checklist: ready for the dormant scope, no P0/P1.

Not verified: Windows cross-volume fallback (the QA box has a single volume), WSL guest execution, real Claude CLI behaviour in these folders, real Keychain naming (planned for a disposable VM in Step 3).

Not caused by this PR: an earlier CI static-analysis failure came from structured-agent-session-adapter.ts on main exceeding max-lines; it is green on the current head.

CI at d311c84198e: all checks green (16 passed incl. typecheck, static analysis, the five unit-test shards and package; 16 skipped by path filters). The PR stays a draft; it was not merged or marked ready.

…into account profiles

A managed account launches Claude with its own config folder, so user-level
rules/, custom themes/ (which a shared `custom:<slug>` theme points at),
personal workflows/ and keybindings.json silently stopped applying. Link the
three directories like skills and commands, and copy keybindings.json with the
same edit-preserving ledger as CLAUDE.md. routines/ stays unshared: routines
belong to the claude.ai account and the folder holds per-run state.
…ead of copying it

Claude also loads ~/.claude/CLAUDE.md as a parent folder's memory for any project under home,
so a copied account CLAUDE.md made every such session read the user's instructions twice
(checked live with Claude 2.1.288). An @~/.claude/CLAUDE.md import resolves to the same real
file, which Claude loads once from home, from projects under home and from folders outside it.
@brennanb2025 brennanb2025 changed the title feat(claude): prepare account profiles and shared history (step 1 of 3) feat(claude): prepare account profiles and shared history (Step 1 of 4) Oct 5, 2026
- Windows keeps each account's history private; drop the hardlink, link
  record and conflict-copy machinery that only Windows reached.
- Share hooks and statusLine as ordinary settings keys: Orca writes the
  same entries into every folder, so the installer finds them present.
  Drops the Orca-entry carve-out, the per-profile statusline follow
  logic and its marker.
- Unreadable ledger is just an empty ledger.
- Share from the user's own CLAUDE_CONFIG_DIR when they set one (marked
  so Orca's injected value is never mistaken for it), and refuse a
  profile at or around it.
- Pin the one canonical profile path spelling in a test.
@Jinwoo-H
Jinwoo-H marked this pull request as ready for review October 6, 2026 21:18
@coderabbitai

coderabbitai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 295fed73-efeb-40a3-9b84-e5b265f39be1
📥 Commits

Reviewing files that changed from the base of the PR and between af0680a and 504cf3a.

📒 Files selected for processing (1)
  • config/tsconfig.cli.json

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


📝 Walkthrough

Walkthrough

This change adds Claude account profile path validation and ownership markers, history sharing, and provisioning for selected resources, settings, and state. It adds a ledger and profile setup reporting for surface outcomes and warnings. Hook status and installation can target a profile config directory, with checks that prevent profile operations from targeting the default home. Folder-trust updates now use a generic config-change operation.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 504cf

Profile setup has no tracked application caller in this revision, so the history-sharing race does not affect current user workflows. Fix it before enabling that flow.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.60% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 57 functions across 16 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main changes: preparing Claude account profiles and sharing history. “Step 1 of 4” provides useful scope context.
Description check ✅ Passed The description is detailed and covers the template’s main sections, including the change, rationale, linked issue identifier, visual proof, testing, review notes, and checklist. It also states that a…
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.
Full details: Docstring Coverage

Explanation

Docstring coverage is 38.60% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 57 functions across 16 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
  • 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

🧹 Nitpick comments (1)
src/main/claude-accounts/claude-profile-provisioning.test.ts (1)

162-175: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Guard the symlink-creating tests with it.skipIf(process.platform === 'win32').

These tests create symlinks in two ways:

  • They call symlinkSync directly.
  • They inject platform: 'linux', so provisioning creates 'dir' symlinks, not junctions.

On a Windows runner without symlink privilege, these tests fail with EPERM.

  • src/main/claude-accounts/claude-profile-provisioning.test.ts#L162-L175: guard this test and the alias test at Lines 336-354. Also guard every other test whose fixture has a source resource directory, because provisioning creates 'dir' links for those directories. Keep the 'junction' test at Line 385 unguarded.
  • src/main/claude-accounts/claude-profile-setup.test.ts#L98-L104: guard the data-root symlink test. Also guard the tests that run the setup flow with platform: 'linux' and source resource directories.
  • src/main/claude/claude-profile-hooks.test.ts#L134-L143: guard the linked-settings refusal test.

Based on learnings: "any test that creates filesystem symlinks ... should be guarded with it.skipIf(process.platform === 'win32')".

Source: Learnings


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: afb82780-694d-484f-851b-945bd0a11fcc
📥 Commits

Reviewing files that changed from the base of the PR and between 2c2dfd0 and 95a3284.

📒 Files selected for processing (17)
  • config/tsconfig.cli.json
  • src/main/claude-accounts/claude-profile-history.test.ts
  • src/main/claude-accounts/claude-profile-history.ts
  • src/main/claude-accounts/claude-profile-paths.test.ts
  • src/main/claude-accounts/claude-profile-paths.ts
  • src/main/claude-accounts/claude-profile-prompt-history.ts
  • src/main/claude-accounts/claude-profile-provisioning.test.ts
  • src/main/claude-accounts/claude-profile-provisioning.ts
  • src/main/claude-accounts/claude-profile-report.ts
  • src/main/claude-accounts/claude-profile-setup.test.ts
  • src/main/claude-accounts/claude-profile-setup.ts
  • src/main/claude-accounts/claude-profile-sharing.ts
  • src/main/claude/claude-folder-trust-file.ts
  • src/main/claude/claude-profile-hook-target.ts
  • src/main/claude/claude-profile-hooks.test.ts
  • src/main/claude/hook-service.ts
  • src/main/claude/hook-settings.ts

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

…es, link setup folders by path

- Prompt-history drain appends only lines the shared file lacks, so a purge never re-adds lines.
- A set-aside history copy whose saved offset reaches its end is deleted on the next run.
- Setup folders link to the default home's own entry, not its resolved target.
- The profile gate and folder creation run once, in provisionClaudeAccountProfile.
- installHooks receives only configDir; drop a duplicate test key that fails CI.

@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: 0e631dda-b350-4661-af1a-ce19d5510db9
📥 Commits

Reviewing files that changed from the base of the PR and between 6077bd9 and 8ef92cf.

📒 Files selected for processing (9)
  • src/main/claude-accounts/claude-profile-history.test.ts
  • src/main/claude-accounts/claude-profile-history.ts
  • src/main/claude-accounts/claude-profile-prompt-history.ts
  • src/main/claude-accounts/claude-profile-provisioning.test.ts
  • src/main/claude-accounts/claude-profile-provisioning.ts
  • src/main/claude-accounts/claude-profile-setup.test.ts
  • src/main/claude-accounts/claude-profile-setup.ts
  • src/main/claude-accounts/claude-profile-sharing.ts
  • src/main/runtime/structured-agent-session-codex-stopped-send-order.test.ts
💤 Files with no reviewable changes (1)
  • src/main/runtime/structured-agent-session-codex-stopped-send-order.test.ts

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

…link tests on Windows

After Orca installs its hooks into an account, record the account's hooks in
the settings ledger so a later run can still bring the user's own hooks in.
Tests that create real symlinks now skip on Windows.
@Jinwoo-H Jinwoo-H closed this Oct 7, 2026
@Jinwoo-H Jinwoo-H reopened this Oct 7, 2026
@Jinwoo-H
Jinwoo-H merged commit dff65d5 into main Oct 7, 2026
51 of 53 checks passed
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.

2 participants