fix: honor CLAUDE_CONFIG_DIR for all Claude Code data (projects, sessions, settings, MCP config) - #1034
fix: honor CLAUDE_CONFIG_DIR for all Claude Code data (projects, sessions, settings, MCP config)#1034zscgeek wants to merge 1 commit into
Conversation
… is read CloudCLI hardcoded ~/.claude across every read/write of Claude Code's own config and data directory (projects, sessions, settings.json, .claude.json, .credentials.json, commands, external-projects, MCP config, skills, and the sessions watcher's root path). Claude Code CLI itself honors CLAUDE_CONFIG_DIR to relocate this directory (e.g. for running multiple accounts/profiles), but CloudCLI silently ignored it and always fell back to the default location. Adds a single shared helper, getClaudeConfigDir(), that resolves CLAUDE_CONFIG_DIR when set (falling back to ~/.claude otherwise), and updates every Claude-specific call site to use it. Also surfaces CLAUDE_CONFIG_DIR in `cloudcli status` output for visibility. Cursor/Codex/OpenCode paths and CloudCLI's own app-level state (~/.cloudcli, ~/.claude-code-ui) are untouched - this only affects paths that are genuinely Claude Code's own. Verified: `cloudcli status` now reports the correct projects folder under CLAUDE_CONFIG_DIR, and a live server run against a populated alt profile correctly indexed all of its sessions instead of the default ~/.claude.
📝 WalkthroughWalkthroughChangesClaude config directory support
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
server/routes/agent.js (1)
424-428: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winHardcoded path check prevents cleanup of custom directories.
The validation check explicitly requires the string
.claude/external-projectsto be present in the path. If a user defines a customCLAUDE_CONFIG_DIRthat does not include.claudein its path, this check will return early and refuse to clean up the temporary cloned project directories.🛠️ Proposed fix
- // Only clean up projects in the external-projects directory - if (!projectPath.includes('.claude/external-projects')) { + // Only clean up projects in the external-projects directory + const externalProjectsDir = path.join(getClaudeConfigDir(), 'external-projects').replace(/\\/g, '/'); + if (!projectPath.replace(/\\/g, '/').includes(externalProjectsDir) && !projectPath.includes('.claude/external-projects')) { console.warn('⚠️ Refusing to clean up non-external project:', projectPath); return; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/routes/agent.js` around lines 424 - 428, Update the project cleanup validation around the external-project path check to derive the allowed directory from the configured CLAUDE_CONFIG_DIR rather than requiring the literal `.claude/external-projects` segment. Preserve the safety check by allowing cleanup only when projectPath is inside the configured external-projects directory, including custom configuration paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@server/claude-sdk.js`:
- Around line 402-404: Restore the default .claude.json lookup when
CLAUDE_CONFIG_DIR is unset: in server/claude-sdk.js lines 402-404, re-import os
and use os.homedir() for the fallback path; in server/utils/mcp-detector.js
lines 21-25, re-import os and dynamically append the same home-directory path to
configPaths when the environment variable is absent, while retaining the
configured directory behavior.
---
Outside diff comments:
In `@server/routes/agent.js`:
- Around line 424-428: Update the project cleanup validation around the
external-project path check to derive the allowed directory from the configured
CLAUDE_CONFIG_DIR rather than requiring the literal `.claude/external-projects`
segment. Preserve the safety check by allowing cleanup only when projectPath is
inside the configured external-projects directory, including custom
configuration paths.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: e5043cf5-6f85-41ac-b051-a3fcf2065066
📒 Files selected for processing (13)
eslint.config.jsserver/claude-sdk.jsserver/cli.jsserver/index.jsserver/modules/providers/list/claude/claude-auth.provider.tsserver/modules/providers/list/claude/claude-mcp.provider.tsserver/modules/providers/list/claude/claude-session-synchronizer.provider.tsserver/modules/providers/list/claude/claude-skills.provider.tsserver/modules/providers/services/sessions-watcher.service.tsserver/routes/agent.jsserver/routes/commands.jsserver/shared/claude-config-dir.tsserver/utils/mcp-detector.js
| try { | ||
| const claudeConfigPath = path.join(os.homedir(), '.claude.json'); | ||
| const claudeConfigPath = path.join(getClaudeConfigDir(), '.claude.json'); | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
Broken default configuration path for .claude.json.
By directly replacing os.homedir() with getClaudeConfigDir(), the path resolved for the .claude.json configuration file changes from ~/.claude.json to ~/.claude/.claude.json when CLAUDE_CONFIG_DIR is unset. This will silently fail to find existing MCP configurations for users who have not explicitly set the environment variable. You must retain ~/.claude.json as a fallback path.
server/claude-sdk.js#L402-L404: Re-import theosmodule in this file and explicitly fall back topath.join(os.homedir(), '.claude.json')whenprocess.env.CLAUDE_CONFIG_DIRis not set.server/utils/mcp-detector.js#L21-L25: Re-import theosmodule in this file and dynamically appendpath.join(os.homedir(), '.claude.json')to theconfigPathsarray as a fallback whenprocess.env.CLAUDE_CONFIG_DIRis not set.
📍 Affects 2 files
server/claude-sdk.js#L402-L404(this comment)server/utils/mcp-detector.js#L21-L25
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@server/claude-sdk.js` around lines 402 - 404, Restore the default
.claude.json lookup when CLAUDE_CONFIG_DIR is unset: in server/claude-sdk.js
lines 402-404, re-import os and use os.homedir() for the fallback path; in
server/utils/mcp-detector.js lines 21-25, re-import os and dynamically append
the same home-directory path to configPaths when the environment variable is
absent, while retaining the configured directory behavior.
a0b276e to
72e26e0
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@server/modules/providers/list/claude/claude-auth.provider.ts`:
- Around line 173-192: Refactor checkCliAuthStatus at
server/modules/providers/list/claude/claude-auth.provider.ts:173-192 to use
asynchronous spawn wrapped in a Promise, preserving the timeout and JSON output
handling while returning the intended “Unable to check...” error when the child
process emits an execution error. Refactor checkInstalled at
server/modules/providers/list/claude/claude-auth.provider.ts:27-41 to use
asynchronous spawn and await it from getStatus, or at minimum require no
result.error and a zero exit status before reporting the CLI as installed.
- Around line 103-118: Update the credential resolution flow around
readCredentialsFile and checkCliAuthStatus so the CLI auth status is checked
first as the primary source. Only invoke readCredentialsFile as a fallback when
checkCliAuthStatus fails to authenticate, preserving the existing
missingCredentialsError handling and return behavior for both sources.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 8b6b27e4-534c-491f-b18c-b58a89b635aa
📒 Files selected for processing (1)
server/modules/providers/list/claude/claude-auth.provider.ts
| // Fall back to a plaintext credentials file if one exists (older Claude | ||
| // Code versions, or platforms where OAuth creds aren't OS-keychain backed). | ||
| const fileCredentials = await this.readCredentialsFile(); | ||
| if (fileCredentials) { | ||
| return fileCredentials; | ||
| } | ||
|
|
||
| // Claude Code stores OAuth credentials in a macOS Keychain entry whose | ||
| // service name is derived from CLAUDE_CONFIG_DIR (a different, hashed | ||
| // service name per profile) rather than a file we can read directly. | ||
| // That derivation isn't a stable contract we should reimplement, so defer | ||
| // to the CLI's own `claude auth status`, which already resolves it | ||
| // correctly (file, keychain, or otherwise) for whichever CLAUDE_CONFIG_DIR | ||
| // is active in this process's environment. | ||
| return this.checkCliAuthStatus(missingCredentialsError); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
Incorrect precedence: the fallback file check overrides the primary CLI auth check.
The PR objective and inline comments indicate that the plaintext .credentials.json file should act as a fallback. However, readCredentialsFile() is executed first. If a user has a stale .credentials.json file on macOS (e.g., from an older CLI version), the code will see the expired token and return an error without ever executing checkCliAuthStatus() to check the valid Keychain entry.
Swap the order to check the CLI status first, and only fall back to the file if the CLI check fails to authenticate.
🐛 Proposed fix
- // Fall back to a plaintext credentials file if one exists (older Claude
- // Code versions, or platforms where OAuth creds aren't OS-keychain backed).
- const fileCredentials = await this.readCredentialsFile();
- if (fileCredentials) {
- return fileCredentials;
- }
-
- // Claude Code stores OAuth credentials in a macOS Keychain entry whose
- // service name is derived from CLAUDE_CONFIG_DIR (a different, hashed
- // service name per profile) rather than a file we can read directly.
- // That derivation isn't a stable contract we should reimplement, so defer
- // to the CLI's own `claude auth status`, which already resolves it
- // correctly (file, keychain, or otherwise) for whichever CLAUDE_CONFIG_DIR
- // is active in this process's environment.
- return this.checkCliAuthStatus(missingCredentialsError);
+ // Claude Code stores OAuth credentials in a macOS Keychain entry whose
+ // service name is derived from CLAUDE_CONFIG_DIR (a different, hashed
+ // service name per profile) rather than a file we can read directly.
+ // That derivation isn't a stable contract we should reimplement, so defer
+ // to the CLI's own `claude auth status`, which already resolves it
+ // correctly (file, keychain, or otherwise) for whichever CLAUDE_CONFIG_DIR
+ // is active in this process's environment.
+ const cliStatus = await this.checkCliAuthStatus(missingCredentialsError);
+ if (cliStatus.authenticated) {
+ return cliStatus;
+ }
+
+ // Fall back to a plaintext credentials file if one exists (older Claude
+ // Code versions, or platforms where OAuth creds aren't OS-keychain backed).
+ const fileCredentials = await this.readCredentialsFile();
+ if (fileCredentials) {
+ return fileCredentials;
+ }
+
+ return cliStatus;📝 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.
| // Fall back to a plaintext credentials file if one exists (older Claude | |
| // Code versions, or platforms where OAuth creds aren't OS-keychain backed). | |
| const fileCredentials = await this.readCredentialsFile(); | |
| if (fileCredentials) { | |
| return fileCredentials; | |
| } | |
| // Claude Code stores OAuth credentials in a macOS Keychain entry whose | |
| // service name is derived from CLAUDE_CONFIG_DIR (a different, hashed | |
| // service name per profile) rather than a file we can read directly. | |
| // That derivation isn't a stable contract we should reimplement, so defer | |
| // to the CLI's own `claude auth status`, which already resolves it | |
| // correctly (file, keychain, or otherwise) for whichever CLAUDE_CONFIG_DIR | |
| // is active in this process's environment. | |
| return this.checkCliAuthStatus(missingCredentialsError); | |
| } | |
| // Claude Code stores OAuth credentials in a macOS Keychain entry whose | |
| // service name is derived from CLAUDE_CONFIG_DIR (a different, hashed | |
| // service name per profile) rather than a file we can read directly. | |
| // That derivation isn't a stable contract we should reimplement, so defer | |
| // to the CLI's own `claude auth status`, which already resolves it | |
| // correctly (file, keychain, or otherwise) for whichever CLAUDE_CONFIG_DIR | |
| // is active in this process's environment. | |
| const cliStatus = await this.checkCliAuthStatus(missingCredentialsError); | |
| if (cliStatus.authenticated) { | |
| return cliStatus; | |
| } | |
| // Fall back to a plaintext credentials file if one exists (older Claude | |
| // Code versions, or platforms where OAuth creds aren't OS-keychain backed). | |
| const fileCredentials = await this.readCredentialsFile(); | |
| if (fileCredentials) { | |
| return fileCredentials; | |
| } | |
| return cliStatus; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@server/modules/providers/list/claude/claude-auth.provider.ts` around lines
103 - 118, Update the credential resolution flow around readCredentialsFile and
checkCliAuthStatus so the CLI auth status is checked first as the primary
source. Only invoke readCredentialsFile as a fallback when checkCliAuthStatus
fails to authenticate, preserving the existing missingCredentialsError handling
and return behavior for both sources.
| let result; | ||
| try { | ||
| result = spawn.sync(cliPath, ['auth', 'status', '--json'], { | ||
| timeout: 10000, | ||
| encoding: 'utf8', | ||
| env: process.env, | ||
| }); | ||
| } catch { | ||
| return { | ||
| authenticated: false, | ||
| email: null, | ||
| method: null, | ||
| error: 'Unable to check Claude authentication status. Run claude /login again.', | ||
| }; | ||
| } | ||
|
|
||
| const stdout = typeof result.stdout === 'string' ? result.stdout.trim() : ''; | ||
| if (!stdout) { | ||
| return { authenticated: false, email: null, method: null, error: missingCredentialsError }; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift
Avoid blocking the event loop and handle spawn.sync execution errors.
Both uses of spawn.sync in this file block the Node.js event loop and fail to check the returned error property. This causes execution failures (like ENOENT when the CLI is missing) to be silently swallowed rather than caught by the catch blocks.
server/modules/providers/list/claude/claude-auth.provider.ts#L173-L192: RefactorcheckCliAuthStatusto use an asynchronousspawnwrapped in a Promise. The current synchronous call blocks all request threads for up to 10 seconds. Additionally, handle child process errors directly so that execution failures return the intended "Unable to check..." error rather than falling through to a "missing credentials" state.server/modules/providers/list/claude/claude-auth.provider.ts#L27-L41: RefactorcheckInstalledto use an asynchronousspawnandawaitit ingetStatus. At a minimum, if kept synchronous, verify!result.error && result.status === 0so the function does not incorrectly returntruewhen the CLI executable cannot be launched.
📍 Affects 1 file
server/modules/providers/list/claude/claude-auth.provider.ts#L173-L192(this comment)server/modules/providers/list/claude/claude-auth.provider.ts#L27-L41
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@server/modules/providers/list/claude/claude-auth.provider.ts` around lines
173 - 192, Refactor checkCliAuthStatus at
server/modules/providers/list/claude/claude-auth.provider.ts:173-192 to use
asynchronous spawn wrapped in a Promise, preserving the timeout and JSON output
handling while returning the intended “Unable to check...” error when the child
process emits an execution error. Refactor checkInstalled at
server/modules/providers/list/claude/claude-auth.provider.ts:27-41 to use
asynchronous spawn and await it from getStatus, or at minimum require no
result.error and a zero exit status before reporting the CLI as installed.
Summary
CloudCLI hardcodes
~/.claudein every place it reads or writes Claude Code's own config/data directory, ignoringCLAUDE_CONFIG_DIR. Claude Code CLI itself honors this env var to relocate its config dir (commonly used to run multiple accounts/profiles side by side), but CloudCLI silently falls back to the default location regardless, so anyone running CloudCLI against a non-defaultCLAUDE_CONFIG_DIRgets the wrong (or empty) projects/sessions list.This affects, at minimum:
cloudcli status's reported projects folder.claude.json)settings.json(for API-key-based auth) and the credentials-file check in the Account tab~/.claude/commands)Fix
Adds a single shared helper,
getClaudeConfigDir()(server/shared/claude-config-dir.ts):and updates every Claude-specific call site (11 files) to use it instead of hardcoding
path.join(os.homedir(), '.claude', ...). Also surfacesCLAUDE_CONFIG_DIRincloudcli statusoutput for visibility.Out of scope / untouched by design:
~/.cloudcli,~/.claude-code-ui) — unrelated to which Claude profile is activeclaude auth statusinstead of a credentials file #1035Testing
npm run typecheck— cleannpm run lint— no new errors (had to register the new shared file in theboundaries/elementseslint config, matching the existing pattern forclaude-cli-path.ts)npm run build— cleancloudcli statusreports the correct projects folder path whenCLAUDE_CONFIG_DIRis set vs unsetCLAUDE_CONFIG_DIRprofile and confirmed the startup session sync indexed all of that profile's real sessions (processedByProvider: { claude: 450, ... }), rather than the default~/.claudeMotivation
I run two separate Claude Code profiles (
CLAUDE_CONFIG_DIRpointed at two different directories) for different projects/accounts, and CloudCLI always showed the default profile's projects no matter whichCLAUDE_CONFIG_DIRwas set for the server process.