Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export default tseslint.config(
"server/shared/utils.{js,ts}",
"server/shared/frontmatter.ts",
"server/shared/claude-cli-path.ts",
"server/shared/claude-config-dir.ts",
"server/shared/image-attachments.ts",
], // classify shared utility files so modules can depend on them explicitly
mode: "file",
Expand Down
4 changes: 2 additions & 2 deletions server/claude-sdk.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

import crypto from 'crypto';
import { promises as fs } from 'fs';
import os from 'os';
import path from 'path';

import { query } from '@anthropic-ai/claude-agent-sdk';
Expand All @@ -23,6 +22,7 @@ import { buildClaudeUserContent, normalizeImageDescriptors } from './shared/imag
import { CLAUDE_FALLBACK_MODELS } from './modules/providers/list/claude/claude-models.provider.js';
import { providerModelsService } from './modules/providers/services/provider-models.service.js';
import { resolveClaudeCodeExecutablePath } from './shared/claude-cli-path.js';
import { getClaudeConfigDir } from './shared/claude-config-dir.js';
import {
createNotificationEvent,
notifyRunFailed,
Expand Down Expand Up @@ -400,7 +400,7 @@ async function buildPromptPayload(command, images, cwd) {
*/
async function loadMcpConfig(cwd) {
try {
const claudeConfigPath = path.join(os.homedir(), '.claude.json');
const claudeConfigPath = path.join(getClaudeConfigDir(), '.claude.json');

Comment on lines 402 to 404

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.

// Check if config file exists
try {
Expand Down
4 changes: 3 additions & 1 deletion server/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import fs from 'fs';
import path from 'path';
import os from 'os';
import { findAppRoot, getModuleDir } from './utils/runtime-paths.js';
import { getClaudeConfigDir } from './shared/claude-config-dir.js';

const __dirname = getModuleDir(import.meta.url);
// The CLI is compiled into dist-server/server, but it still needs to read the top-level
Expand Down Expand Up @@ -120,9 +121,10 @@ function showStatus() {
console.log(` DATABASE_PATH: ${c.dim(process.env.DATABASE_PATH || '(using default location)')}`);
console.log(` CLAUDE_CLI_PATH: ${c.dim(process.env.CLAUDE_CLI_PATH || 'claude (default)')}`);
console.log(` CONTEXT_WINDOW: ${c.dim(process.env.CONTEXT_WINDOW || '160000 (default)')}`);
console.log(` CLAUDE_CONFIG_DIR: ${c.dim(process.env.CLAUDE_CONFIG_DIR || '(using default ~/.claude)')}`);

// Claude projects folder
const claudeProjectsPath = path.join(os.homedir(), '.claude', 'projects');
const claudeProjectsPath = path.join(getClaudeConfigDir(), 'projects');
const projectsExists = fs.existsSync(claudeProjectsPath);
console.log(`\n${c.info('[INFO]')} Claude Projects Folder:`);
console.log(` ${c.dim(claudeProjectsPath)}`);
Expand Down
6 changes: 4 additions & 2 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import mime from 'mime-types';
import Database from 'better-sqlite3';

import { AppError, WORKSPACES_ROOT, getOpenCodeDatabasePath, validateWorkspacePath } from '@/shared/utils.js';
import { getClaudeConfigDir } from '@/shared/claude-config-dir.js';
import { closeSessionsWatcher, initializeSessionsWatcher } from '@/modules/providers/index.js';
import { createWebSocketServer } from '@/modules/websocket/index.js';

Expand Down Expand Up @@ -1256,10 +1257,11 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
}

// Construct the JSONL file path
// Claude stores session files in ~/.claude/projects/[encoded-project-path]/[session-id].jsonl
// Claude stores session files in $CLAUDE_CONFIG_DIR/projects/[encoded-project-path]/[session-id].jsonl
// (or ~/.claude/projects/... when CLAUDE_CONFIG_DIR is unset)
// The encoding replaces any non-alphanumeric character (except -) with -
const encodedPath = projectPath.replace(/[^a-zA-Z0-9-]/g, '-');
const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
const projectDir = path.join(getClaudeConfigDir(), 'projects', encodedPath);

// Prefer the indexed transcript path (already produced by the trusted
// session synchronizer); fall back to the conventional location
Expand Down
6 changes: 3 additions & 3 deletions server/modules/providers/list/claude/claude-auth.provider.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { readFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';

import spawn from 'cross-spawn';

import { resolveClaudeCodeExecutablePath } from '@/shared/claude-cli-path.js';
import { getClaudeConfigDir } from '@/shared/claude-config-dir.js';
import type { IProviderAuth } from '@/shared/interfaces.js';
import type { ProviderAuthStatus } from '@/shared/types.js';
import { readObjectRecord, readOptionalString } from '@/shared/utils.js';
Expand Down Expand Up @@ -68,7 +68,7 @@ export class ClaudeProviderAuth implements IProviderAuth {
*/
private async loadSettingsEnv(): Promise<Record<string, unknown>> {
try {
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
const settingsPath = path.join(getClaudeConfigDir(), 'settings.json');
const content = await readFile(settingsPath, 'utf8');
const settings = readObjectRecord(JSON.parse(content));
return readObjectRecord(settings?.env) ?? {};
Expand Down Expand Up @@ -101,7 +101,7 @@ export class ClaudeProviderAuth implements IProviderAuth {
}

try {
const credPath = path.join(os.homedir(), '.claude', '.credentials.json');
const credPath = path.join(getClaudeConfigDir(), '.credentials.json');
const content = await readFile(credPath, 'utf8');
const creds = readObjectRecord(JSON.parse(content)) ?? {};
const oauth = readObjectRecord(creds.claudeAiOauth);
Expand Down
6 changes: 3 additions & 3 deletions server/modules/providers/list/claude/claude-mcp.provider.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os from 'node:os';
import path from 'node:path';

import { McpProvider } from '@/modules/providers/shared/mcp/mcp.provider.js';
import { getClaudeConfigDir } from '@/shared/claude-config-dir.js';
import type { McpScope, ProviderMcpServer, UpsertProviderMcpServerInput } from '@/shared/types.js';
import {
AppError,
Expand All @@ -25,7 +25,7 @@ export class ClaudeMcpProvider extends McpProvider {
return readObjectRecord(config.mcpServers) ?? {};
}

const filePath = path.join(os.homedir(), '.claude.json');
const filePath = path.join(getClaudeConfigDir(), '.claude.json');
const config = await readJsonConfig(filePath);
if (scope === 'user') {
return readObjectRecord(config.mcpServers) ?? {};
Expand All @@ -49,7 +49,7 @@ export class ClaudeMcpProvider extends McpProvider {
return;
}

const filePath = path.join(os.homedir(), '.claude.json');
const filePath = path.join(getClaudeConfigDir(), '.claude.json');
const config = await readJsonConfig(filePath);
if (scope === 'user') {
config.mcpServers = servers;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import os from 'node:os';
import path from 'node:path';
import { readFile } from 'node:fs/promises';

Expand All @@ -10,6 +9,7 @@ import {
normalizeSessionName,
readFileTimestamps,
} from '@/shared/utils.js';
import { getClaudeConfigDir } from '@/shared/claude-config-dir.js';
import type { IProviderSessionSynchronizer } from '@/shared/interfaces.js';

type ParsedSession = {
Expand All @@ -23,7 +23,7 @@ type ParsedSession = {
*/
export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
private readonly provider = 'claude' as const;
private readonly claudeHome = path.join(os.homedir(), '.claude');
private readonly claudeHome = getClaudeConfigDir();

/**
* Returns true when a JSONL file is a subagent transcript rather than a
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { readFile, readdir, stat } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';

import { SkillsProvider } from '@/modules/providers/shared/skills/skills.provider.js';
import { parseFrontMatter } from '@/shared/frontmatter.js';
import { getClaudeConfigDir } from '@/shared/claude-config-dir.js';
import type {
ProviderSkill,
ProviderSkillListOptions,
Expand All @@ -17,7 +17,7 @@ import {
readProviderSkillMarkdownDefinition,
} from '@/shared/utils.js';

const getClaudeHomePath = (): string => path.join(os.homedir(), '.claude');
const getClaudeHomePath = (): string => getClaudeConfigDir();

const getClaudePluginName = (pluginId: string): string | null => {
const normalizedPluginId = pluginId.trim();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import { sessionSynchronizerService } from '@/modules/providers/services/session
import { WS_OPEN_STATE, connectedClients } from '@/modules/websocket/index.js';
import type { LLMProvider } from '@/shared/types.js';
import { generateDisplayName } from '@/modules/projects/index.js';
import { getClaudeConfigDir } from '@/shared/claude-config-dir.js';

type WatcherEventType = 'add' | 'change';

const PROVIDER_WATCH_PATHS: Array<{ provider: LLMProvider; rootPath: string }> = [
{
provider: 'claude',
rootPath: path.join(os.homedir(), '.claude', 'projects'),
rootPath: path.join(getClaudeConfigDir(), 'projects'),
},
{
provider: 'cursor',
Expand Down
6 changes: 3 additions & 3 deletions server/routes/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import express from 'express';
// cross-spawn: drop-in spawn with Windows .cmd/PATHEXT resolution.
import spawn from 'cross-spawn';
import path from 'path';
import os from 'os';
import { promises as fs } from 'fs';
import crypto from 'crypto';
import { userDb, apiKeysDb, githubTokensDb, projectsDb } from '../modules/database/index.js';
Expand All @@ -14,6 +13,7 @@ import { Octokit } from '@octokit/rest';
import { providerModelsService } from '../modules/providers/services/provider-models.service.js';
import { IS_PLATFORM } from '../constants/config.js';
import { normalizeProjectPath } from '../shared/utils.js';
import { getClaudeConfigDir } from '../shared/claude-config-dir.js';

const router = express.Router();

Expand Down Expand Up @@ -434,7 +434,7 @@ async function cleanupProject(projectPath, sessionId = null) {
// Also clean up the Claude session directory if sessionId provided
if (sessionId) {
try {
const sessionPath = path.join(os.homedir(), '.claude', 'sessions', sessionId);
const sessionPath = path.join(getClaudeConfigDir(), 'sessions', sessionId);
console.log('🧹 Cleaning up session directory:', sessionPath);
await fs.rm(sessionPath, { recursive: true, force: true });
console.log('✅ Session directory cleaned up');
Expand Down Expand Up @@ -895,7 +895,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
} else {
// Generate a unique path for cloning
const repoHash = crypto.createHash('md5').update(githubUrl + Date.now()).digest('hex');
targetPath = path.join(os.homedir(), '.claude', 'external-projects', repoHash);
targetPath = path.join(getClaudeConfigDir(), 'external-projects', repoHash);
}

finalProjectPath = await cloneGitHubRepo(githubUrl.trim(), tokenToUse, targetPath);
Expand Down
9 changes: 4 additions & 5 deletions server/routes/commands.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { promises as fs } from "fs";
import os from "os";
import path from "path";

import express from "express";

import { providerModelsService } from "../modules/providers/services/provider-models.service.js";
import { parseFrontMatter } from "../shared/frontmatter.js";
import { findAppRoot, getModuleDir } from "../utils/runtime-paths.js";
import { getClaudeConfigDir } from "../shared/claude-config-dir.js";

const __dirname = getModuleDir(import.meta.url);
// This route reads the top-level package.json for the status command, so it needs the real
Expand Down Expand Up @@ -452,9 +452,8 @@ router.post("/list", async (req, res) => {
allCommands.push(...projectCommands);
}

// Scan user-level commands (~/.claude/commands/)
const homeDir = os.homedir();
const userCommandsDir = path.join(homeDir, ".claude", "commands");
// Scan user-level commands (~/.claude/commands/, or CLAUDE_CONFIG_DIR/commands/ if set)
const userCommandsDir = path.join(getClaudeConfigDir(), "commands");
const userCommands = await scanCommandsDirectory(
userCommandsDir,
userCommandsDir,
Expand Down Expand Up @@ -534,7 +533,7 @@ router.post("/execute", async (req, res) => {
{
const resolvedPath = path.resolve(commandPath);
const userBase = path.resolve(
path.join(os.homedir(), ".claude", "commands"),
path.join(getClaudeConfigDir(), "commands"),
);
const projectBase = context?.projectPath
? path.resolve(path.join(context.projectPath, ".claude", "commands"))
Expand Down
21 changes: 21 additions & 0 deletions server/shared/claude-config-dir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import os from 'node:os';
import path from 'node:path';

/**
* Resolves Claude Code's own config/data directory (the directory that
* holds `projects/`, `settings.json`, `.claude.json`, `.credentials.json`,
* `sessions/`, `commands/`, etc).
*
* Claude Code itself honors `CLAUDE_CONFIG_DIR` to relocate this directory
* away from the default `~/.claude` — commonly used to run multiple Claude
* accounts/profiles side by side. CloudCLI needs to resolve the same
* directory so it reads/writes the profile the user actually has active,
* instead of always falling back to `~/.claude`.
*/
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');
}
8 changes: 4 additions & 4 deletions server/utils/mcp-detector.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import { promises as fsPromises } from 'fs';
import path from 'path';
import os from 'os';
import { getClaudeConfigDir } from '../shared/claude-config-dir.js';

/**
* Check if task-master-ai MCP server is configured
Expand All @@ -18,10 +18,10 @@ import os from 'os';
export async function detectTaskMasterMCPServer() {
try {
// Read Claude configuration files directly (same logic as mcp.js)
const homeDir = os.homedir();
const claudeConfigDir = getClaudeConfigDir();
const configPaths = [
path.join(homeDir, '.claude.json'),
path.join(homeDir, '.claude', 'settings.json')
path.join(claudeConfigDir, '.claude.json'),
path.join(claudeConfigDir, 'settings.json')
];

let configData = null;
Expand Down