Repository navigation
feat(claude): prepare account profiles and shared history (Step 1 of 4) - #24300
Conversation
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.
Review status: Step 1 of 3 reviewed, fixed and QA'd; left as draftThe problem: users with several Orca-managed Claude accounts keep being forced back to 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 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 Fixed during review (6 commits on this branch):
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 Deferred (low severity, recorded):
Verified:
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 CI at |
…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.
- 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.
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (1)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review. 📝 WalkthroughWalkthroughThis 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 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)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation 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.)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/main/claude-accounts/claude-profile-provisioning.test.ts (1)
162-175: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winGuard the symlink-creating tests with
it.skipIf(process.platform === 'win32').These tests create symlinks in two ways:
- They call
symlinkSyncdirectly.- 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 withplatform: '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
📒 Files selected for processing (17)
config/tsconfig.cli.jsonsrc/main/claude-accounts/claude-profile-history.test.tssrc/main/claude-accounts/claude-profile-history.tssrc/main/claude-accounts/claude-profile-paths.test.tssrc/main/claude-accounts/claude-profile-paths.tssrc/main/claude-accounts/claude-profile-prompt-history.tssrc/main/claude-accounts/claude-profile-provisioning.test.tssrc/main/claude-accounts/claude-profile-provisioning.tssrc/main/claude-accounts/claude-profile-report.tssrc/main/claude-accounts/claude-profile-setup.test.tssrc/main/claude-accounts/claude-profile-setup.tssrc/main/claude-accounts/claude-profile-sharing.tssrc/main/claude/claude-folder-trust-file.tssrc/main/claude/claude-profile-hook-target.tssrc/main/claude/claude-profile-hooks.test.tssrc/main/claude/hook-service.tssrc/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.
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
0e631dda-b350-4661-af1a-ce19d5510db9
📒 Files selected for processing (9)
src/main/claude-accounts/claude-profile-history.test.tssrc/main/claude-accounts/claude-profile-history.tssrc/main/claude-accounts/claude-profile-prompt-history.tssrc/main/claude-accounts/claude-profile-provisioning.test.tssrc/main/claude-accounts/claude-profile-provisioning.tssrc/main/claude-accounts/claude-profile-setup.test.tssrc/main/claude-accounts/claude-profile-setup.tssrc/main/claude-accounts/claude-profile-sharing.tssrc/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.
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.
provisionClaudeAccountProfileprepares an account folder at<Orca data>/claude-profiles/<account>/homeso it behaves like the user's normal Claude:settings.jsonis 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.mdis 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./resumeand session IDs work across accounts. An interrupted run finishes on the next run.CLAUDE_CONFIG_DIRwhen they set one, otherwise~/.claude.Safety checks:
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:
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_DIRcase and Windows behaviour (withprocess.platformpinned).A sandbox probe against Claude 2.1.289 and 2.1.291 with a fake login showed:
history.jsonlin place;/resumeworks 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) andclaude purgeact 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
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)