fix(settings): correct stale settings path references (#1666)

* fix(settings): correct OpenClaude settings paths

* fix(settings): address review path clarity

* fix(sandbox): protect OpenClaude settings in changed cwd
This commit is contained in:
Bogdan
2026-06-17 11:44:03 +08:00
committed by GitHub
parent 29aea4969d
commit 544b857876
30 changed files with 664 additions and 67 deletions
+19
View File
@@ -0,0 +1,19 @@
import { describe, expect, test } from 'bun:test'
import autoFixCommand from './auto-fix.js'
describe('/auto-fix command prompt', () => {
test('points project and local settings at canonical .openclaude paths', async () => {
expect(autoFixCommand.type).toBe('prompt')
if (autoFixCommand.type !== 'prompt') {
throw new Error('/auto-fix must be a prompt command')
}
const blocks = await autoFixCommand.getPromptForCommand('', {} as never)
const text = blocks?.map(block => ('text' in block ? block.text : '')).join('\n')
expect(text).toContain('.openclaude/settings.json')
expect(text).toContain('.openclaude/settings.local.json')
expect(text).not.toContain('.claude/settings.json')
expect(text).not.toContain('.claude/settings.local.json')
})
})
+2 -1
View File
@@ -1,4 +1,5 @@
import type { Command } from '../types/command.js'
import { getRelativeSettingsFilePathForSource } from '../utils/settings/settings.js'
const command: Command = {
name: 'auto-fix',
@@ -14,7 +15,7 @@ const command: Command = {
type: 'text',
text:
'The user wants to configure auto-fix settings. Auto-fix automatically runs lint and test commands after AI file edits, feeding errors back for self-repair.\n\n' +
'Current settings location: `.claude/settings.json` or `.claude/settings.local.json`\n\n' +
`Current settings location: \`${getRelativeSettingsFilePathForSource('projectSettings')}\` or \`${getRelativeSettingsFilePathForSource('localSettings')}\`\n\n` +
'Example configuration:\n```json\n{\n "autoFix": {\n "enabled": true,\n "lint": "eslint . --fix",\n "test": "bun test",\n "maxRetries": 3,\n "timeout": 30000\n }\n}\n```\n\n' +
'Ask the user what lint and test commands they use, then help them set up the configuration.',
},
+2 -2
View File
@@ -196,7 +196,7 @@ Check the environment and ask about each gap you find (use AskUserQuestion):
For each hook preference (from the queue or the formatter fallback):
1. Target file: default based on the Phase 1 instruction-file choice — project → \`.claude/settings.json\` (team-shared, committed); personal → \`.claude/settings.local.json\`. Only ask if the user chose "both" in Phase 1 or the preference is ambiguous. Ask once for all hooks, not per-hook.
1. Target file: default based on the Phase 1 instruction-file choice — project → \`.openclaude/settings.json\` (team-shared, committed); personal → \`.openclaude/settings.local.json\`. Only ask if the user chose "both" in Phase 1 or the preference is ambiguous. Ask once for all hooks, not per-hook.
2. Pick the event and matcher from the preference:
- "after every edit" → \`PostToolUse\` with matcher \`Write|Edit\`
@@ -205,7 +205,7 @@ Check the environment and ask about each gap you find (use AskUserQuestion):
- "before committing" (literal git-commit gate) → **not a hooks.json hook.** Matchers can't filter Bash by command content, so there's no way to target only \`git commit\`. Route this to a git pre-commit hook (\`.git/hooks/pre-commit\`, husky, pre-commit framework) instead — offer to write one. If the user actually means "before I review and commit Claude's output", that's \`Stop\` — probe to disambiguate.
Probe if the preference is ambiguous.
3. **Load the hook reference** (once per \`/init\` run, before the first hook): invoke the Skill tool with \`skill: 'update-config'\` and args starting with \`[hooks-only]\` followed by a one-line summary of what you're building — e.g., \`[hooks-only] Constructing a PostToolUse/Write|Edit format hook for .claude/settings.json using ruff\`. This loads the hooks schema and verification flow into context. Subsequent hooks reuse it — don't re-invoke.
3. **Load the hook reference** (once per \`/init\` run, before the first hook): invoke the Skill tool with \`skill: 'update-config'\` and args starting with \`[hooks-only]\` followed by a one-line summary of what you're building — e.g., \`[hooks-only] Constructing a PostToolUse/Write|Edit format hook for .openclaude/settings.json using ruff\`. This loads the hooks schema and verification flow into context. Subsequent hooks reuse it — don't re-invoke.
4. Follow the skill's **"Constructing a Hook"** flow: dedup check → construct for THIS project → pipe-test raw → wrap → write JSON → \`jq -e\` validate → live-proof (for \`Pre|PostToolUse\` on triggerable matchers) → cleanup → handoff. Target file and event/matcher come from steps 12 above.
+1 -1
View File
@@ -1223,7 +1223,7 @@ Include 3 friction categories with 2 examples each.`,
- Good for: repetitive workflows - /commit, /review, /test, /deploy, /pr, or complex multi-step workflows
3. **Hooks**: Shell commands that auto-run at specific lifecycle events.
- How to use: Add to \`.claude/settings.json\` under "hooks" key.
- How to use: Add to \`.openclaude/settings.json\` under "hooks" key.
- Good for: auto-formatting code, running type checks, enforcing conventions
4. **Headless Mode**: Run Claude non-interactively from scripts and CI/CD.
@@ -203,7 +203,7 @@ function OnboardGithub(props: {
if (!activated.ok) {
setErrorMsg(
`Token saved, but settings were not updated: ${activated.detail ?? 'unknown error'}. ` +
`Add env CLAUDE_CODE_USE_GITHUB=1 and OPENAI_MODEL to ~/.claude/settings.json manually.`,
`Add env CLAUDE_CODE_USE_GITHUB=1 and OPENAI_MODEL to ~/.openclaude/settings.json manually.`,
)
setStep('error')
return
+5 -5
View File
@@ -1043,7 +1043,7 @@ export function ManagePlugins({
{
if (isBuiltin) break; // guarded above; narrows pluginScope
if (!isInstallableScope(pluginScope)) break;
// If the plugin is enabled in .claude/settings.json (shared with the
// If the plugin is enabled in .openclaude/settings.json (shared with the
// team), divert to a confirmation dialog that offers to disable in
// settings.local.json instead. Check the settings file directly —
// `pluginScope` (from installed_plugins.json) can be 'user' even when
@@ -1526,7 +1526,7 @@ export function ManagePlugins({
return;
}
clearAllCaches();
setResult(`✓ Disabled ${selectedPlugin.plugin.name} in .claude/settings.local.json. Run /reload-plugins to apply.`);
setResult(`✓ Disabled ${selectedPlugin.plugin.name} in .openclaude/settings.local.json. Run /reload-plugins to apply.`);
if (onManageComplete) void onManageComplete();
setParentViewState({
type: 'menu'
@@ -1760,16 +1760,16 @@ export function ManagePlugins({
</Box>;
}
// Confirm-project-uninstall: warn about shared .claude/settings.json,
// Confirm-project-uninstall: warn about shared .openclaude/settings.json,
// offer to disable in settings.local.json instead.
if (viewState === 'confirm-project-uninstall' && selectedPlugin) {
return <Box flexDirection="column">
<Text bold color="warning">
{selectedPlugin.plugin.name} is enabled in .claude/settings.json
{selectedPlugin.plugin.name} is enabled in .openclaude/settings.json
(shared with your team)
</Text>
<Box marginTop={1} flexDirection="column">
<Text>Disable it just for you in .claude/settings.local.json?</Text>
<Text>Disable it just for you in .openclaude/settings.local.json?</Text>
<Text dimColor>
This has the same effect as uninstalling, without affecting other
contributors.
+1 -1
View File
@@ -9,7 +9,7 @@ const statusline = {
aliases: [],
name: 'statusline',
progressMessage: 'setting up statusLine',
allowedTools: [AGENT_TOOL_NAME, 'Read(~/**)', 'Edit(~/.claude/settings.json)'],
allowedTools: [AGENT_TOOL_NAME, 'Read(~/**)', 'Edit(~/.openclaude/settings.json)'],
source: 'builtin',
disableNonInteractive: true,
async getPromptForCommand(args): Promise<ContentBlockParam[]> {
+2 -2
View File
@@ -35,8 +35,8 @@ export type Props = {
/**
* When true, skip writing effortLevel to userSettings on selection.
* Used by the assistant installer wizard where the model choice is
* project-scoped (written to the assistant's .claude/settings.json via
* install.ts) and should not leak to the user's global ~/.claude/settings.
* project-scoped (written to the assistant's .openclaude/settings.json via
* install.ts) and should not leak to the user's global ~/.openclaude/settings.json.
*/
skipSettingsWrite?: boolean;
optionsOverride?: ModelOption[];
+292
View File
@@ -0,0 +1,292 @@
import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test'
import { acquireSharedMutationLock, releaseSharedMutationLock } from '../../test/sharedMutationLock.js'
import { BASH_TOOL_NAME } from '../../tools/BashTool/toolName.js'
import type { PermissionRule } from '../../utils/permissions/PermissionRule.js'
import { getRelativeSettingsFilePathForSource as REAL_canonicalPath } from '../../utils/settings/settings.js'
import type { SettingsJson } from '../../utils/settings/types.js'
// TrustDialog/utils.ts emits file-path strings shown to users in the trust
// dialog. These MUST come from getRelativeSettingsFilePathForSource — the
// single source of truth — not hardcoded literals. The previous code hardcoded
// '.claude/...' and drifted from the canonical '.openclaude/...'.
//
// This is a TRUE contract test with TWO independent assertions:
// 1. The real getRelativeSettingsFilePathForSource returns the EXPECTED
// literal '.openclaude/...' (catches a settings.ts regression).
// 2. utils.ts getters return the same value as the real function (catches
// a utils.ts hardcoding regression).
// The EXPECTED literal is hardcoded HERE and only here — if settings.ts drifts
// away from it, assertion (1) fails; if utils.ts stops using the function,
// assertion (2) fails. No self-reference.
await acquireSharedMutationLock('components/TrustDialog/utils.test.ts')
// The fork's canonical project settings paths. Hardcoded expected values —
// independent of the implementation under test.
const EXPECTED = {
projectSettings: '.openclaude/settings.json',
localSettings: '.openclaude/settings.local.json',
} as const
// Contract assertion (1): the real source-of-truth function must return these.
// Wrapped in a named test so a settings.ts regression shows up as a clear
// test failure in CI rather than a module-load error.
test('getRelativeSettingsFilePathForSource returns the canonical .openclaude/ paths', () => {
expect(REAL_canonicalPath('projectSettings')).toBe(EXPECTED.projectSettings)
expect(REAL_canonicalPath('localSettings')).toBe(EXPECTED.localSettings)
})
const settingsState: {
projectSettings: SettingsJson | null
localSettings: SettingsJson | null
} = {
projectSettings: null,
localSettings: null,
}
const permissionRulesState: {
projectSettings: PermissionRule[]
localSettings: PermissionRule[]
} = {
projectSettings: [],
localSettings: [],
}
mock.module('../../utils/settings/settings.js', () => ({
// Stateful — needs mocking to control test inputs.
getSettingsForSource: (source: 'projectSettings' | 'localSettings') =>
settingsState[source],
// Pure — pass the REAL function through. The mock exists only because
// utils.ts imports both names from the same module; we must provide both.
getRelativeSettingsFilePathForSource: REAL_canonicalPath,
}))
mock.module('../../utils/permissions/permissionsLoader.js', () => ({
getPermissionRulesForSource: (
source: 'projectSettings' | 'localSettings',
): PermissionRule[] => permissionRulesState[source],
}))
afterAll(() => {
try {
mock.restore()
} finally {
releaseSharedMutationLock()
}
})
beforeEach(() => {
settingsState.projectSettings = null
settingsState.localSettings = null
permissionRulesState.projectSettings = []
permissionRulesState.localSettings = []
})
async function freshUtils() {
const stamp = `${Date.now()}-${Math.random()}`
return import(`./utils.ts?ts=${stamp}`)
}
const bashAllow = (
source: PermissionRule['source'],
toolName: string = BASH_TOOL_NAME,
): PermissionRule => ({
source,
ruleBehavior: 'allow',
ruleValue: { toolName },
})
// Helper: build a SettingsJson with only the fields we care about.
const settings = (overrides: Partial<SettingsJson>): SettingsJson =>
({ ...overrides }) as unknown as SettingsJson
describe('TrustDialog utils — canonical paths from source-of-truth', () => {
describe('getHooksSources', () => {
test('reports canonical paths when hooks present in both sources', async () => {
settingsState.projectSettings = settings({
hooks: {
PreToolUse: [{ matcher: 'Read', hooks: [{ type: 'command', command: 'true' }] }],
},
})
settingsState.localSettings = settings({
statusLine: { type: 'command', command: 'echo hi' },
})
const { getHooksSources } = await freshUtils()
expect(getHooksSources().sort()).toEqual([
EXPECTED.projectSettings,
EXPECTED.localSettings,
])
})
test('reports only project when local has no hooks', async () => {
settingsState.projectSettings = settings({
hooks: { PostToolUse: [{ matcher: '.*', hooks: [{ type: 'command', command: 'true' }] }] },
})
settingsState.localSettings = settings({})
const { getHooksSources } = await freshUtils()
expect(getHooksSources()).toEqual([EXPECTED.projectSettings])
})
test('empty hooks object does not count as having hooks', async () => {
settingsState.projectSettings = settings({ hooks: {} })
const { getHooksSources } = await freshUtils()
expect(getHooksSources()).toEqual([])
})
test('hooks with empty matcher arrays do not count', async () => {
settingsState.projectSettings = settings({
hooks: { PreToolUse: [] },
})
const { getHooksSources } = await freshUtils()
expect(getHooksSources()).toEqual([])
})
test('fileSuggestion alone counts as a hook source', async () => {
settingsState.projectSettings = settings({
fileSuggestion: { enabled: true } as unknown as SettingsJson['fileSuggestion'],
})
const { getHooksSources } = await freshUtils()
expect(getHooksSources()).toEqual([EXPECTED.projectSettings])
})
test('disableAllHooks suppresses even when hooks are configured', async () => {
settingsState.projectSettings = settings({
disableAllHooks: true,
hooks: {
PreToolUse: [{ matcher: 'Read', hooks: [{ type: 'command', command: 'true' }] }],
},
statusLine: { type: 'command', command: 'echo hi' },
})
const { getHooksSources } = await freshUtils()
expect(getHooksSources()).toEqual([])
})
test('null settings produce no sources', async () => {
const { getHooksSources } = await freshUtils()
expect(getHooksSources()).toEqual([])
})
test('non-null settings with no hooks/statusLine/fileSuggestion produce no sources', async () => {
// Covers the `if (!settings.hooks) return false` branch — distinct from
// the null-settings branch above. Settings exist but have nothing that
// counts as a hook source.
settingsState.projectSettings = settings({ model: 'claude-3' })
const { getHooksSources } = await freshUtils()
expect(getHooksSources()).toEqual([])
})
})
describe('getBashPermissionSources', () => {
test('reports canonical paths when bash allow rules in both sources', async () => {
permissionRulesState.projectSettings = [bashAllow('projectSettings')]
permissionRulesState.localSettings = [
bashAllow('localSettings', `${BASH_TOOL_NAME}(npm test)`),
]
const { getBashPermissionSources } = await freshUtils()
expect(getBashPermissionSources().sort()).toEqual([
EXPECTED.projectSettings,
EXPECTED.localSettings,
])
})
test('toolName prefix match (Bash(...)) counts as bash permission', async () => {
permissionRulesState.localSettings = [
bashAllow('localSettings', `${BASH_TOOL_NAME}(rm -rf /)`),
]
const { getBashPermissionSources } = await freshUtils()
expect(getBashPermissionSources()).toEqual([EXPECTED.localSettings])
})
test('non-Bash tool allow rule does NOT count', async () => {
permissionRulesState.projectSettings = [bashAllow('projectSettings', 'Read')]
const { getBashPermissionSources } = await freshUtils()
expect(getBashPermissionSources()).toEqual([])
})
test('deny behavior does NOT count even for Bash', async () => {
permissionRulesState.projectSettings = [
{ ...bashAllow('projectSettings'), ruleBehavior: 'deny' },
]
const { getBashPermissionSources } = await freshUtils()
expect(getBashPermissionSources()).toEqual([])
})
test('empty rules produce no sources', async () => {
const { getBashPermissionSources } = await freshUtils()
expect(getBashPermissionSources()).toEqual([])
})
})
// Cover the remaining getters — each should emit canonical paths when its
// trigger field is present, and nothing otherwise.
describe('remaining getters — canonical path coverage', () => {
test('getOtelHeadersHelperSources', async () => {
const { getOtelHeadersHelperSources } = await freshUtils()
expect(getOtelHeadersHelperSources()).toEqual([])
settingsState.projectSettings = settings({
otelHeadersHelper: 'cat /tmp/headers',
} as Partial<SettingsJson>)
expect(getOtelHeadersHelperSources()).toEqual([EXPECTED.projectSettings])
})
test('getApiKeyHelperSources', async () => {
const { getApiKeyHelperSources } = await freshUtils()
expect(getApiKeyHelperSources()).toEqual([])
settingsState.localSettings = settings({
apiKeyHelper: '/usr/local/bin/token-helper',
} as Partial<SettingsJson>)
expect(getApiKeyHelperSources()).toEqual([EXPECTED.localSettings])
})
test('getAwsCommandsSources', async () => {
const { getAwsCommandsSources } = await freshUtils()
expect(getAwsCommandsSources()).toEqual([])
settingsState.projectSettings = settings({
awsAuthRefresh: 'aws sso login',
} as Partial<SettingsJson>)
expect(getAwsCommandsSources()).toEqual([EXPECTED.projectSettings])
})
test('getAwsCommandsSources — awsCredentialExport alone also triggers', async () => {
// hasAwsCommands is an OR of awsAuthRefresh || awsCredentialExport.
// Cover the second operand so a regression that drops it is caught.
settingsState.localSettings = settings({
awsCredentialExport: 'export AWS_CREDENTIALS=$CREDS',
} as unknown as Partial<SettingsJson>)
const { getAwsCommandsSources } = await freshUtils()
expect(getAwsCommandsSources()).toEqual([EXPECTED.localSettings])
})
test('getGcpCommandsSources', async () => {
const { getGcpCommandsSources } = await freshUtils()
expect(getGcpCommandsSources()).toEqual([])
settingsState.localSettings = settings({
gcpAuthRefresh: 'gcloud auth print-access-token',
} as Partial<SettingsJson>)
expect(getGcpCommandsSources()).toEqual([EXPECTED.localSettings])
})
test('getDangerousEnvVarsSources — dangerous var present', async () => {
const { getDangerousEnvVarsSources } = await freshUtils()
expect(getDangerousEnvVarsSources()).toEqual([])
// Most env vars are unsafe by design; pick one not in SAFE_ENV_VARS.
settingsState.projectSettings = settings({ env: { SUPER_SECRET_TOKEN: 'x' } })
expect(getDangerousEnvVarsSources()).toEqual([EXPECTED.projectSettings])
})
test('getDangerousEnvVarsSources — only safe vars produces nothing', async () => {
// AWS_REGION is in SAFE_ENV_VARS; must not trigger the warning.
settingsState.projectSettings = settings({ env: { AWS_REGION: 'us-east-1' } })
const { getDangerousEnvVarsSources } = await freshUtils()
expect(getDangerousEnvVarsSources()).toEqual([])
})
})
})
+18 -15
View File
@@ -1,5 +1,8 @@
import type { PermissionRule } from 'src/utils/permissions/PermissionRule.js'
import { getSettingsForSource } from 'src/utils/settings/settings.js'
import {
getRelativeSettingsFilePathForSource,
getSettingsForSource,
} from 'src/utils/settings/settings.js'
import type { SettingsJson } from 'src/utils/settings/types.js'
import { BASH_TOOL_NAME } from '../../tools/BashTool/toolName.js'
import { SAFE_ENV_VARS } from '../../utils/managedEnvConstants.js'
@@ -31,12 +34,12 @@ export function getHooksSources(): string[] {
const projectSettings = getSettingsForSource('projectSettings')
if (hasHooks(projectSettings)) {
sources.push('.claude/settings.json')
sources.push(getRelativeSettingsFilePathForSource('projectSettings'))
}
const localSettings = getSettingsForSource('localSettings')
if (hasHooks(localSettings)) {
sources.push('.claude/settings.local.json')
sources.push(getRelativeSettingsFilePathForSource('localSettings'))
}
return sources
@@ -60,12 +63,12 @@ export function getBashPermissionSources(): string[] {
const projectRules = getPermissionRulesForSource('projectSettings')
if (hasBashPermission(projectRules)) {
sources.push('.claude/settings.json')
sources.push(getRelativeSettingsFilePathForSource('projectSettings'))
}
const localRules = getPermissionRulesForSource('localSettings')
if (hasBashPermission(localRules)) {
sources.push('.claude/settings.local.json')
sources.push(getRelativeSettingsFilePathForSource('localSettings'))
}
return sources
@@ -119,12 +122,12 @@ export function getOtelHeadersHelperSources(): string[] {
const projectSettings = getSettingsForSource('projectSettings')
if (hasOtelHeadersHelper(projectSettings)) {
sources.push('.claude/settings.json')
sources.push(getRelativeSettingsFilePathForSource('projectSettings'))
}
const localSettings = getSettingsForSource('localSettings')
if (hasOtelHeadersHelper(localSettings)) {
sources.push('.claude/settings.local.json')
sources.push(getRelativeSettingsFilePathForSource('localSettings'))
}
return sources
@@ -146,12 +149,12 @@ export function getApiKeyHelperSources(): string[] {
const projectSettings = getSettingsForSource('projectSettings')
if (hasApiKeyHelper(projectSettings)) {
sources.push('.claude/settings.json')
sources.push(getRelativeSettingsFilePathForSource('projectSettings'))
}
const localSettings = getSettingsForSource('localSettings')
if (hasApiKeyHelper(localSettings)) {
sources.push('.claude/settings.local.json')
sources.push(getRelativeSettingsFilePathForSource('localSettings'))
}
return sources
@@ -173,12 +176,12 @@ export function getAwsCommandsSources(): string[] {
const projectSettings = getSettingsForSource('projectSettings')
if (hasAwsCommands(projectSettings)) {
sources.push('.claude/settings.json')
sources.push(getRelativeSettingsFilePathForSource('projectSettings'))
}
const localSettings = getSettingsForSource('localSettings')
if (hasAwsCommands(localSettings)) {
sources.push('.claude/settings.local.json')
sources.push(getRelativeSettingsFilePathForSource('localSettings'))
}
return sources
@@ -200,12 +203,12 @@ export function getGcpCommandsSources(): string[] {
const projectSettings = getSettingsForSource('projectSettings')
if (hasGcpCommands(projectSettings)) {
sources.push('.claude/settings.json')
sources.push(getRelativeSettingsFilePathForSource('projectSettings'))
}
const localSettings = getSettingsForSource('localSettings')
if (hasGcpCommands(localSettings)) {
sources.push('.claude/settings.local.json')
sources.push(getRelativeSettingsFilePathForSource('localSettings'))
}
return sources
@@ -233,12 +236,12 @@ export function getDangerousEnvVarsSources(): string[] {
const projectSettings = getSettingsForSource('projectSettings')
if (hasDangerousEnvVars(projectSettings)) {
sources.push('.claude/settings.json')
sources.push(getRelativeSettingsFilePathForSource('projectSettings'))
}
const localSettings = getSettingsForSource('localSettings')
if (hasDangerousEnvVars(localSettings)) {
sources.push('.claude/settings.local.json')
sources.push(getRelativeSettingsFilePathForSource('localSettings'))
}
return sources
+1 -1
View File
@@ -46,7 +46,7 @@ export function SelectEventMode(t0) {
const subtitle = `${totalHooksCount} ${t1} configured`;
let t2;
if ($[2] !== restrictedByPolicy) {
t2 = restrictedByPolicy && <Box flexDirection="column"><Text color="suggestion">{figures.info} Hooks Restricted by Policy</Text><Text dimColor={true}>Only hooks from managed settings can run. User-defined hooks from ~/.claude/settings.json, .claude/settings.json, and .claude/settings.local.json are blocked.</Text></Box>;
t2 = restrictedByPolicy && <Box flexDirection="column"><Text color="suggestion">{figures.info} Hooks Restricted by Policy</Text><Text dimColor={true}>Only hooks from managed settings can run. User-defined hooks from ~/.openclaude/settings.json (user), .openclaude/settings.json (project), and .openclaude/settings.local.json (local) are blocked.</Text></Box>;
$[2] = restrictedByPolicy;
$[3] = t2;
} else {
+3 -3
View File
@@ -1215,9 +1215,9 @@ export const SettingSourceSchema = lazySchema(() =>
.enum(['user', 'project', 'local'])
.describe(
'Source for loading filesystem-based settings. ' +
"'user' - Global user settings (~/.claude/settings.json). " +
"'project' - Project settings (.claude/settings.json). " +
"'local' - Local settings (.claude/settings.local.json).",
"'user' - Global user settings (~/.openclaude/settings.json). " +
"'project' - Project settings (.openclaude/settings.json). " +
"'local' - Local settings (.openclaude/settings.local.json).",
),
)
+1 -1
View File
@@ -1538,7 +1538,7 @@ export type AgentDefinition = {
permissionMode?: "default" | "acceptEdits" | "bypassPermissions" | "fullAccess" | "plan" | "dontAsk"
}
/** Source for loading filesystem-based settings. 'user' - Global user settings (~/.claude/settings.json). 'project' - Project settings (.claude/settings.json). 'local' - Local settings (.claude/settings.local.json). */
/** Source for loading filesystem-based settings. 'user' - Global user settings (~/.openclaude/settings.json). 'project' - Project settings (.openclaude/settings.json). 'local' - Local settings (.openclaude/settings.local.json). */
export type SettingSource = "user" | "project" | "local"
/** Configuration for loading a plugin. */
+1 -1
View File
@@ -1713,7 +1713,7 @@ export function REPL({
if (wt.creationDurationMs < 15_000) return;
worktreeTipShownRef.current = true;
const secs = Math.round(wt.creationDurationMs / 1000);
setMessages(prev => [...prev, createSystemMessage(`Worktree creation took ${secs}s. For large repos, set \`worktree.sparsePaths\` in .claude/settings.json to check out only the directories you need — e.g. \`{"worktree": {"sparsePaths": ["src", "packages/foo"]}}\`.`, 'info')]);
setMessages(prev => [...prev, createSystemMessage(`Worktree creation took ${secs}s. For large repos, set \`worktree.sparsePaths\` in .openclaude/settings.json to check out only the directories you need — e.g. \`{"worktree": {"sparsePaths": ["src", "packages/foo"]}}\`.`, 'info')]);
}, [setMessages]);
// Hide spinner when the only in-progress tool is Sleep
+4 -4
View File
@@ -116,7 +116,7 @@ export function getProjectPathForScope(scope: PluginScope): string | undefined {
}
/**
* Is this plugin enabled (value === true) in .claude/settings.json?
* Is this plugin enabled (value === true) in .openclaude/settings.json?
*
* Distinct from V2 installed_plugins.json scope: that file tracks where a
* plugin was *installed from*, but the same plugin can also be enabled at
@@ -482,12 +482,12 @@ export async function uninstallPluginOp(
// Try to find where the plugin is actually installed to provide a helpful error
const { scope: actualScope } = getPluginInstallationFromV2(pluginId)
if (actualScope !== scope && installations && installations.length > 0) {
// Project scope is special: .claude/settings.json is shared with the team.
// Project scope is special: .openclaude/settings.json is shared with the team.
// Point users at the local-override escape hatch instead of --scope project.
if (actualScope === 'project') {
return {
success: false,
message: `Plugin "${plugin}" is enabled at project scope (.claude/settings.json, shared with your team). To disable just for you: claude plugin disable ${plugin} --scope local`,
message: `Plugin "${plugin}" is enabled at project scope (.openclaude/settings.json, shared with your team). To disable just for you: claude plugin disable ${plugin} --scope local`,
}
}
return {
@@ -668,7 +668,7 @@ export async function setPluginEnabledOp(
// different scope, guide the user to the right --scope — UNLESS they're
// writing to a higher-precedence scope to override a lower one
// (e.g. `disable --scope local` to override a project-enabled plugin
// without touching the shared .claude/settings.json).
// without touching the shared .openclaude/settings.json).
const SCOPE_PRECEDENCE: Record<InstallableScope, number> = {
user: 0,
project: 1,
+12
View File
@@ -24,4 +24,16 @@ test('update-config skill can generate its prompt without JSON Schema conversion
expect((blocks[0] as { text: string }).text).toContain(
'## Full Settings JSON Schema',
)
expect((blocks[0] as { text: string }).text).toContain(
'.openclaude/settings.json',
)
expect((blocks[0] as { text: string }).text).toContain(
'.openclaude/settings.local.json',
)
expect((blocks[0] as { text: string }).text).not.toContain(
'.claude/settings.json',
)
expect((blocks[0] as { text: string }).text).not.toContain(
'.claude/settings.local.json',
)
})
+12 -7
View File
@@ -1,4 +1,5 @@
import { toJSONSchema } from 'zod/v4'
import { getRelativeSettingsFilePathForSource } from '../../utils/settings/settings.js'
import { SettingsSchema } from '../../utils/settings/types.js'
import { jsonStringify } from '../../utils/slowOperations.js'
import { registerBundledSkill } from '../bundledSkills.js'
@@ -12,15 +13,19 @@ function generateSettingsSchema(): string {
return jsonStringify(jsonSchema, null, 2)
}
const USER_SETTINGS_PATH = '~/.openclaude/settings.json'
const PROJECT_SETTINGS_PATH = getRelativeSettingsFilePathForSource('projectSettings')
const LOCAL_SETTINGS_PATH = getRelativeSettingsFilePathForSource('localSettings')
const SETTINGS_EXAMPLES_DOCS = `## Settings File Locations
Choose the appropriate file based on scope:
| File | Scope | Git | Use For |
|------|-------|-----|---------|
| \`~/.claude/settings.json\` | Global | N/A | Personal preferences for all projects |
| \`.claude/settings.json\` | Project | Commit | Team-wide hooks, permissions, plugins |
| \`.claude/settings.local.json\` | Project | Gitignore | Personal overrides for this project |
| \`${USER_SETTINGS_PATH}\` | Global | N/A | Personal preferences for all projects |
| \`${PROJECT_SETTINGS_PATH}\` | Project | Commit | Team-wide hooks, permissions, plugins |
| \`${LOCAL_SETTINGS_PATH}\` | Project | Gitignore | Personal overrides for this project |
Settings load in order: user → project → local (later overrides earlier).
@@ -286,7 +291,7 @@ Given an event, matcher, target file, and desired behavior, follow this flow. Ea
Check exit code AND side effect (file actually formatted, test actually ran). If it fails you get a real error — fix (wrong package manager? tool not installed? jq path wrong?) and retest. Once it works, wrap with \`2>/dev/null || true\` (unless the user wants a blocking check).
4. **Write the JSON.** Merge into the target file (schema shape in the "Hook Structure" section above). If this creates \`.claude/settings.local.json\` for the first time, add it to .gitignore — the Write tool doesn't auto-gitignore it.
4. **Write the JSON.** Merge into the target file (schema shape in the "Hook Structure" section above). If this creates \`${LOCAL_SETTINGS_PATH}\` for the first time, add it to .gitignore — the Write tool doesn't auto-gitignore it.
5. **Validate syntax + schema in one shot:**
@@ -300,7 +305,7 @@ Given an event, matcher, target file, and desired behavior, follow this flow. Ea
**Always clean up** — revert the violation, strip the sentinel prefix — whether the proof passed or failed.
**If proof fails but pipe-test passed and \`jq -e\` passed**: the settings watcher isn't watching \`.claude/\` — it only watches directories that had a settings file when this session started. The hook is written correctly. Tell the user to open \`/hooks\` once (reloads config) or restart — you can't do this yourself; \`/hooks\` is a user UI menu and opening it ends this turn.
**If proof fails but pipe-test passed and \`jq -e\` passed**: the settings watcher isn't watching \`.openclaude/\` — it only watches directories that had a settings file when this session started. The hook is written correctly. Tell the user to open \`/hooks\` once (reloads config) or restart — you can't do this yourself; \`/hooks\` is a user UI menu and opening it ends this turn.
7. **Handoff.** Tell the user the hook is live (or needs \`/hooks\`/restart per the watcher caveat). Point them at \`/hooks\` to review, edit, or disable it later. The UI only shows "Ran N hooks" if a hook errors or is slow — silent success is invisible by design.
`
@@ -389,7 +394,7 @@ ${HOOK_VERIFICATION_FLOW}
User: "Format my code after Claude writes it"
1. **Clarify**: Which formatter? (prettier, gofmt, etc.)
2. **Read**: \`.claude/settings.json\` (or create if missing)
2. **Read**: \`${PROJECT_SETTINGS_PATH}\` (or create if missing)
3. **Merge**: Add to existing hooks, don't replace
4. **Result**:
\`\`\`json
@@ -435,7 +440,7 @@ User: "Set DEBUG=true"
## Troubleshooting Hooks
If a hook isn't running:
1. **Check the settings file** - Read ~/.claude/settings.json or .claude/settings.json
1. **Check the settings file** - Read ${USER_SETTINGS_PATH}, ${PROJECT_SETTINGS_PATH}, or ${LOCAL_SETTINGS_PATH}
2. **Verify JSON syntax** - Invalid JSON silently fails
3. **Check the matcher** - Does it match the tool name? (e.g., "Bash", "Write", "Edit")
4. **Check hook type** - Is it "command", "prompt", or "agent"?
@@ -0,0 +1,115 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'fs'
import { tmpdir } from 'os'
import { dirname, join } from 'path'
import { getOriginalCwd, setOriginalCwd } from '../bootstrap/state.js'
import {
acquireSharedMutationLock,
releaseSharedMutationLock,
} from '../test/sharedMutationLock.js'
import { detectStaleProjectSettingsPaths } from './doctorDiagnostic.js'
let tempDir: string | null = null
let originalCwd: string | null = null
function createProject(): string {
tempDir = mkdtempSync(join(tmpdir(), 'openclaude-settings-drift-'))
return tempDir
}
function writeJson(path: string): void {
mkdirSync(dirname(path), { recursive: true })
writeFileSync(path, '{}', 'utf8')
}
beforeEach(async () => {
await acquireSharedMutationLock('utils/doctorDiagnostic.settingsPath.test.ts')
originalCwd = getOriginalCwd()
})
afterEach(() => {
try {
if (originalCwd) {
setOriginalCwd(originalCwd)
originalCwd = null
}
if (tempDir) {
rmSync(tempDir, { recursive: true, force: true })
tempDir = null
}
} finally {
releaseSharedMutationLock()
}
})
describe('detectStaleProjectSettingsPaths', () => {
test('warns when legacy project settings exist without canonical settings', async () => {
const project = createProject()
writeJson(join(project, '.claude', 'settings.json'))
const warning = await detectStaleProjectSettingsPaths(project)
expect(warning).toEqual({
issue:
'Legacy project settings file .claude/settings.json found, but OpenClaude reads .openclaude/settings.json',
fix:
'Move or copy .claude/settings.json to .openclaude/settings.json if you intended OpenClaude to use those project settings.',
})
})
test('does not warn when the matching canonical project settings file exists', async () => {
const project = createProject()
writeJson(join(project, '.claude', 'settings.json'))
writeJson(join(project, '.openclaude', 'settings.json'))
await expect(detectStaleProjectSettingsPaths(project)).resolves.toBeNull()
})
test('does not warn when no legacy project settings files exist', async () => {
const project = createProject()
await expect(detectStaleProjectSettingsPaths(project)).resolves.toBeNull()
})
test('does not warn when only canonical settings files exist', async () => {
const project = createProject()
writeJson(join(project, '.openclaude', 'settings.json'))
writeJson(join(project, '.openclaude', 'settings.local.json'))
await expect(detectStaleProjectSettingsPaths(project)).resolves.toBeNull()
})
test('warns independently for legacy local settings', async () => {
const project = createProject()
writeJson(join(project, '.claude', 'settings.local.json'))
const warning = await detectStaleProjectSettingsPaths(project)
expect(warning?.issue).toContain('.claude/settings.local.json')
expect(warning?.issue).toContain('.openclaude/settings.local.json')
})
test('warns about both legacy settings files when both canonical files are absent', async () => {
const project = createProject()
writeJson(join(project, '.claude', 'settings.json'))
writeJson(join(project, '.claude', 'settings.local.json'))
const warning = await detectStaleProjectSettingsPaths(project)
expect(warning?.issue).toContain('.claude/settings.json')
expect(warning?.issue).toContain('.claude/settings.local.json')
expect(warning?.issue).toContain('.openclaude/settings.json')
expect(warning?.issue).toContain('.openclaude/settings.local.json')
})
test('uses the settings resolver project root by default', async () => {
const project = createProject()
writeJson(join(project, '.claude', 'settings.json'))
setOriginalCwd(project)
const warning = await detectStaleProjectSettingsPaths()
expect(warning?.issue).toContain('.claude/settings.json')
expect(warning?.issue).toContain('.openclaude/settings.json')
})
})
+53
View File
@@ -35,6 +35,10 @@ import { getPlatform } from './platform.js'
import { getRipgrepStatus } from './ripgrep.js'
import { SandboxManager } from './sandbox/sandbox-adapter.js'
import { getManagedFilePath } from './settings/managedPath.js'
import {
getRelativeSettingsFilePathForSource,
getSettingsRootPathForSource,
} from './settings/settings.js'
import { CUSTOMIZATION_SURFACES } from './settings/types.js'
import {
findClaudeAlias,
@@ -336,11 +340,60 @@ async function detectMultipleInstallations(): Promise<
return installations
}
async function pathExists(path: string): Promise<boolean> {
try {
await getFsImplementation().stat(path)
return true
} catch {
return false
}
}
export async function detectStaleProjectSettingsPaths(
cwd: string = getSettingsRootPathForSource('projectSettings'),
): Promise<{ issue: string; fix: string } | null> {
const pairs = [
{
legacy: '.claude/settings.json',
canonical: getRelativeSettingsFilePathForSource('projectSettings'),
},
{
legacy: '.claude/settings.local.json',
canonical: getRelativeSettingsFilePathForSource('localSettings'),
},
]
const stale: Array<{ legacy: string; canonical: string }> = []
for (const pair of pairs) {
const legacyExists = await pathExists(join(cwd, pair.legacy))
if (!legacyExists) continue
const canonicalExists = await pathExists(join(cwd, pair.canonical))
if (!canonicalExists) {
stale.push(pair)
}
}
if (stale.length === 0) return null
const legacyPaths = stale.map(pair => pair.legacy).join(', ')
const canonicalPaths = stale.map(pair => pair.canonical).join(', ')
return {
issue: `Legacy project settings file${stale.length === 1 ? '' : 's'} ${legacyPaths} found, but OpenClaude reads ${canonicalPaths}`,
fix: `Move or copy ${legacyPaths} to ${canonicalPaths} if you intended OpenClaude to use those project settings.`,
}
}
async function detectConfigurationIssues(
type: InstallationType,
): Promise<Array<{ issue: string; fix: string }>> {
const warnings: Array<{ issue: string; fix: string }> = []
const staleProjectSettingsWarning = await detectStaleProjectSettingsPaths()
if (staleProjectSettingsWarning) {
warnings.push(staleProjectSettingsWarning)
}
// Managed-settings forwards-compat: the schema preprocess silently drops
// unknown strictPluginOnlyCustomization surface names so one future enum
// value doesn't null out the entire policy file (settings.ts:101). But
+13
View File
@@ -0,0 +1,13 @@
import { describe, expect, test } from 'bun:test'
import { hookSourceDescriptionDisplayString } from './hooksSettings.js'
describe('hookSourceDescriptionDisplayString', () => {
test('uses the canonical OpenClaude plugin path for plugin hooks', () => {
const description = hookSourceDescriptionDisplayString('pluginHook')
expect(description).toBe(
'Plugin hooks (~/.openclaude/plugins/*/hooks/hooks.json)',
)
expect(description).not.toContain('~/.claude/')
})
})
+5 -5
View File
@@ -170,16 +170,16 @@ export function getHooksForEvent(
export function hookSourceDescriptionDisplayString(source: HookSource): string {
switch (source) {
case 'userSettings':
return 'User settings (~/.claude/settings.json)'
return 'User settings (~/.openclaude/settings.json)'
case 'projectSettings':
return 'Project settings (.claude/settings.json)'
return 'Project settings (.openclaude/settings.json)'
case 'localSettings':
return 'Local settings (.claude/settings.local.json)'
return 'Local settings (.openclaude/settings.local.json)'
case 'pluginHook':
// TODO: Get the actual plugin hook file paths instead of using glob pattern
// We should capture the specific plugin paths during hook registration and display them here
// e.g., "Plugin hooks (~/.claude/plugins/repos/source/example-plugin/example-plugin/hooks/hooks.json)"
return 'Plugin hooks (~/.claude/plugins/*/hooks/hooks.json)'
// e.g., "Plugin hooks (~/.openclaude/plugins/repos/source/example-plugin/example-plugin/hooks/hooks.json)"
return 'Plugin hooks (~/.openclaude/plugins/*/hooks/hooks.json)'
case 'sessionHook':
return 'Session hooks (in-memory, temporary)'
case 'builtinHook':
+1 -1
View File
@@ -6,7 +6,7 @@
* - Which plugins are installed globally
* - Installation metadata (version, timestamps, paths)
*
* The enabled/disabled state remains in .claude/settings.json for per-repo control.
* The enabled/disabled state remains in .openclaude/settings.json for per-repo control.
*
* Rationale: Installation is global (a plugin is either on disk or not), while
* enabled/disabled state is per-repository (different projects may want different
+3 -3
View File
@@ -1496,9 +1496,9 @@ export const InstalledPluginsFileSchemaV1 = lazySchema(() =>
*
* Plugins can be installed at different scopes:
* - managed: Enterprise/system-wide (read-only, platform-specific paths)
* - user: User's global settings (~/.claude/settings.json)
* - project: Shared project settings ($project/.claude/settings.json)
* - local: Personal project overrides ($project/.claude/settings.local.json)
* - user: User's global settings (~/.openclaude/settings.json)
* - project: Shared project settings ($project/.openclaude/settings.json)
* - local: Personal project overrides ($project/.openclaude/settings.local.json)
*
* Note: 'flag' scope plugins (from --settings) are session-only and
* are NOT persisted to installed_plugins.json.
+75
View File
@@ -0,0 +1,75 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import { mkdtemp, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join, resolve } from 'path'
import {
getCwdState,
getOriginalCwd,
setCwdState,
setOriginalCwd,
} from '../../bootstrap/state.js'
import {
acquireSharedMutationLock,
releaseSharedMutationLock,
} from '../../test/sharedMutationLock.js'
import { resetSettingsCache } from '../settings/settingsCache.js'
import type { SettingsJson } from '../settings/types.js'
import { convertToSandboxRuntimeConfig } from './sandbox-adapter.js'
describe('convertToSandboxRuntimeConfig', () => {
let previousConfigDir: string | undefined
let previousOriginalCwd: string
let previousCwd: string
let tempRoot: string
let activeCwd: string
beforeEach(async () => {
await acquireSharedMutationLock('utils/sandbox/sandbox-adapter.test.ts')
previousConfigDir = process.env.CLAUDE_CONFIG_DIR
previousOriginalCwd = getOriginalCwd()
previousCwd = getCwdState()
tempRoot = await mkdtemp(join(tmpdir(), 'openclaude-sandbox-adapter-'))
const originalCwd = join(tempRoot, 'original-project')
activeCwd = join(tempRoot, 'active-project')
process.env.CLAUDE_CONFIG_DIR = join(tempRoot, 'config')
resetSettingsCache()
setOriginalCwd(originalCwd)
setCwdState(activeCwd)
})
afterEach(async () => {
try {
if (previousConfigDir === undefined) {
delete process.env.CLAUDE_CONFIG_DIR
} else {
process.env.CLAUDE_CONFIG_DIR = previousConfigDir
}
setOriginalCwd(previousOriginalCwd)
setCwdState(previousCwd)
resetSettingsCache()
await rm(tempRoot, { recursive: true, force: true })
} finally {
releaseSharedMutationLock()
}
})
test('denies canonical OpenClaude settings files in changed cwd', () => {
const config = convertToSandboxRuntimeConfig({} as SettingsJson)
expect(config.filesystem.denyWrite).toContain(
resolve(activeCwd, '.openclaude', 'settings.json'),
)
expect(config.filesystem.denyWrite).toContain(
resolve(activeCwd, '.openclaude', 'settings.local.json'),
)
expect(config.filesystem.denyWrite).toContain(
resolve(activeCwd, '.claude', 'settings.json'),
)
expect(config.filesystem.denyWrite).toContain(
resolve(activeCwd, '.claude', 'settings.local.json'),
)
})
})
+11 -2
View File
@@ -37,6 +37,7 @@ import { SETTING_SOURCES, type SettingSource } from '../settings/constants.js'
import { getManagedSettingsDropInDir } from '../settings/managedPath.js'
import {
getInitialSettings,
getRelativeSettingsFilePathForSource,
getSettings_DEPRECATED,
getSettingsFilePathForSource,
getSettingsForSource,
@@ -145,6 +146,15 @@ export function resolveSandboxFilesystemPath(
return expandPath(pattern, getSettingsRootPathForSource(source))
}
function getCurrentCwdSettingsDenyWritePaths(cwd: string): string[] {
return [
resolve(cwd, '.claude', 'settings.json'),
resolve(cwd, '.claude', 'settings.local.json'),
resolve(cwd, getRelativeSettingsFilePathForSource('projectSettings')),
resolve(cwd, getRelativeSettingsFilePathForSource('localSettings')),
]
}
/**
* Check if only managed sandbox domains should be used.
* This is true when policySettings has sandbox.network.allowManagedDomainsOnly: true
@@ -240,8 +250,7 @@ export function convertToSandboxRuntimeConfig(
const cwd = getCwdState()
const originalCwd = getOriginalCwd()
if (cwd !== originalCwd) {
denyWrite.push(resolve(cwd, '.claude', 'settings.json'))
denyWrite.push(resolve(cwd, '.claude', 'settings.local.json'))
denyWrite.push(...getCurrentCwdSettingsDenyWritePaths(cwd))
}
// Block writes to .claude/skills in both original and current working directories.
+1 -1
View File
@@ -109,7 +109,7 @@ export async function processSessionStartHooks(
errorMessage.includes('schema')
) {
userGuidance =
'This appears to be a configuration issue. Check your plugin settings in .claude/settings.json'
'This appears to be a configuration issue. Check your plugin settings in .openclaude/settings.json'
} else {
userGuidance =
'Please fix the plugin configuration or remove problematic plugins from your settings.'
+2 -2
View File
@@ -14,8 +14,8 @@ type SettingsChangeDetectorModule = typeof import('./changeDetector.js') & {
const pathsBySource: Record<SettingSource, string | null> = {
userSettings: normalize('/tmp/openclaude/user/settings.json'),
projectSettings: normalize('/tmp/openclaude/project/.claude/settings.json'),
localSettings: normalize('/tmp/openclaude/project/.claude/settings.local.json'),
projectSettings: normalize('/tmp/openclaude/project/.openclaude/settings.json'),
localSettings: normalize('/tmp/openclaude/project/.openclaude/settings.local.json'),
flagSettings: null,
policySettings: normalize('/tmp/openclaude/managed/managed-settings.json'),
}
+1 -1
View File
@@ -232,7 +232,7 @@ function parseSettingsFileUncached(path: string): {
/**
* Get the absolute path to the associated file root for a given settings source
* (e.g. for $PROJ_DIR/.claude/settings.json, returns $PROJ_DIR)
* (e.g. for $PROJ_DIR/.openclaude/settings.json, returns $PROJ_DIR)
* @param source The source of the settings
* @returns The root path of the settings file
*/
+3 -3
View File
@@ -218,7 +218,7 @@ export const DeniedMcpServerEntrySchema = lazySchema(() =>
*
* BACKWARD COMPATIBILITY NOTICE
*
* This schema defines the structure of user settings files (.claude/settings.json).
* This schema defines the structure of user settings files (~/.openclaude/settings.json).
* We support backward-compatible changes! Here's how:
*
* ALLOWED CHANGES:
@@ -619,7 +619,7 @@ export const SettingsSchema = lazySchema(() =>
})
.optional()
.describe(
'Additional marketplaces to make available for this repository. Typically used in repository .claude/settings.json to ensure team members have required plugin sources.',
'Additional marketplaces to make available for this repository. Typically used in repository .openclaude/settings.json to ensure team members have required plugin sources.',
),
// Enterprise strict list of allowed marketplace sources (policy settings only)
// When set, ONLY these exact sources can be added. Check happens BEFORE download.
@@ -1034,7 +1034,7 @@ export const SettingsSchema = lazySchema(() =>
.string()
.optional()
.describe(
'Custom directory path for auto-memory storage. Supports ~/ prefix for home directory expansion. Ignored if set in projectSettings (checked-in .claude/settings.json) for security. When unset, defaults to ~/.claude/projects/<sanitized-cwd>/memory/.',
'Custom directory path for auto-memory storage. Supports ~/ prefix for home directory expansion. Ignored if set in projectSettings (checked-in .openclaude/settings.json) for security. When unset, defaults to ~/.openclaude/projects/<sanitized-cwd>/memory/.',
),
autoDreamEnabled: z
.boolean()
+4 -4
View File
@@ -8,17 +8,17 @@ export interface SettingsFile {
export const settingsFiles: SettingsFile[] = [
{
path: '~/.claude/settings.json',
path: '~/.openclaude/settings.json',
scope: 'user',
notes: 'Global settings for every project on the machine.',
},
{
path: '.claude/settings.json',
path: '.openclaude/settings.json',
scope: 'project',
notes: 'Shared project settings, committed to the repo.',
},
{
path: '.claude/settings.local.json',
path: '.openclaude/settings.local.json',
scope: 'local',
notes: 'Per-machine overrides for one project; typically gitignored.',
},
@@ -68,7 +68,7 @@ export const envVars: EnvVar[] = [
{ name: 'MIMO_API_KEY', description: 'Xiaomi MiMo API key.' },
{ name: 'OPENCODE_API_KEY', description: 'OpenCode Zen / Go gateway key.' },
{ name: 'GITHUB_TOKEN', description: 'GitHub token for GitHub Models and PR workflows.' },
{ name: 'CLAUDE_CONFIG_DIR', description: 'Override the config directory (default ~/.claude).' },
{ name: 'CLAUDE_CONFIG_DIR', description: 'Override the config directory (default ~/.openclaude).' },
{ name: 'HTTP_PROXY / HTTPS_PROXY', description: 'Route API traffic through a proxy.' },
{ name: 'NODE_EXTRA_CA_CERTS', description: 'Extra CA certificates for corporate TLS interception.' },
{ name: 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC', description: 'Disable non-essential network traffic.' },