diff --git a/src/hooks/useOfficialMarketplaceNotification.tsx b/src/hooks/useOfficialMarketplaceNotification.tsx index 7784c23d..3d7b939f 100644 --- a/src/hooks/useOfficialMarketplaceNotification.tsx +++ b/src/hooks/useOfficialMarketplaceNotification.tsx @@ -19,7 +19,7 @@ async function _temp() { logForDebugging("Showing marketplace config save failure notification"); notifs.push({ key: "marketplace-config-save-failed", - jsx: Failed to save marketplace retry info · Check ~/.claude.json permissions, + jsx: Failed to save marketplace retry info · Check ~/.openclaude.json permissions, priority: "immediate", timeoutMs: 10000 }); diff --git a/src/migrations/resetAutoModeOptInForDefaultOffer.ts b/src/migrations/resetAutoModeOptInForDefaultOffer.ts index bc0c78a4..c79aba72 100644 --- a/src/migrations/resetAutoModeOptInForDefaultOffer.ts +++ b/src/migrations/resetAutoModeOptInForDefaultOffer.ts @@ -12,7 +12,7 @@ import { * One-shot migration: clear skipAutoPermissionPrompt for users who accepted * the old 2-option AutoModeOptInDialog but don't have auto as their default. * Re-surfaces the dialog so they see the new "make it my default mode" option. - * Guard lives in GlobalConfig (~/.claude.json), not settings.json, so it + * Guard lives in GlobalConfig (~/.openclaude.json), not settings.json, so it * survives settings resets and doesn't re-arm itself. * * Only runs when tengu_auto_mode_config.enabled === 'enabled'. For 'opt-in' diff --git a/src/screens/REPL.tsx b/src/screens/REPL.tsx index 90537dbb..d3e3f0f9 100644 --- a/src/screens/REPL.tsx +++ b/src/screens/REPL.tsx @@ -3873,7 +3873,7 @@ export function REPL({ // empty to non-empty, not on every length change -- otherwise a render loop // (concurrent onQuery thrashing, etc.) spams saveGlobalConfig, which hits // ELOCKED under concurrent sessions and falls back to unlocked writes. - // That write storm is the primary trigger for ~/.claude.json corruption + // That write storm is the primary trigger for ~/.openclaude.json corruption // (GH #3117). const hasCountedQueueUseRef = useRef(false); useEffect(() => { diff --git a/src/services/analytics/growthbook.ts b/src/services/analytics/growthbook.ts index 458df719..d9eba44e 100644 --- a/src/services/analytics/growthbook.ts +++ b/src/services/analytics/growthbook.ts @@ -334,7 +334,7 @@ async function processRemoteEvalPayload( // Empty object is truthy — without the length check, `{features: {}}` // (transient server bug, truncated response) would pass, clear the maps // below, return true, and syncRemoteEvalToDisk would wholesale-write `{}` - // to disk: total flag blackout for every process sharing ~/.claude.json. + // to disk: total flag blackout for every process sharing ~/.openclaude.json. if (!payload?.features || Object.keys(payload.features).length === 0) { return false } diff --git a/src/tools/ConfigTool/prompt.ts b/src/tools/ConfigTool/prompt.ts index 2441b956..361f93c9 100644 --- a/src/tools/ConfigTool/prompt.ts +++ b/src/tools/ConfigTool/prompt.ts @@ -59,7 +59,7 @@ export function generatePrompt(): string { ## Configurable settings list The following settings are available for you to change: -### Global Settings (stored in ~/.claude.json) +### Global Settings (stored in ~/.openclaude.json) ${globalSettings.join('\n')} ### Project Settings (stored in settings.json) diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 713e4195..b4a67f24 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -693,7 +693,7 @@ export function refreshAwsAuth(awsAuthRefresh: string): Promise { 'AWS auth refresh timed out after 3 minutes. Run your auth command manually in a separate terminal.', ) : chalk.red( - 'Error running awsAuthRefresh (in settings or ~/.claude.json):', + 'Error running awsAuthRefresh (in settings or ~/.openclaude.json):', ) // biome-ignore lint/suspicious/noConsole:: intentional console output console.error(message) @@ -771,7 +771,7 @@ async function getAwsCredsFromCredentialExport(): Promise<{ } } catch (e) { const message = chalk.red( - 'Error getting AWS credentials from awsCredentialExport (in settings or ~/.claude.json):', + 'Error getting AWS credentials from awsCredentialExport (in settings or ~/.openclaude.json):', ) if (e instanceof Error) { // biome-ignore lint/suspicious/noConsole:: intentional console output @@ -961,7 +961,7 @@ export function refreshGcpAuth(gcpAuthRefresh: string): Promise { 'GCP auth refresh timed out after 3 minutes. Run your auth command manually in a separate terminal.', ) : chalk.red( - 'Error running gcpAuthRefresh (in settings or ~/.claude.json):', + 'Error running gcpAuthRefresh (in settings or ~/.openclaude.json):', ) // biome-ignore lint/suspicious/noConsole:: intentional console output console.error(message) @@ -1959,7 +1959,7 @@ export async function validateForceLoginOrg(): Promise { // Always fetch the authoritative org UUID from the profile endpoint. // Even keychain-sourced tokens verify server-side: the cached org UUID - // in ~/.claude.json is user-writable and cannot be trusted. + // in ~/.openclaude.json is user-writable and cannot be trusted. const { source } = getAuthTokenSource() const isEnvVarToken = source === 'CLAUDE_CODE_OAUTH_TOKEN' || diff --git a/src/utils/caCertsConfig.ts b/src/utils/caCertsConfig.ts index 7bcaef3f..456e4121 100644 --- a/src/utils/caCertsConfig.ts +++ b/src/utils/caCertsConfig.ts @@ -28,7 +28,7 @@ import { getSettingsForSource } from './settings/settings.js' * is lazy-initialized) and ensure Node.js compatibility. * * This is safe to call before the trust dialog because we only read from - * user-controlled files (~/.claude/settings.json and ~/.claude.json), + * user-controlled files (~/.claude/settings.json and ~/.openclaude.json), * not from project-level settings. */ export function applyExtraCACertsFromConfig(): void { @@ -52,7 +52,7 @@ export function applyExtraCACertsFromConfig(): void { * after the trust dialog. But we need the CA cert early to establish the TLS * connection to an HTTPS proxy during init(). * - * We read from global config (~/.claude.json) and user settings + * We read from global config (~/.openclaude.json) and user settings * (~/.claude/settings.json). These are user-controlled files that don't * require trust approval. */ diff --git a/src/utils/claudeInChrome/setup.ts b/src/utils/claudeInChrome/setup.ts index 4f251b5c..a78c065e 100644 --- a/src/utils/claudeInChrome/setup.ts +++ b/src/utils/claudeInChrome/setup.ts @@ -355,7 +355,7 @@ exec ${command} * * Only positive detections are persisted. A negative result from the * filesystem scan is not cached, because it may come from a machine that - * shares ~/.claude.json but has no local Chrome (e.g. a remote dev + * shares ~/.openclaude.json but has no local Chrome (e.g. a remote dev * environment using the bridge), and caching it would permanently poison * auto-enable for every session on every machine that reads that config. */ diff --git a/src/utils/config.ts b/src/utils/config.ts index 610e9820..1c999625 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -918,7 +918,7 @@ let configCacheHits = 0 let configCacheMisses = 0 // Session-total count of actual disk writes to the global config file. // Exposed for internal-only dev diagnostics (see inc-4552) so anomalous write -// rates surface in the UI before they corrupt ~/.claude.json. +// rates surface in the UI before they corrupt ~/.openclaude.json. let globalConfigWriteCount = 0 export function getGlobalConfigWriteCount(): number { @@ -1257,7 +1257,7 @@ function saveConfigWithLock( const currentConfig = getConfig(file, createDefault) if (file === getGlobalClaudeFile() && wouldLoseAuthState(currentConfig)) { logForDebugging( - 'saveConfigWithLock: re-read config is missing auth that cache has; refusing to write to avoid wiping ~/.claude.json. See GH #3117.', + 'saveConfigWithLock: re-read config is missing auth that cache has; refusing to write to avoid wiping ~/.openclaude.json. See GH #3117.', { level: 'error' }, ) logEvent('tengu_config_auth_loss_prevented', {}) diff --git a/src/utils/deepLink/registerProtocol.ts b/src/utils/deepLink/registerProtocol.ts index 0e630ee6..88eb8abd 100644 --- a/src/utils/deepLink/registerProtocol.ts +++ b/src/utils/deepLink/registerProtocol.ts @@ -253,7 +253,7 @@ async function resolveClaudePath(): Promise { * Check whether the OS-level protocol handler is already registered AND * points at the expected `claude` binary. Reads the registration artifact * directly (symlink target, .desktop Exec line, registry value) rather than - * a cached flag in ~/.claude.json, so: + * a cached flag in ~/.openclaude.json, so: * - the check is per-machine (config can sync across machines; OS state can't) * - stale paths self-heal (install-method change → re-register next session) * - deleted artifacts self-heal @@ -311,7 +311,7 @@ export async function ensureDeepLinkProtocolRegistered(): Promise { // EACCES/ENOSPC are deterministic — retrying next session won't help. // Throttle to once per 24h so a read-only ~/.local/share/applications // doesn't generate a failure event on every startup. Marker lives in - // ~/.claude (per-machine, not synced) rather than ~/.claude.json (can sync). + // ~/.claude (per-machine, not synced) rather than ~/.openclaude.json (can sync). const failureMarkerPath = path.join( getClaudeConfigHomeDir(), '.deep-link-register-failed', diff --git a/src/utils/env.test.ts b/src/utils/env.test.ts new file mode 100644 index 00000000..ba052371 --- /dev/null +++ b/src/utils/env.test.ts @@ -0,0 +1,62 @@ +import { afterEach, beforeEach, expect, test } from 'bun:test' +import { mkdtempSync, rmSync, writeFileSync } from 'fs' +import { tmpdir } from 'os' +import { join } from 'path' + +const originalEnv = { + CLAUDE_CONFIG_DIR: process.env.CLAUDE_CONFIG_DIR, + CLAUDE_CODE_CUSTOM_OAUTH_URL: process.env.CLAUDE_CODE_CUSTOM_OAUTH_URL, + USER_TYPE: process.env.USER_TYPE, +} + +let tempDir: string + +beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'openclaude-env-test-')) + process.env.CLAUDE_CONFIG_DIR = tempDir + delete process.env.CLAUDE_CODE_CUSTOM_OAUTH_URL + delete process.env.USER_TYPE +}) + +afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }) + if (originalEnv.CLAUDE_CONFIG_DIR === undefined) { + delete process.env.CLAUDE_CONFIG_DIR + } else { + process.env.CLAUDE_CONFIG_DIR = originalEnv.CLAUDE_CONFIG_DIR + } + if (originalEnv.CLAUDE_CODE_CUSTOM_OAUTH_URL === undefined) { + delete process.env.CLAUDE_CODE_CUSTOM_OAUTH_URL + } else { + process.env.CLAUDE_CODE_CUSTOM_OAUTH_URL = originalEnv.CLAUDE_CODE_CUSTOM_OAUTH_URL + } + if (originalEnv.USER_TYPE === undefined) { + delete process.env.USER_TYPE + } else { + process.env.USER_TYPE = originalEnv.USER_TYPE + } +}) + +async function importFreshEnvModule() { + return import(`./env.js?ts=${Date.now()}-${Math.random()}`) +} + +// getGlobalClaudeFile — three migration branches + +test('getGlobalClaudeFile: new install returns .openclaude.json when neither file exists', async () => { + const { getGlobalClaudeFile } = await importFreshEnvModule() + expect(getGlobalClaudeFile()).toBe(join(tempDir, '.openclaude.json')) +}) + +test('getGlobalClaudeFile: existing user keeps .claude.json when only legacy file exists', async () => { + writeFileSync(join(tempDir, '.claude.json'), '{}') + const { getGlobalClaudeFile } = await importFreshEnvModule() + expect(getGlobalClaudeFile()).toBe(join(tempDir, '.claude.json')) +}) + +test('getGlobalClaudeFile: migrated user uses .openclaude.json when both files exist', async () => { + writeFileSync(join(tempDir, '.claude.json'), '{}') + writeFileSync(join(tempDir, '.openclaude.json'), '{}') + const { getGlobalClaudeFile } = await importFreshEnvModule() + expect(getGlobalClaudeFile()).toBe(join(tempDir, '.openclaude.json')) +}) diff --git a/src/utils/env.ts b/src/utils/env.ts index 0dfdc803..44c95c28 100644 --- a/src/utils/env.ts +++ b/src/utils/env.ts @@ -21,8 +21,21 @@ export const getGlobalClaudeFile = memoize((): string => { return join(getClaudeConfigHomeDir(), '.config.json') } - const filename = `.claude${fileSuffixForOauthConfig()}.json` - return join(process.env.CLAUDE_CONFIG_DIR || homedir(), filename) + const oauthSuffix = fileSuffixForOauthConfig() + const configDir = process.env.CLAUDE_CONFIG_DIR || homedir() + + // Default to .openclaude.json. Fall back to .claude.json only if the new + // file doesn't exist yet and the legacy one does (same migration pattern + // as resolveClaudeConfigHomeDir for the config directory). + const newFilename = `.openclaude${oauthSuffix}.json` + const legacyFilename = `.claude${oauthSuffix}.json` + if ( + !getFsImplementation().existsSync(join(configDir, newFilename)) && + getFsImplementation().existsSync(join(configDir, legacyFilename)) + ) { + return join(configDir, legacyFilename) + } + return join(configDir, newFilename) }) const hasInternetAccess = memoize(async (): Promise => { diff --git a/src/utils/json.ts b/src/utils/json.ts index 2cfc67b7..3515dd9e 100644 --- a/src/utils/json.ts +++ b/src/utils/json.ts @@ -24,7 +24,7 @@ type CachedParse = { ok: true; value: unknown } | { ok: false } // lodash memoize default resolver = first arg only). // Skip caching above this size — the LRU stores the full string as the key, // so a 200KB config file would pin ~10MB in #keyList across 50 slots. Large -// inputs like ~/.claude.json also change between reads (numStartups bumps on +// inputs like ~/.openclaude.json also change between reads (numStartups bumps on // every CC startup), so the cache never hits anyway. const PARSE_CACHE_MAX_KEY_BYTES = 8 * 1024 diff --git a/src/utils/managedEnv.ts b/src/utils/managedEnv.ts index 0ed32a3d..471723e8 100644 --- a/src/utils/managedEnv.ts +++ b/src/utils/managedEnv.ts @@ -131,7 +131,7 @@ export function applySafeConfigEnvironmentVariables(): void { : null } - // Global config (~/.claude.json) is user-controlled. In CCD mode, + // Global config (~/.openclaude.json) is user-controlled. In CCD mode, // filterSettingsEnv strips keys that were in the spawn env snapshot so // the desktop host's operational vars (OTEL, etc.) are not overridden. Object.assign(process.env, filterSettingsEnv(getGlobalConfig().env)) diff --git a/src/utils/permissions/filesystem.ts b/src/utils/permissions/filesystem.ts index db9d8ef0..392c0433 100644 --- a/src/utils/permissions/filesystem.ts +++ b/src/utils/permissions/filesystem.ts @@ -64,6 +64,7 @@ export const DANGEROUS_FILES = [ '.profile', '.ripgreprc', '.mcp.json', + '.openclaude.json', '.claude.json', ] as const