Skip to content

fix: honor CLAUDE_CONFIG_DIR for all Claude Code data (projects, sessions, settings, MCP config) - #1034

Open
zscgeek wants to merge 1 commit into
siteboon:mainfrom
zscgeek:fix/claude-config-dir-projects
Open

fix: honor CLAUDE_CONFIG_DIR for all Claude Code data (projects, sessions, settings, MCP config)#1034
zscgeek wants to merge 1 commit into
siteboon:mainfrom
zscgeek:fix/claude-config-dir-projects

Conversation

@zscgeek

@zscgeek zscgeek commented Jul 18, 2026

Copy link
Copy Markdown

Summary

CloudCLI hardcodes ~/.claude in every place it reads or writes Claude Code's own config/data directory, ignoring CLAUDE_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-default CLAUDE_CONFIG_DIR gets the wrong (or empty) projects/sessions list.

This affects, at minimum:

  • cloudcli status's reported projects folder
  • the sessions watcher that indexes Claude sessions on startup
  • reading/writing MCP server config (.claude.json)
  • reading settings.json (for API-key-based auth) and the credentials-file check in the Account tab
  • user-level slash commands (~/.claude/commands)
  • session cleanup and external-project clone targets in the agent routes
  • Claude skills discovery
  • the token-usage endpoint's fallback JSONL path resolution

Fix

Adds a single shared helper, getClaudeConfigDir() (server/shared/claude-config-dir.ts):

export function getClaudeConfigDir(): string {
  const override = process.env.CLAUDE_CONFIG_DIR;
  if (override && override.trim().length > 0) {
    return path.resolve(override.trim());
  }
  return path.join(os.homedir(), '.claude');
}

and updates every Claude-specific call site (11 files) to use it instead of hardcoding path.join(os.homedir(), '.claude', ...). Also surfaces CLAUDE_CONFIG_DIR in cloudcli status output for visibility.

Out of scope / untouched by design:

Testing

  • npm run typecheck — clean
  • npm run lint — no new errors (had to register the new shared file in the boundaries/elements eslint config, matching the existing pattern for claude-cli-path.ts)
  • npm run build — clean
  • Manually verified cloudcli status reports the correct projects folder path when CLAUDE_CONFIG_DIR is set vs unset
  • Ran the built server against a populated alt CLAUDE_CONFIG_DIR profile and confirmed the startup session sync indexed all of that profile's real sessions (processedByProvider: { claude: 450, ... }), rather than the default ~/.claude

Motivation

I run two separate Claude Code profiles (CLAUDE_CONFIG_DIR pointed at two different directories) for different projects/accounts, and CloudCLI always showed the default profile's projects no matter which CLAUDE_CONFIG_DIR was set for the server process.

… 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.
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Claude config directory support

Layer / File(s) Summary
Shared config directory resolver
server/shared/claude-config-dir.ts, eslint.config.js
Adds getClaudeConfigDir(), resolving CLAUDE_CONFIG_DIR or falling back to ~/.claude, and registers the helper as a shared backend utility.
Provider discovery paths
server/modules/providers/list/claude/*, server/modules/providers/services/sessions-watcher.service.ts
Claude providers and session watching use the resolved directory for settings, credentials, MCP servers, skills, history, and projects.
Server config consumers
server/claude-sdk.js, server/cli.js, server/index.js, server/utils/mcp-detector.js
MCP loading, CLI status, token-usage lookup, and MCP detection derive Claude paths from the shared directory.
Route filesystem paths
server/routes/agent.js, server/routes/commands.js
Agent cleanup, external-project cloning, command listing, and command authorization use the configured Claude directory.

Possibly related PRs

Suggested reviewers: blackmammoth

Poem

A bunny found a folder new,
Where Claude’s files may safely brew.
Config paths hop, old homes depart,
MCP and sessions play their part.
One helper guides them through and through.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main change: honoring CLAUDE_CONFIG_DIR across Claude Code data paths.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

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 win

Hardcoded path check prevents cleanup of custom directories.

The validation check explicitly requires the string .claude/external-projects to be present in the path. If a user defines a custom CLAUDE_CONFIG_DIR that does not include .claude in 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

📥 Commits

Reviewing files that changed from the base of the PR and between 27eaf01 and 72e26e0.

📒 Files selected for processing (13)
  • eslint.config.js
  • server/claude-sdk.js
  • server/cli.js
  • server/index.js
  • server/modules/providers/list/claude/claude-auth.provider.ts
  • server/modules/providers/list/claude/claude-mcp.provider.ts
  • server/modules/providers/list/claude/claude-session-synchronizer.provider.ts
  • server/modules/providers/list/claude/claude-skills.provider.ts
  • server/modules/providers/services/sessions-watcher.service.ts
  • server/routes/agent.js
  • server/routes/commands.js
  • server/shared/claude-config-dir.ts
  • server/utils/mcp-detector.js

Comment thread server/claude-sdk.js
Comment on lines 402 to 404
try {
const claudeConfigPath = path.join(os.homedir(), '.claude.json');
const claudeConfigPath = path.join(getClaudeConfigDir(), '.claude.json');

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 | 🔴 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 the os module in this file and explicitly fall back to path.join(os.homedir(), '.claude.json') when process.env.CLAUDE_CONFIG_DIR is not set.
  • server/utils/mcp-detector.js#L21-L25: Re-import the os module in this file and dynamically append path.join(os.homedir(), '.claude.json') to the configPaths array as a fallback when process.env.CLAUDE_CONFIG_DIR is 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.

@zscgeek
zscgeek force-pushed the fix/claude-config-dir-projects branch from a0b276e to 72e26e0 Compare July 18, 2026 13:29

@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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 72e26e0 and a0b276e.

📒 Files selected for processing (1)
  • server/modules/providers/list/claude/claude-auth.provider.ts

Comment on lines +103 to +118
// 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);
}

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 | 🔴 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.

Suggested change
// 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.

Comment on lines +173 to +192
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 };
}

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.

🩺 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: Refactor checkCliAuthStatus to use an asynchronous spawn wrapped 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: Refactor checkInstalled to use an asynchronous spawn and await it in getStatus. At a minimum, if kept synchronous, verify !result.error && result.status === 0 so the function does not incorrectly return true when 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.

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