π File detail
utils/settings/types.ts
π― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes EnvironmentVariablesSchema, PermissionsSchema, ExtraKnownMarketplaceSchema, AllowedMcpServerEntrySchema, and DeniedMcpServerEntrySchema (and more) β mainly types, interfaces, or factory objects. Dependencies touch bun:bundle and schema validation. It composes internal code from entrypoints, envUtils, lazySchema, permissions, and plugins (relative imports).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
import { feature } from 'bun:bundle' import { z } from 'zod/v4' import { SandboxSettingsSchema } from '../../entrypoints/sandboxTypes.js' import { isEnvTruthy } from '../envUtils.js' import { lazySchema } from '../lazySchema.js'
π€ Exports (heuristic)
EnvironmentVariablesSchemaPermissionsSchemaExtraKnownMarketplaceSchemaAllowedMcpServerEntrySchemaDeniedMcpServerEntrySchemaCUSTOMIZATION_SURFACESSettingsSchemaPluginHookMatcherSkillHookMatcherAllowedMcpServerEntryDeniedMcpServerEntrySettingsJsonisMcpServerNameEntryisMcpServerCommandEntryisMcpServerUrlEntryUserConfigValuesPluginConfigtype AgentHooktype BashCommandHooktype HookCommandHookCommandSchematype HookMatcherHookMatcherSchemaHooksSchematype HooksSettingstype HttpHooktype PromptHook
π External import roots
Package roots from from "β¦" (relative paths omitted).
bun:bundlezod
π₯οΈ Source preview
import { feature } from 'bun:bundle'
import { z } from 'zod/v4'
import { SandboxSettingsSchema } from '../../entrypoints/sandboxTypes.js'
import { isEnvTruthy } from '../envUtils.js'
import { lazySchema } from '../lazySchema.js'
import {
EXTERNAL_PERMISSION_MODES,
PERMISSION_MODES,
} from '../permissions/PermissionMode.js'
import { MarketplaceSourceSchema } from '../plugins/schemas.js'
import { CLAUDE_CODE_SETTINGS_SCHEMA_URL } from './constants.js'
import { PermissionRuleSchema } from './permissionValidation.js'
// Re-export hook schemas and types from centralized location for backward compatibility
export {
type AgentHook,
type BashCommandHook,
type HookCommand,
HookCommandSchema,
type HookMatcher,
HookMatcherSchema,
HooksSchema,
type HooksSettings,
type HttpHook,
type PromptHook,
} from '../../schemas/hooks.js'
// Also import for use within this file
import { type HookCommand, HooksSchema } from '../../schemas/hooks.js'
import { count } from '../array.js'
/**
* Schema for environment variables
*/
export const EnvironmentVariablesSchema = lazySchema(() =>
z.record(z.string(), z.coerce.string()),
)
/**
* Schema for permissions section
*/
export const PermissionsSchema = lazySchema(() =>
z
.object({
allow: z
.array(PermissionRuleSchema())
.optional()
.describe('List of permission rules for allowed operations'),
deny: z
.array(PermissionRuleSchema())
.optional()
.describe('List of permission rules for denied operations'),
ask: z
.array(PermissionRuleSchema())
.optional()
.describe(
'List of permission rules that should always prompt for confirmation',
),
defaultMode: z
.enum(
feature('TRANSCRIPT_CLASSIFIER')
? PERMISSION_MODES
: EXTERNAL_PERMISSION_MODES,
)
.optional()
.describe('Default permission mode when Claude Code needs access'),
disableBypassPermissionsMode: z
.enum(['disable'])
.optional()
.describe('Disable the ability to bypass permission prompts'),
...(feature('TRANSCRIPT_CLASSIFIER')
? {
disableAutoMode: z
.enum(['disable'])
.optional()
.describe('Disable auto mode'),
}
: {}),
additionalDirectories: z
.array(z.string())
.optional()
.describe('Additional directories to include in the permission scope'),
})
.passthrough(),
)
/**
* Schema for extra marketplaces defined in repository settings
* Same as KnownMarketplace but without lastUpdated (which is managed automatically)
*/
export const ExtraKnownMarketplaceSchema = lazySchema(() =>
z.object({
source: MarketplaceSourceSchema().describe(
'Where to fetch the marketplace from',
),
installLocation: z
.string()
.optional()
.describe(
'Local cache path where marketplace manifest is stored (auto-generated if not provided)',
),
autoUpdate: z
.boolean()
.optional()
.describe(
'Whether to automatically update this marketplace and its installed plugins on startup',
),
}),
)
/**
* Schema for allowed MCP server entry in enterprise allowlist.
* Supports matching by serverName, serverCommand, or serverUrl (mutually exclusive).
*/
export const AllowedMcpServerEntrySchema = lazySchema(() =>
z
.object({
serverName: z
.string()
.regex(
/^[a-zA-Z0-9_-]+$/,
'Server name can only contain letters, numbers, hyphens, and underscores',
)
.optional()
.describe('Name of the MCP server that users are allowed to configure'),
serverCommand: z
.array(z.string())
.min(1, 'Server command must have at least one element (the command)')
.optional()
.describe(
'Command array [command, ...args] to match exactly for allowed stdio servers',
),
serverUrl: z
.string()
.optional()
.describe(
'URL pattern with wildcard support (e.g., "https://*.example.com/*") for allowed remote MCP servers',
),
// Future extensibility: allowedTransports, requiredArgs, maxInstances, etc.
})
.refine(
data => {
const defined = count(
[
data.serverName !== undefined,
data.serverCommand !== undefined,
data.serverUrl !== undefined,
],
Boolean,
)
return defined === 1
},
{
message:
'Entry must have exactly one of "serverName", "serverCommand", or "serverUrl"',
},
),
)
/**
* Schema for denied MCP server entry in enterprise denylist.
* Supports matching by serverName, serverCommand, or serverUrl (mutually exclusive).
*/
export const DeniedMcpServerEntrySchema = lazySchema(() =>
z
.object({
serverName: z
.string()
.regex(
/^[a-zA-Z0-9_-]+$/,
'Server name can only contain letters, numbers, hyphens, and underscores',
)
.optional()
.describe('Name of the MCP server that is explicitly blocked'),
serverCommand: z
.array(z.string())
.min(1, 'Server command must have at least one element (the command)')
.optional()
.describe(
'Command array [command, ...args] to match exactly for blocked stdio servers',
),
serverUrl: z
.string()
.optional()
.describe(
'URL pattern with wildcard support (e.g., "https://*.example.com/*") for blocked remote MCP servers',
),
// Future extensibility: reason, blockedSince, etc.
})
.refine(
data => {
const defined = count(
[
data.serverName !== undefined,
data.serverCommand !== undefined,
data.serverUrl !== undefined,
],
Boolean,
)
return defined === 1
},
{
message:
'Entry must have exactly one of "serverName", "serverCommand", or "serverUrl"',
},
),
)
/**
* Unified schema for settings files
*
* β οΈ BACKWARD COMPATIBILITY NOTICE β οΈ
*
* This schema defines the structure of user settings files (.claude/settings.json).
* We support backward-compatible changes! Here's how:
*
* β
ALLOWED CHANGES:
* - Adding new optional fields (always use .optional())
* - Adding new enum values (keeping existing ones)
* - Adding new properties to objects
* - Making validation more permissive
* - Using union types for gradual migration (e.g., z.union([oldType, newType]))
*
* β BREAKING CHANGES TO AVOID:
* - Removing fields (mark as deprecated instead)
* - Removing enum values
* - Making optional fields required
* - Making types more restrictive
* - Renaming fields without keeping the old name
*
* TO ENSURE BACKWARD COMPATIBILITY:
* 1. Run: npm run test:file -- test/utils/settings/backward-compatibility.test.ts
* 2. If tests fail, you've introduced a breaking change
* 3. When adding new fields, add a test to BACKWARD_COMPATIBILITY_CONFIGS
*
* The settings system handles backward compatibility automatically:
* - When updating settings, invalid fields are preserved in the file (see settings.ts lines 233-249)
* - Type coercion via z.coerce (e.g., env vars convert numbers to strings)
* - .passthrough() preserves unknown fields in permissions object
* - Invalid settings are simply not used, but remain in the file to be fixed by the user
*/
/**
* Surfaces lockable by `strictPluginOnlyCustomization`. Exported so the
* schema preprocess (below) and the runtime helper (pluginOnlyPolicy.ts)
* share one source of truth.
*/
export const CUSTOMIZATION_SURFACES = [
'skills',
'agents',
'hooks',
'mcp',
] as const
export const SettingsSchema = lazySchema(() =>
z
.object({
$schema: z
.literal(CLAUDE_CODE_SETTINGS_SCHEMA_URL)
.optional()
.describe('JSON Schema reference for Claude Code settings'),
apiKeyHelper: z
.string()
.optional()
.describe('Path to a script that outputs authentication values'),
awsCredentialExport: z
.string()
.optional()
.describe('Path to a script that exports AWS credentials'),
awsAuthRefresh: z
.string()
.optional()
.describe('Path to a script that refreshes AWS authentication'),
gcpAuthRefresh: z
.string()
.optional()
.describe(
'Command to refresh GCP authentication (e.g., gcloud auth application-default login)',
),
// Gated so the SDK generator (which runs without CLAUDE_CODE_ENABLE_XAA)
// doesn't surface this in GlobalClaudeSettings. Read via getXaaIdpSettings().
// .passthrough() on the outer object keeps an existing settings.json key
// alive across env-var-off sessions β it's just not schema-validated then.
...(isEnvTruthy(process.env.CLAUDE_CODE_ENABLE_XAA)
? {
xaaIdp: z
.object({
issuer: z
.string()
.url()
.describe('IdP issuer URL for OIDC discovery'),
clientId: z
.string()
.describe("Claude Code's client_id registered at the IdP"),
callbackPort: z
.number()
.int()
.positive()
.optional()
.describe(
'Fixed loopback callback port for the IdP OIDC login. ' +
'Only needed if the IdP does not honor RFC 8252 port-any matching.',
),
})
.optional()
.describe(
'XAA (SEP-990) IdP connection. Configure once; all XAA-enabled MCP servers reuse this.',
),
}
: {}),
fileSuggestion: z
.object({
type: z.literal('command'),
command: z.string(),
})
.optional()
.describe('Custom file suggestion configuration for @ mentions'),
respectGitignore: z
.boolean()
.optional()
.describe(
'Whether file picker should respect .gitignore files (default: true). ' +
'Note: .ignore files are always respected.',
),
cleanupPeriodDays: z
.number()
.nonnegative()
.int()
.optional()
.describe(
'Number of days to retain chat transcripts (default: 30). Setting to 0 disables session persistence entirely: no transcripts are written and existing transcripts are deleted at startup.',
),
env: EnvironmentVariablesSchema()
.optional()
.describe('Environment variables to set for Claude Code sessions'),
// Attribution for commits and PRs
attribution: z
.object({
commit: z
.string()
.optional()
.describe(
'Attribution text for git commits, including any trailers. ' +
'Empty string hides attribution.',
),
pr: z
.string()
.optional()
.describe(
'Attribution text for pull request descriptions. ' +
'Empty string hides attribution.',
),
})
.optional()
.describe(
'Customize attribution text for commits and PRs. ' +
'Each field defaults to the standard Claude Code attribution if not set.',
),
includeCoAuthoredBy: z
.boolean()
.optional()
.describe(
'Deprecated: Use attribution instead. ' +
"Whether to include Claude's co-authored by attribution in commits and PRs (defaults to true)",
),
includeGitInstructions: z
.boolean()
.optional()
.describe(
"Include built-in commit and PR workflow instructions in Claude's system prompt (default: true)",
),
permissions: PermissionsSchema()
.optional()
.describe('Tool usage permissions configuration'),
model: z
.string()
.optional()
.describe('Override the default model used by Claude Code'),
// Enterprise allowlist of models
availableModels: z
.array(z.string())
.optional()
.describe(
'Allowlist of models that users can select. ' +
'Accepts family aliases ("opus" allows any opus version), ' +
'version prefixes ("opus-4-5" allows only that version), ' +
'and full model IDs. ' +
'If undefined, all models are available. If empty array, only the default model is available. ' +
'Typically set in managed settings by enterprise administrators.',
),
modelOverrides: z
.record(z.string(), z.string())
.optional()
.describe(
'Override mapping from Anthropic model ID (e.g. "claude-opus-4-6") to provider-specific ' +
'model ID (e.g. a Bedrock inference profile ARN). Typically set in managed settings by ' +
'enterprise administrators.',
),
// Whether to automatically approve all MCP servers in the project
enableAllProjectMcpServers: z
.boolean()
.optional()
.describe(
'Whether to automatically approve all MCP servers in the project',
),
// List of approved MCP servers from .mcp.json
enabledMcpjsonServers: z
.array(z.string())
.optional()
.describe('List of approved MCP servers from .mcp.json'),
// List of rejected MCP servers from .mcp.json
disabledMcpjsonServers: z
.array(z.string())
.optional()
.describe('List of rejected MCP servers from .mcp.json'),
// Enterprise allowlist of MCP servers
allowedMcpServers: z
.array(AllowedMcpServerEntrySchema())
.optional()
.describe(
'Enterprise allowlist of MCP servers that can be used. ' +
'Applies to all scopes including enterprise servers from managed-mcp.json. ' +
'If undefined, all servers are allowed. If empty array, no servers are allowed. ' +
'Denylist takes precedence - if a server is on both lists, it is denied.',
),
// Enterprise denylist of MCP servers
deniedMcpServers: z
.array(DeniedMcpServerEntrySchema())
.optional()
.describe(
'Enterprise denylist of MCP servers that are explicitly blocked. ' +
'If a server is on the denylist, it will be blocked across all scopes including enterprise. ' +
'Denylist takes precedence over allowlist - if a server is on both lists, it is denied.',
),
hooks: HooksSchema()
.optional()
.describe('Custom commands to run before/after tool executions'),
worktree: z
.object({
symlinkDirectories: z
.array(z.string())
.optional()
.describe(
'Directories to symlink from main repository to worktrees to avoid disk bloat. ' +
'Must be explicitly configured - no directories are symlinked by default. ' +
'Common examples: "node_modules", ".cache", ".bin"',
),
sparsePaths: z
.array(z.string())
.optional()
.describe(
'Directories to include when creating worktrees, via git sparse-checkout (cone mode). ' +
'Dramatically faster in large monorepos β only the listed paths are written to disk.',
),
})
.optional()
.describe('Git worktree configuration for --worktree flag.'),
// Whether to disable all hooks and statusLine
disableAllHooks: z
.boolean()
.optional()
.describe('Disable all hooks and statusLine execution'),
// Which shell backs input-box `!` (see docs/design/ps-shell-selection.md Β§4.2)
defaultShell: z
.enum(['bash', 'powershell'])
.optional()
.describe(
'Default shell for input-box ! commands. ' +
"Defaults to 'bash' on all platforms (no Windows auto-flip).",
),
// Only run hooks defined in managed settings (managed-settings.json)
allowManagedHooksOnly: z
.boolean()
.optional()
.describe(
'When true (and set in managed settings), only hooks from managed settings run. ' +
'User, project, and local hooks are ignored.',
),
// Allowlist of URL patterns HTTP hooks may target (follows allowedMcpServers precedent)
allowedHttpHookUrls: z
.array(z.string())
.optional()
.describe(
'Allowlist of URL patterns that HTTP hooks may target. ' +
'Supports * as a wildcard (e.g. "https://hooks.example.com/*"). ' +
'When set, HTTP hooks with non-matching URLs are blocked. ' +
'If undefined, all URLs are allowed. If empty array, no HTTP hooks are allowed. ' +
'Arrays merge across settings sources (same semantics as allowedMcpServers).',
),
// Allowlist of env var names HTTP hooks may interpolate into headers
httpHookAllowedEnvVars: z
.array(z.string())
.optional()
.describe(
'Allowlist of environment variable names HTTP hooks may interpolate into headers. ' +
"When set, each hook's effective allowedEnvVars is the intersection with this list. " +
'If undefined, no restriction is applied. ' +
'Arrays merge across settings sources (same semantics as allowedMcpServers).',
),
// Only use permission rules defined in managed settings (managed-settings.json)
allowManagedPermissionRulesOnly: z
.boolean()
.optional()
.describe(
'When true (and set in managed settings), only permission rules (allow/deny/ask) from managed settings are respected. ' +
'User, project, local, and CLI argument permission rules are ignored.',
),
// Only read MCP allowlist policy from managed settings
allowManagedMcpServersOnly: z
.boolean()
.optional()
.describe(
'When true (and set in managed settings), allowedMcpServers is only read from managed settings. ' +
'deniedMcpServers still merges from all sources, so users can deny servers for themselves. ' +
'Users can still add their own MCP servers, but only the admin-defined allowlist applies.',
),
// Force customizations through plugins only (LinkedIn ask via GTM)
strictPluginOnlyCustomization: z
.preprocess(
// Forwards-compat: drop unknown surface names so a future enum
// value (e.g. 'commands') doesn't fail safeParse and null out the
// ENTIRE managed-settings file (settings.ts:101). ["skills",
// "commands"] on an old client β ["skills"] β locks what it knows,
// ignores what it doesn't. Degrades to less-locked, never to
// everything-unlocked.
v =>
Array.isArray(v)
? v.filter(x =>
(CUSTOMIZATION_SURFACES as readonly string[]).includes(x),
)
: v,
z.union([z.boolean(), z.array(z.enum(CUSTOMIZATION_SURFACES))]),
)
.optional()
// Non-array invalid values ("skills" string, {object}) pass through
// the preprocess unchanged and would fail the union β null the whole
// managed-settings file. .catch drops the field to undefined instead.
// Degrades to unlocked-for-this-field, never to everything-broken.
// Doctor flags the raw value.
.catch(undefined)
.describe(
'When set in managed settings, blocks non-plugin customization sources for the listed surfaces. ' +
'Array form locks specific surfaces (e.g. ["skills", "hooks"]); `true` locks all four; `false` is an explicit no-op. ' +
'Blocked: ~/.claude/{surface}/, .claude/{surface}/ (project), settings.json hooks, .mcp.json. ' +
'NOT blocked: managed (policySettings) sources, plugin-provided customizations. ' +
'Composes with strictKnownMarketplaces for end-to-end admin control β plugins gated by ' +
'marketplace allowlist, everything else blocked here.',
),
// Status line for custom status line display
statusLine: z
.object({
type: z.literal('command'),
command: z.string(),
padding: z.number().optional(),
})
.optional()
.describe('Custom status line display configuration'),
// Enabled plugins using marketplace-first format
enabledPlugins: z
.record(
z.string(),
z.union([z.array(z.string()), z.boolean(), z.undefined()]),
)
.optional()
.describe(
'Enabled plugins using plugin-id@marketplace-id format. Example: { "formatter@anthropic-tools": true }. Also supports extended format with version constraints.',
),
// Extra marketplaces for this repository (usually for project settings)
extraKnownMarketplaces: z
.record(z.string(), ExtraKnownMarketplaceSchema())
.check(ctx => {
// For settings sources, key must equal source.name. diffMarketplaces
// looks up materialized state by dict key; addMarketplaceSource stores
// under marketplace.name (= source.name for settings). A mismatch means
// the reconciler never converges β every session: key-lookup misses β
// 'missing' β source-idempotency returns alreadyMaterialized but
// installed++ anyway β pointless cache clears. For github/git/url the
// name comes from a fetched marketplace.json (mismatch is expected and
// benign); for settings, both key and name are user-authored in the
// same JSON object.
for (const [key, entry] of Object.entries(ctx.value)) {
if (
entry.source.source === 'settings' &&
entry.source.name !== key
) {
ctx.issues.push({
code: 'custom',
input: entry.source.name,
path: [key, 'source', 'name'],
message:
`Settings-sourced marketplace name must match its extraKnownMarketplaces key ` +
`(got key "${key}" but source.name "${entry.source.name}")`,
})
}
}
})
.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.',
),
// Enterprise strict list of allowed marketplace sources (policy settings only)
// When set, ONLY these exact sources can be added. Check happens BEFORE download.
strictKnownMarketplaces: z
.array(MarketplaceSourceSchema())
.optional()
.describe(
'Enterprise strict list of allowed marketplace sources. When set in managed settings, ' +
'ONLY these exact sources can be added as marketplaces. The check happens BEFORE ' +
'downloading, so blocked sources never touch the filesystem. ' +
'Note: this is a policy gate only β it does NOT register marketplaces. ' +
'To pre-register allowed marketplaces for users, also set extraKnownMarketplaces.',
),
// Enterprise blocklist of marketplace sources (policy settings only)
// When set, these exact sources are blocked. Check happens BEFORE download.
blockedMarketplaces: z
.array(MarketplaceSourceSchema())
.optional()
.describe(
'Enterprise blocklist of marketplace sources. When set in managed settings, ' +
'these exact sources are blocked from being added as marketplaces. The check happens BEFORE ' +
'downloading, so blocked sources never touch the filesystem.',
),
// Force a specific login method: 'claudeai' for Claude Pro/Max, 'console' for Console billing
forceLoginMethod: z
.enum(['claudeai', 'console'])
.optional()
.describe(
'Force a specific login method: "claudeai" for Claude Pro/Max, "console" for Console billing',
),
// Organization UUID to use for OAuth login (will be added as URL param to authorization URL)
forceLoginOrgUUID: z
.string()
.optional()
.describe('Organization UUID to use for OAuth login'),
otelHeadersHelper: z
.string()
.optional()
.describe('Path to a script that outputs OpenTelemetry headers'),
outputStyle: z
.string()
.optional()
.describe('Controls the output style for assistant responses'),
language: z
.string()
.optional()
.describe(
'Preferred language for Claude responses and voice dictation (e.g., "japanese", "spanish")',
),
skipWebFetchPreflight: z
.boolean()
.optional()
.describe(
'Skip the WebFetch blocklist check for enterprise environments with restrictive security policies',
),
sandbox: SandboxSettingsSchema().optional(),
feedbackSurveyRate: z
.number()
.min(0)
.max(1)
.optional()
.describe(
'Probability (0β1) that the session quality survey appears when eligible. 0.05 is a reasonable starting point.',
),
spinnerTipsEnabled: z
.boolean()
.optional()
.describe('Whether to show tips in the spinner'),
spinnerVerbs: z
.object({
mode: z.enum(['append', 'replace']),
verbs: z.array(z.string()),
})
.optional()
.describe(
'Customize spinner verbs. mode: "append" adds verbs to defaults, "replace" uses only your verbs.',
),
spinnerTipsOverride: z
.object({
excludeDefault: z.boolean().optional(),
tips: z.array(z.string()),
})
.optional()
.describe(
'Override spinner tips. tips: array of tip strings. excludeDefault: if true, only show custom tips (default: false).',
),
syntaxHighlightingDisabled: z
.boolean()
.optional()
.describe('Whether to disable syntax highlighting in diffs'),
terminalTitleFromRename: z
.boolean()
.optional()
.describe(
'Whether /rename updates the terminal tab title (defaults to true). Set to false to keep auto-generated topic titles.',
),
alwaysThinkingEnabled: z
.boolean()
.optional()
.describe(
'When false, thinking is disabled. When absent or true, thinking is ' +
'enabled automatically for supported models.',
),
effortLevel: z
.enum(
process.env.USER_TYPE === 'ant'
? ['low', 'medium', 'high', 'max']
: ['low', 'medium', 'high'],
)
.optional()
.catch(undefined)
.describe('Persisted effort level for supported models.'),
advisorModel: z
.string()
.optional()
.describe('Advisor model for the server-side advisor tool.'),
fastMode: z
.boolean()
.optional()
.describe(
'When true, fast mode is enabled. When absent or false, fast mode is off.',
),
fastModePerSessionOptIn: z
.boolean()
.optional()
.describe(
'When true, fast mode does not persist across sessions. Each session starts with fast mode off.',
),
promptSuggestionEnabled: z
.boolean()
.optional()
.describe(
'When false, prompt suggestions are disabled. When absent or true, ' +
'prompt suggestions are enabled.',
),
showClearContextOnPlanAccept: z
.boolean()
.optional()
.describe(
'When true, the plan-approval dialog offers a "clear context" option. Defaults to false.',
),
agent: z
.string()
.optional()
.describe(
'Name of an agent (built-in or custom) to use for the main thread. ' +
"Applies the agent's system prompt, tool restrictions, and model.",
),
companyAnnouncements: z
.array(z.string())
.optional()
.describe(
'Company announcements to display at startup (one will be randomly selected if multiple are provided)',
),
pluginConfigs: z
.record(
z.string(),
z.object({
mcpServers: z
.record(
z.string(),
z.record(
z.string(),
z.union([
z.string(),
z.number(),
z.boolean(),
z.array(z.string()),
]),
),
)
.optional()
.describe(
'User configuration values for MCP servers keyed by server name',
),
options: z
.record(
z.string(),
z.union([
z.string(),
z.number(),
z.boolean(),
z.array(z.string()),
]),
)
.optional()
.describe(
'Non-sensitive option values from plugin manifest userConfig, keyed by option name. Sensitive values go to secure storage instead.',
),
}),
)
.optional()
.describe(
'Per-plugin configuration including MCP server user configs, keyed by plugin ID (plugin@marketplace format)',
),
remote: z
.object({
defaultEnvironmentId: z
.string()
.optional()
.describe('Default environment ID to use for remote sessions'),
})
.optional()
.describe('Remote session configuration'),
autoUpdatesChannel: z
.enum(['latest', 'stable'])
.optional()
.describe('Release channel for auto-updates (latest or stable)'),
...(feature('LODESTONE')
? {
disableDeepLinkRegistration: z
.enum(['disable'])
.optional()
.describe(
'Prevent claude-cli:// protocol handler registration with the OS',
),
}
: {}),
minimumVersion: z
.string()
.optional()
.describe(
'Minimum version to stay on - prevents downgrades when switching to stable channel',
),
plansDirectory: z
.string()
.optional()
.describe(
'Custom directory for plan files, relative to project root. ' +
'If not set, defaults to ~/.claude/plans/',
),
...(process.env.USER_TYPE === 'ant'
? {
classifierPermissionsEnabled: z
.boolean()
.optional()
.describe(
'Enable AI-based classification for Bash(prompt:...) permission rules',
),
}
: {}),
...(feature('PROACTIVE') || feature('KAIROS')
? {
minSleepDurationMs: z
.number()
.nonnegative()
.int()
.optional()
.describe(
'Minimum duration in milliseconds that the Sleep tool must sleep for. ' +
'Useful for throttling proactive tick frequency.',
),
maxSleepDurationMs: z
.number()
.int()
.min(-1)
.optional()
.describe(
'Maximum duration in milliseconds that the Sleep tool can sleep for. ' +
'Set to -1 for indefinite sleep (waits for user input). ' +
'Useful for limiting idle time in remote/managed environments.',
),
}
: {}),
...(feature('VOICE_MODE')
? {
voiceEnabled: z
.boolean()
.optional()
.describe('Enable voice mode (hold-to-talk dictation)'),
}
: {}),
...(feature('KAIROS')
? {
assistant: z
.boolean()
.optional()
.describe(
'Start Claude in assistant mode (custom system prompt, brief view, scheduled check-in skills)',
),
assistantName: z
.string()
.optional()
.describe(
'Display name for the assistant, shown in the claude.ai session list',
),
}
: {}),
// Teams/Enterprise opt-IN for channel notifications. Default OFF.
// MCP servers that declare the claude/channel capability can push
// inbound messages into the conversation; for managed orgs this only
// works when explicitly enabled. Which servers can connect at all is
// still governed by allowedMcpServers/deniedMcpServers. Not
// feature-spread: KAIROS_CHANNELS is external:true, and the spread
// wrecks type inference for allowedChannelPlugins (the .passthrough()
// catch-all gives {} instead of the array type).
channelsEnabled: z
.boolean()
.optional()
.describe(
'Teams/Enterprise opt-in for channel notifications (MCP servers with the ' +
'claude/channel capability pushing inbound messages). Default off. ' +
'Set true to allow; users then select servers via --channels.',
),
// Org-level channel plugin allowlist. When set, REPLACES the
// Anthropic ledger β admin owns the trust decision. Undefined means
// fall back to the ledger. Plugin-only entry shape (same as the
// ledger); server-kind entries still need the dev flag.
allowedChannelPlugins: z
.array(
z.object({
marketplace: z.string(),
plugin: z.string(),
}),
)
.optional()
.describe(
'Teams/Enterprise allowlist of channel plugins. When set, ' +
'replaces the default Anthropic allowlist β admins decide which ' +
'plugins may push inbound messages. Undefined falls back to the default. ' +
'Requires channelsEnabled: true.',
),
...(feature('KAIROS') || feature('KAIROS_BRIEF')
? {
defaultView: z
.enum(['chat', 'transcript'])
.optional()
.describe(
'Default transcript view: chat (SendUserMessage checkpoints only) or transcript (full)',
),
}
: {}),
prefersReducedMotion: z
.boolean()
.optional()
.describe(
'Reduce or disable animations for accessibility (spinner shimmer, flash effects, etc.)',
),
autoMemoryEnabled: z
.boolean()
.optional()
.describe(
'Enable auto-memory for this project. When false, Claude will not read from or write to the auto-memory directory.',
),
autoMemoryDirectory: z
.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/.',
),
autoDreamEnabled: z
.boolean()
.optional()
.describe(
'Enable background memory consolidation (auto-dream). When set, overrides the server-side default.',
),
showThinkingSummaries: z
.boolean()
.optional()
.describe(
'Show thinking summaries in the transcript view (ctrl+o). Default: false.',
),
skipDangerousModePermissionPrompt: z
.boolean()
.optional()
.describe(
'Whether the user has accepted the bypass permissions mode dialog',
),
...(feature('TRANSCRIPT_CLASSIFIER')
? {
skipAutoPermissionPrompt: z
.boolean()
.optional()
.describe(
'Whether the user has accepted the auto mode opt-in dialog',
),
useAutoModeDuringPlan: z
.boolean()
.optional()
.describe(
'Whether plan mode uses auto mode semantics when auto mode is available (default: true)',
),
autoMode: z
.object({
allow: z
.array(z.string())
.optional()
.describe('Rules for the auto mode classifier allow section'),
soft_deny: z
.array(z.string())
.optional()
.describe('Rules for the auto mode classifier deny section'),
...(process.env.USER_TYPE === 'ant'
? {
// Back-compat alias for ant users; external users use soft_deny
deny: z.array(z.string()).optional(),
}
: {}),
environment: z
.array(z.string())
.optional()
.describe(
'Entries for the auto mode classifier environment section',
),
})
.optional()
.describe('Auto mode classifier prompt customization'),
}
: {}),
disableAutoMode: z
.enum(['disable'])
.optional()
.describe('Disable auto mode'),
sshConfigs: z
.array(
z.object({
id: z
.string()
.describe(
'Unique identifier for this SSH config. Used to match configs across settings sources.',
),
name: z.string().describe('Display name for the SSH connection'),
sshHost: z
.string()
.describe(
'SSH host in format "user@hostname" or "hostname", or a host alias from ~/.ssh/config',
),
sshPort: z
.number()
.int()
.optional()
.describe('SSH port (default: 22)'),
sshIdentityFile: z
.string()
.optional()
.describe('Path to SSH identity file (private key)'),
startDirectory: z
.string()
.optional()
.describe(
'Default working directory on the remote host. ' +
'Supports tilde expansion (e.g. ~/projects). ' +
'If not specified, defaults to the remote user home directory. ' +
'Can be overridden by the [dir] positional argument in `claude ssh <config> [dir]`.',
),
}),
)
.optional()
.describe(
'SSH connection configurations for remote environments. ' +
'Typically set in managed settings by enterprise administrators ' +
'to pre-configure SSH connections for team members.',
),
claudeMdExcludes: z
.array(z.string())
.optional()
.describe(
'Glob patterns or absolute paths of CLAUDE.md files to exclude from loading. ' +
'Patterns are matched against absolute file paths using picomatch. ' +
'Only applies to User, Project, and Local memory types (Managed/policy files cannot be excluded). ' +
'Examples: "/home/user/monorepo/CLAUDE.md", "**/code/CLAUDE.md", "**/some-dir/.claude/rules/**"',
),
pluginTrustMessage: z
.string()
.optional()
.describe(
'Custom message to append to the plugin trust warning shown before installation. ' +
'Only read from policy settings (managed-settings.json / MDM). ' +
'Useful for enterprise administrators to add organization-specific context ' +
'(e.g., "All plugins from our internal marketplace are vetted and approved.").',
),
})
.passthrough(),
)
/**
* Internal type for plugin hooks - includes plugin context for execution.
* Not a Zod schema since it's not user-facing (plugins provide native hooks).
*/
export type PluginHookMatcher = {
matcher?: string
hooks: HookCommand[]
pluginRoot: string
pluginName: string
pluginId: string // format: "pluginName@marketplaceName"
}
/**
* Internal type for skill hooks - includes skill context for execution.
* Not a Zod schema since it's not user-facing (skills provide native hooks).
*/
export type SkillHookMatcher = {
matcher?: string
hooks: HookCommand[]
skillRoot: string
skillName: string
}
export type AllowedMcpServerEntry = z.infer<
ReturnType<typeof AllowedMcpServerEntrySchema>
>
export type DeniedMcpServerEntry = z.infer<
ReturnType<typeof DeniedMcpServerEntrySchema>
>
export type SettingsJson = z.infer<ReturnType<typeof SettingsSchema>>
/**
* Type guard for MCP server entry with serverName
*/
export function isMcpServerNameEntry(
entry: AllowedMcpServerEntry | DeniedMcpServerEntry,
): entry is { serverName: string } {
return 'serverName' in entry && entry.serverName !== undefined
}
/**
* Type guard for MCP server entry with serverCommand
*/
export function isMcpServerCommandEntry(
entry: AllowedMcpServerEntry | DeniedMcpServerEntry,
): entry is { serverCommand: string[] } {
return 'serverCommand' in entry && entry.serverCommand !== undefined
}
/**
* Type guard for MCP server entry with serverUrl
*/
export function isMcpServerUrlEntry(
entry: AllowedMcpServerEntry | DeniedMcpServerEntry,
): entry is { serverUrl: string } {
return 'serverUrl' in entry && entry.serverUrl !== undefined
}
/**
* User configuration values for MCPB MCP servers
*/
export type UserConfigValues = Record<
string,
string | number | boolean | string[]
>
/**
* Plugin configuration stored in settings.json
*/
export type PluginConfig = {
mcpServers?: {
[serverName: string]: UserConfigValues
}
}