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