feat(agent-hooks): install managed Claude hooks into discovered CLAUDE_CONFIG_DIR config dirs#9416
feat(agent-hooks): install managed Claude hooks into discovered CLAUDE_CONFIG_DIR config dirs#9416bbingz wants to merge 8 commits into
Conversation
…l convention Home-dir entries matching .claude-<name> / .claude.<name> are treated as Claude config dirs when a marker file (settings.json | .claude.json | .credentials.json) exists inside. stat/readdir only — contents are never read and listings are never logged (these dirs hold credentials). Works over local fs and any SFTP-shaped transport (SSH, WSL fs bridge). The shared flavor-label helper turns a configDir path into the sidebar label suffix (~/.claude-grok -> grok).
- updateHooksJsonWithRetry: read-modify-write with a compare-and-retry stale check (re-read before replace, bounded attempts, guard dropped on the final attempt so install converges). ClaudeHookService install/remove now re-merge instead of clobbering keys a concurrent writer (the CLI itself, a second Orca instance) added between read and replace. - Remote (SFTP) writes take a one-shot .orca-backup of an existing settings file before Orca's first modification; never rotated later. - Widen ClaudeCompatibleHookSettings.configDirName to string so discovered CLAUDE_CONFIG_DIR flavor dirs can reuse the per-dir service shape.
…g dirs (local) One ClaudeHookService per discovered ~/.claude-<name> dir — N dirs mean N settings.json writes all pointing at the one shared claude-hook script. The lifecycle rides the existing claude entry in the managed install/remove/status flow (same agentStatusHooksEnabled gate). Install ledger under userData/agent-hooks records every dir Orca wrote into (union across runs, written before installing), so uninstall cleans exactly the touched dirs even when discovery results change later. Ledger entries are re-validated against the naming convention on read. Status folds per-dir health into the claude row as a count-only detail — dir names never appear in status text or logs.
…and WSL remotes installRemoteManagedAgentHooks now runs config-dir discovery over the same SFTP-shaped transport after the static agents, so SSH remotes and the WSL fs bridge both inherit it with no per-host wiring. POSIX remotes only, as before (Windows remotes stay skipped upstream). Discovery/install failures degrade status reporting only and log no dir names. The static-agent parity invariant is unchanged by design — dynamic dirs are instances of the registered claude agent, covered by their own remote + WSL tests.
…into modules Mechanical extraction, no behavior change: the hooks settings JSON contract (types + read/write/retry primitives) moves to hooks-json-file.ts and the timeout-guarded ssh2 wrappers move to sftp-file-primitives.ts. Restores the max-lines budget the concurrency guard exceeded; installer-utils re-exports the JSON surface so call sites keep their single historical import.
…sidebar The POSIX hook script and the Windows .cmd variant post a configDir form field (path string only — never tokens; empty for default installs). The listener threads it into the parsed status payload for claude posts, it persists/hydrates like other payload fields, and the compact sidebar agent row renders 'Claude · <flavor>' (~/.claude-grok -> grok) when an entry carries a non-default config dir. Payloads without the field behave exactly as before (back-compat with already-installed scripts).
- Make claude-config-dir-hook-controls electron-free: the module rides in the standalone orca CLI build (ELECTRON_RUN_AS_NODE, no electron in packaged node_modules), so a static electron import crashed every CLI command. Lazy-require electron and fall back to the shared userData convention, extracted to src/shared/orca-user-data-path.ts and reused by src/cli/runtime/metadata.ts. Guard test scans the whole tsconfig.cli.json include set for static electron imports. - Reinstate the remove() no-op gate: updateHooksJsonWithRetry mutate may return null (nothing to change) so a no-op removal no longer creates a missing settings.json, reformats/.bak-rolls a user file without managed hooks, or resurrects a deleted flavor dir. - Prune ledger entries whose dir was deleted entirely (install persists the prune, status skips them) so a legitimately deleted dir cannot pin the aggregated claude status at 'partial' forever. - Emit one name-free console.warn when a discovered-dir install fails, matching the remote flow's diagnosability.
📝 WalkthroughWalkthroughThe change adds Claude flavor config-directory discovery and ledgered hook lifecycle management for local, remote, and WSL environments. Hook JSON updates gain atomic writes, backups, and stale-write retries, while SFTP operations are centralized behind timeout-guarded primitives. Claude hook payloads now include bounded 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
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: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/claude/hook-service.ts (1)
305-330: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
remove()silently swallowsupdateHooksJsonWithRetryfailures.Unlike
install()(lines 222-233), which checks the return value and reportsstate: 'error'when the retry-update fails,remove()discards the result entirely. If all retry attempts fail (unparseable baseline or repeated write/backup failures under concurrent settings.json edits), the managed hooks are not actually removed, butremove()still proceeds togetStatus()without ever surfacing anerrorstate ordetailexplaining why removal did not take effect.🛡️ Proposed fix mirroring install()'s error handling
- updateHooksJsonWithRetry(configPath, (current) => { + const updated = updateHooksJsonWithRetry(configPath, (current) => { const { config: next, changed } = removeManagedHooks( current, getManagedScriptFileName(this.options.settings) ) return changed ? next : null }) + if (!updated) { + return { + agent: this.options.agent, + state: 'error', + configPath, + managedHooksPresent: false, + detail: `Could not parse ${this.options.displayName} settings.json` + } + } return this.getStatus()
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 9b972146-206b-4003-905f-181777cc5193
📒 Files selected for processing (29)
config/tsconfig.cli.jsonsrc/cli/cli-build-electron-free.test.tssrc/cli/runtime/metadata.tssrc/main/agent-hooks/hooks-json-file.tssrc/main/agent-hooks/installer-utils-remote.test.tssrc/main/agent-hooks/installer-utils-remote.tssrc/main/agent-hooks/installer-utils.test.tssrc/main/agent-hooks/installer-utils.tssrc/main/agent-hooks/managed-agent-hook-controls.tssrc/main/agent-hooks/remote-hook-service-installers.test.tssrc/main/agent-hooks/remote-managed-hook-installers.tssrc/main/agent-hooks/sftp-file-primitives.tssrc/main/agent-hooks/wsl-hook-relay-manager.test.tssrc/main/claude/claude-config-dir-discovery.test.tssrc/main/claude/claude-config-dir-discovery.tssrc/main/claude/claude-config-dir-hook-controls.test.tssrc/main/claude/claude-config-dir-hook-controls.tssrc/main/claude/hook-service.test.tssrc/main/claude/hook-service.tssrc/main/claude/hook-settings.tssrc/renderer/src/components/sidebar/worktree-card-compact-agent-row.tsxsrc/renderer/src/hooks/useIpcEvents.tssrc/renderer/src/store/slices/agent-status.tssrc/shared/agent-hook-listener.test.tssrc/shared/agent-hook-listener.tssrc/shared/agent-status-types.test.tssrc/shared/agent-status-types.tssrc/shared/claude-config-dir-label.tssrc/shared/orca-user-data-path.ts
|
Addressed the remaining non-inline finding from CodeRabbit review #9416 (review) in f8dd301. |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/claude/claude-config-dir-hook-controls.ts (1)
125-144: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winRemove redundant
remainingarray to prevent potential race conditions.The
remainingarray collects directories that encountered an error during removal. However, because these directories are never added to theremovedSet, they are inherently preserved by thecurrent.filter((dirName) => !removed.has(dirName))logic.Concatenating
...remainingis not only redundant but can introduce a race condition: if a concurrent process successfully removes one of these directories from the ledger before this callback executes, unconditionally appending...remainingwill erroneously resurrect the deleted directory.🧹 Proposed fix to rely strictly on the `removed` Set
- const remaining: string[] = [] const removed = new Set<string>() for (const dirName of readClaudeConfigDirLedger(ledgerPath)) { let status: AgentHookInstallStatus try { status = createService(dirName).remove() } catch (error) { status = errorStatus(dirName, error) } - if (status.state === 'error') { - remaining.push(dirName) - } else { + if (status.state !== 'error') { removed.add(dirName) } statuses.push(status) } - updateClaudeConfigDirLedger(ledgerPath, (current) => [ - ...current.filter((dirName) => !removed.has(dirName)), - ...remaining - ]) + updateClaudeConfigDirLedger(ledgerPath, (current) => + current.filter((dirName) => !removed.has(dirName)) + )
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 033efd53-ee65-4cdc-91c5-266b94085983
📒 Files selected for processing (9)
config/tsconfig.cli.jsonsrc/main/agent-hooks/installer-utils-remote.test.tssrc/main/agent-hooks/installer-utils-remote.tssrc/main/claude/claude-config-dir-discovery.tssrc/main/claude/claude-config-dir-hook-controls.test.tssrc/main/claude/claude-config-dir-hook-controls.tssrc/main/claude/claude-config-dir-ledger.tssrc/main/claude/hook-service.test.tssrc/main/claude/hook-service.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- config/tsconfig.cli.json
- src/main/claude/claude-config-dir-hook-controls.test.ts
- src/main/agent-hooks/installer-utils-remote.ts
- src/main/claude/hook-service.ts
| const tmpPath = join(dirname(ledgerPath), `.${Date.now()}-${randomUUID()}.tmp`) | ||
| try { | ||
| writeFileSync(tmpPath, serialized, 'utf-8') | ||
| // Why: a second Orca process may have updated the shared ledger after our | ||
| // read. Abort and re-merge instead of replacing its newly tracked dirs. | ||
| if (readLedgerBaseline(ledgerPath) !== expectedDiskContent) { | ||
| return false | ||
| } | ||
| renameSync(tmpPath, ledgerPath) | ||
| return true | ||
| } finally { | ||
| if (existsSync(tmpPath)) { | ||
| try { | ||
| unlinkSync(tmpPath) | ||
| } catch { | ||
| // Best-effort cleanup after a failed or stale replace. | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Fix the time-of-check to time-of-use (TOCTOU) race condition during file update.
Although this logic attempts an optimistic compare-and-swap, the sequence of readLedgerBaseline followed by renameSync is not atomic. If two processes concurrently verify the baseline and both proceed to renameSync, the earlier process's update will be silently overwritten and lost.
To achieve mutual exclusion and make the compare-and-rename operation truly atomic across processes, you can use a lock file with the exclusive creation flag (wx). This flawlessly integrates with your existing maxAttempts retry loop.
🔒️ Proposed fix to synchronize updates
- const tmpPath = join(dirname(ledgerPath), `.${Date.now()}-${randomUUID()}.tmp`)
- try {
- writeFileSync(tmpPath, serialized, 'utf-8')
- // Why: a second Orca process may have updated the shared ledger after our
- // read. Abort and re-merge instead of replacing its newly tracked dirs.
- if (readLedgerBaseline(ledgerPath) !== expectedDiskContent) {
- return false
- }
- renameSync(tmpPath, ledgerPath)
- return true
- } finally {
- if (existsSync(tmpPath)) {
- try {
- unlinkSync(tmpPath)
- } catch {
- // Best-effort cleanup after a failed or stale replace.
- }
- }
- }
+ const lockPath = `${ledgerPath}.lock`
+ try {
+ // Atomically acquire lock
+ writeFileSync(lockPath, '', { flag: 'wx' })
+ } catch (err) {
+ if ((err as NodeJS.ErrnoException).code === 'EEXIST') {
+ return false // Another process is currently updating; trigger a retry
+ }
+ throw err
+ }
+
+ const tmpPath = join(dirname(ledgerPath), `.${Date.now()}-${randomUUID()}.tmp`)
+ try {
+ writeFileSync(tmpPath, serialized, 'utf-8')
+ // Why: a second Orca process may have updated the shared ledger after our
+ // read. Abort and re-merge instead of replacing its newly tracked dirs.
+ if (readLedgerBaseline(ledgerPath) !== expectedDiskContent) {
+ return false
+ }
+ renameSync(tmpPath, ledgerPath)
+ return true
+ } finally {
+ try {
+ unlinkSync(lockPath)
+ } catch {
+ // Best-effort cleanup
+ }
+ if (existsSync(tmpPath)) {
+ try {
+ unlinkSync(tmpPath)
+ } catch {
+ // Best-effort cleanup after a failed or stale replace.
+ }
+ }
+ }📝 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.
| const tmpPath = join(dirname(ledgerPath), `.${Date.now()}-${randomUUID()}.tmp`) | |
| try { | |
| writeFileSync(tmpPath, serialized, 'utf-8') | |
| // Why: a second Orca process may have updated the shared ledger after our | |
| // read. Abort and re-merge instead of replacing its newly tracked dirs. | |
| if (readLedgerBaseline(ledgerPath) !== expectedDiskContent) { | |
| return false | |
| } | |
| renameSync(tmpPath, ledgerPath) | |
| return true | |
| } finally { | |
| if (existsSync(tmpPath)) { | |
| try { | |
| unlinkSync(tmpPath) | |
| } catch { | |
| // Best-effort cleanup after a failed or stale replace. | |
| } | |
| } | |
| } | |
| const lockPath = `${ledgerPath}.lock` | |
| try { | |
| // Atomically acquire lock | |
| writeFileSync(lockPath, '', { flag: 'wx' }) | |
| } catch (err) { | |
| if ((err as NodeJS.ErrnoException).code === 'EEXIST') { | |
| return false // Another process is currently updating; trigger a retry | |
| } | |
| throw err | |
| } | |
| const tmpPath = join(dirname(ledgerPath), `.${Date.now()}-${randomUUID()}.tmp`) | |
| try { | |
| writeFileSync(tmpPath, serialized, 'utf-8') | |
| // Why: a second Orca process may have updated the shared ledger after our | |
| // read. Abort and re-merge instead of replacing its newly tracked dirs. | |
| if (readLedgerBaseline(ledgerPath) !== expectedDiskContent) { | |
| return false | |
| } | |
| renameSync(tmpPath, ledgerPath) | |
| return true | |
| } finally { | |
| try { | |
| unlinkSync(lockPath) | |
| } catch { | |
| // Best-effort cleanup | |
| } | |
| if (existsSync(tmpPath)) { | |
| try { | |
| unlinkSync(tmpPath) | |
| } catch { | |
| // Best-effort cleanup after a failed or stale replace. | |
| } | |
| } | |
| } |
|
Taking a look! |
agent-hooks: install managed Claude hooks into discovered
CLAUDE_CONFIG_DIRconfig dirsPart of #9414. Independent of the
close-intent PR — the two can land in either order.
Problem
Orca's managed Claude status hooks are only written to
~/.claude/settings.json(and
~/.openclaude). A genuineclaudelaunched withCLAUDE_CONFIG_DIR=$HOME/.claude-<name>— a common wrapper pattern foralternate Anthropic-compatible endpoints — loads settings exclusively from that
dir, so it never picks up Orca's hooks and posts no status events: no sidebar
state, no working/done transitions, no tool readout. This affects local, SSH,
and WSL hosts equally.
What this PR does
Discovers Claude-shaped config dirs in the home directory and installs the
SAME managed hooks into each of them, on all three host kinds. No new hook
endpoints: these agents are genuine claude and post to the existing
/hook/claude, via the one shared~/.orca/agent-hooks/claude-hook.sh(.cmd)script — N dirs mean N
settings.jsonwrites pointing at the same script.Discovery rules and privacy posture
^\.claude[-.]<suffix>(no vendor names anywhere in code — dir basenamesare user-observed data), excluding
.claudeand.openclaudethemselves(already managed), AND a marker file exists inside it:
settings.json|.claude.json|.credentials.json.readdir+statONLY. These dirs hold credentials, sofile contents are never read, and dir listings / entry names are never
logged (tests assert both). The marker probe doubles as the directory
check: a regular file named
.claude-foohas no children, so its markerstat fails and it is excluded.
have to be parsed as code to find
CLAUDE_CONFIG_DIRassignments, and envinspection of running processes leaks exactly the secrets-adjacent context
we refuse to touch. The dot-dir convention is observable with two cheap,
content-free syscalls and covers the wrapper pattern in the wild.
extra dirs" and never blocks the default
.claudeinstall.Install ledger
userData/agent-hooks/claude-config-dirs.jsonrecords every dir Orcainstalled into:
crash mid-install still leaves every touched dir tracked.
skipped by status (nothing is left to clean in a deleted dir), so a
legitimately deleted flavor dir cannot pin the claude row at
partial;entries whose dir exists but whose removal failed stay ledgered for retry.
agentStatusHooksEnabledtoggle) removes managed entriesfrom exactly the ledgered dirs — even dirs discovery no longer returns
(e.g. marker deleted after install) — then clears the ledger; dirs whose
removal failed stay ledgered for retry.
a corrupted/tampered ledger can never drive writes into
.claudeitselfor an arbitrary path.
partialdetail; dir names never appear in status text or logs.Concurrency safety (from adversarial review)
The settings.json read-modify-write had no stale check: a write by the CLI
itself (or a second Orca instance) landing between Orca's read and its
atomic replace was silently clobbered. Now:
updateHooksJsonWithRetryre-reads the file just before the tmp→renamereplace; on divergence the attempt is discarded and re-run against fresh
content (bounded attempts; the final attempt drops the guard so install
still converges under a pathological writer). Claude install/remove — and
therefore every discovered-dir service — use it. A mutate callback may
return
nullto signal "nothing to change", which skips the writeentirely —
remove()uses this so a no-op removal never creates a missingsettings.json, reformats a user file Orca never managed, or rolls its
.bak.unguarded, so a writer that races us on every attempt can still have keys
written inside the last read→rename window clobbered (only keys present at
our final read survive). The guard's CAS re-read also happens just before
the
.bakcopy + rename, leaving a small TOCTOU window even on guardedattempts. Both are bounded and accepted: install re-runs on next app
start, and converging beats failing here.
settings.json.orca-backupbefore Orca's FIRST modification of anexisting file. Unlike the local rolling
.bakit is never rotated, so thepre-Orca original stays recoverable across a transport with more
partial-failure modes.
Remote parity (SSH + WSL)
Discovery runs inside
installRemoteManagedAgentHooksover the sameSFTP-shaped transport, after the static agents — so SSH (real
SFTPWrapper,after
ssh-relay-sessionresolves remote$HOME) and WSL (the fs-bridgeadapter, which has readdir/stat) inherit it with zero per-host wiring.
POSIX remotes only, matching the existing scope. The
REMOTE_MANAGED_HOOK_INSTALLER_AGENTSparity invariant intentionally keepscovering static agents only: discovered dirs are dynamic per-host instances
of the already-registered
claudeagent, exercised by their own remote andWSL tests. The remote flow is install-only today, so no remote ledger exists
to consume (documented in code).
Flavor label
The POSIX hook script and the Windows
.cmdvariant post aconfigDirformfield — the
CLAUDE_CONFIG_DIRpath string only, never tokens; empty fordefault installs (cmd batch context expands an undefined
%VAR%to empty).normalizeHookPayloadthreads it into the parsed status payload for claudeposts; it persists/hydrates like any other payload field. The compact
sidebar agent row renders
Claude · <flavor>(~/.claude-grok→ "grok",.claude.foo→ "foo") when an entry carries a non-default configDir.Payloads without the field behave exactly as today, so already-installed
scripts keep working unchanged (protocol stays v1 — the field is additive).
CLI compatibility (standalone
orcabinary)The ledger module rides in the CLI tsc build (
orca agent-hooks enable/disable/statusrun the real installers offline), and the packagedCLI is plain Node under
ELECTRON_RUN_AS_NODEwith noelectronmodule innode_modules. The module is therefore electron-free: it lazy-requireselectron for
app.getPath('userData')and falls back to the CLI's existingdefault-userData convention, now extracted to
src/shared/orca-user-data-path.tsand shared withsrc/cli/runtime/metadata.ts— so the app and the offline CLI resolve thesame ledger file. A guard test (
src/cli/cli-build-electron-free.test.ts)scans the whole
tsconfig.cli.jsoninclude set and fails on any staticelectron import.
Diagnosability
A per-dir install that ends in
state: 'error'emits one name-freeconsole.warn(local and remote flows alike) — dir names are user-privateobserved data and never appear in logs; detailed health remains visible via
the aggregated hook status.
Incidental refactor
The concurrency guard pushed two files over the 300-line lint cap (no
max-linesbumps allowed), so two mechanical extractions ride along:hooks-json-file.ts(hooks settings JSON types + read/write/retry,re-exported from
installer-utils.tsso call sites keep their importsurface) and
sftp-file-primitives.ts(the timeout-guarded ssh2 wrappers).No behavior change.
Known limitations
$HOMEdot-dir convention are missed: an absoluteCLAUDE_CONFIG_DIR=/opt/agents/claude-x, or a dir whose name doesn'tstart with
.claude-/.claude., is invisible to stat-only discovery.Deliberate trade-off — see the privacy posture above.
them (POSIX script bodies only), and discovered dirs follow that scope.
config dir to trust the hook command, exactly as it did for the first
.claudeinstall. Expected, one-time, per dir.flow exists today for any agent).
start or toggling
agentStatusHooksEnabled), not live.Tests
claude-config-dir-discovery.test.ts— fixture home with.claude,.claude-grok(marker),.claude.vertex(dot separator),.claudeX(noseparator),
.claude-empty(no marker), a regular file.claude-file,unrelated dot-dirs; asserts stat/readdir-only via the injected fs, probe
paths restricted to marker files of matching candidates, nothing logged,
fail-open on listing/probe errors; SFTP-shaped discovery incl. failure
path; label derivation (
~/.claude-grok→ "grok",.claude.foo→ "foo",Windows paths, null cases).
claude-config-dir-hook-controls.test.ts— multi-dir install writesmanaged entries into every discovered dir (same shared script); ledger
union survives discovery-set changes; uninstall cleans exactly the
ledgered dirs and leaves untracked dirs untouched; failed removals stay
ledgered; fully deleted dirs are pruned from install/status and never
recreated by remove; corrupt/tampered ledger filtered; count-only status
aggregation; name-free warn on per-dir install failure; no logging on the
success path.
hook-service.test.ts—remove()no-op contract: does not create amissing settings.json/config dir, leaves a user file with no managed
hooks byte-identical (no reformat, no
.bak), and still cleans managedentries while preserving user hooks after a real install.
cli-build-electron-free.test.ts— no static electron import anywhere inthe CLI build graph.
installer-utils.test.ts— concurrent settings.json mutation between readand write takes the retry path with no lost user keys; unparseable file
aborts without writing; final attempt converges under a pathological
writer.
installer-utils-remote.test.ts— one-shot backup written before firstmodification, never rotated, absent on first install.
remote-hook-service-installers.test.ts— aggregate SSH installer writesmanaged hooks into a discovered remote dir, skips marker-less matches;
parity invariant still green.
wsl-hook-relay-manager.test.ts— end-to-end WSL fs-bridge run installsinto a discovered guest config dir.
agent-hook-listener.test.ts/agent-status-types.test.ts—configDirround-trips into the status payload; absent/empty field unchanged
(back-compat); length-capped; hook script bodies carry the field and never
a token (asserted for POSIX and the win32
.cmd).Verification
tsc --noEmiton all three projects (node, cli, web): clean.oxlinton every touched file, plus type-aware switch-exhaustiveness,max-lines ratchet, reliability gates: clean.
Post-review-fix re-run of the affected files (11 files): 132 passed,
4 skipped, 0 failed.
tsc -p config/tsconfig.cli.jsonand ran
node out/cli/index.js agent hooks status/agent hooks offagainst a sandboxed
HOME+ORCA_USER_DATA_PATH— commands complete(previously the electron import crashed/broke them), and toggling off in a
pristine home no longer creates
~/.claude.