types.ts
1 import { feature } from 'bun:bundle' 2 import { z } from 'zod/v4' 3 import { SandboxSettingsSchema } from '../../entrypoints/sandboxTypes.js' 4 import { isEnvTruthy } from '../envUtils.js' 5 import { lazySchema } from '../lazySchema.js' 6 import { 7 EXTERNAL_PERMISSION_MODES, 8 PERMISSION_MODES, 9 } from '../permissions/PermissionMode.js' 10 import { MarketplaceSourceSchema } from '../plugins/schemas.js' 11 import { CLAUDE_CODE_SETTINGS_SCHEMA_URL } from './constants.js' 12 import { PermissionRuleSchema } from './permissionValidation.js' 13 14 // Re-export hook schemas and types from centralized location for backward compatibility 15 export { 16 type AgentHook, 17 type BashCommandHook, 18 type HookCommand, 19 HookCommandSchema, 20 type HookMatcher, 21 HookMatcherSchema, 22 HooksSchema, 23 type HooksSettings, 24 type HttpHook, 25 type PromptHook, 26 } from '../../schemas/hooks.js' 27 28 // Also import for use within this file 29 import { type HookCommand, HooksSchema } from '../../schemas/hooks.js' 30 import { count } from '../array.js' 31 32 /** 33 * Schema for environment variables 34 */ 35 export const EnvironmentVariablesSchema = lazySchema(() => 36 z.record(z.string(), z.coerce.string()), 37 ) 38 39 /** 40 * Schema for permissions section 41 */ 42 export const PermissionsSchema = lazySchema(() => 43 z 44 .object({ 45 allow: z 46 .array(PermissionRuleSchema()) 47 .optional() 48 .describe('List of permission rules for allowed operations'), 49 deny: z 50 .array(PermissionRuleSchema()) 51 .optional() 52 .describe('List of permission rules for denied operations'), 53 ask: z 54 .array(PermissionRuleSchema()) 55 .optional() 56 .describe( 57 'List of permission rules that should always prompt for confirmation', 58 ), 59 defaultMode: z 60 .enum( 61 feature('TRANSCRIPT_CLASSIFIER') 62 ? PERMISSION_MODES 63 : EXTERNAL_PERMISSION_MODES, 64 ) 65 .optional() 66 .describe('Default permission mode when Claude Code needs access'), 67 disableBypassPermissionsMode: z 68 .enum(['disable']) 69 .optional() 70 .describe('Disable the ability to bypass permission prompts'), 71 ...(feature('TRANSCRIPT_CLASSIFIER') 72 ? { 73 disableAutoMode: z 74 .enum(['disable']) 75 .optional() 76 .describe('Disable auto mode'), 77 } 78 : {}), 79 additionalDirectories: z 80 .array(z.string()) 81 .optional() 82 .describe('Additional directories to include in the permission scope'), 83 }) 84 .passthrough(), 85 ) 86 87 /** 88 * Schema for extra marketplaces defined in repository settings 89 * Same as KnownMarketplace but without lastUpdated (which is managed automatically) 90 */ 91 export const ExtraKnownMarketplaceSchema = lazySchema(() => 92 z.object({ 93 source: MarketplaceSourceSchema().describe( 94 'Where to fetch the marketplace from', 95 ), 96 installLocation: z 97 .string() 98 .optional() 99 .describe( 100 'Local cache path where marketplace manifest is stored (auto-generated if not provided)', 101 ), 102 autoUpdate: z 103 .boolean() 104 .optional() 105 .describe( 106 'Whether to automatically update this marketplace and its installed plugins on startup', 107 ), 108 }), 109 ) 110 111 /** 112 * Schema for allowed MCP server entry in enterprise allowlist. 113 * Supports matching by serverName, serverCommand, or serverUrl (mutually exclusive). 114 */ 115 export const AllowedMcpServerEntrySchema = lazySchema(() => 116 z 117 .object({ 118 serverName: z 119 .string() 120 .regex( 121 /^[a-zA-Z0-9_-]+$/, 122 'Server name can only contain letters, numbers, hyphens, and underscores', 123 ) 124 .optional() 125 .describe('Name of the MCP server that users are allowed to configure'), 126 serverCommand: z 127 .array(z.string()) 128 .min(1, 'Server command must have at least one element (the command)') 129 .optional() 130 .describe( 131 'Command array [command, ...args] to match exactly for allowed stdio servers', 132 ), 133 serverUrl: z 134 .string() 135 .optional() 136 .describe( 137 'URL pattern with wildcard support (e.g., "https://*.example.com/*") for allowed remote MCP servers', 138 ), 139 // Future extensibility: allowedTransports, requiredArgs, maxInstances, etc. 140 }) 141 .refine( 142 data => { 143 const defined = count( 144 [ 145 data.serverName !== undefined, 146 data.serverCommand !== undefined, 147 data.serverUrl !== undefined, 148 ], 149 Boolean, 150 ) 151 return defined === 1 152 }, 153 { 154 message: 155 'Entry must have exactly one of "serverName", "serverCommand", or "serverUrl"', 156 }, 157 ), 158 ) 159 160 /** 161 * Schema for denied MCP server entry in enterprise denylist. 162 * Supports matching by serverName, serverCommand, or serverUrl (mutually exclusive). 163 */ 164 export const DeniedMcpServerEntrySchema = lazySchema(() => 165 z 166 .object({ 167 serverName: z 168 .string() 169 .regex( 170 /^[a-zA-Z0-9_-]+$/, 171 'Server name can only contain letters, numbers, hyphens, and underscores', 172 ) 173 .optional() 174 .describe('Name of the MCP server that is explicitly blocked'), 175 serverCommand: z 176 .array(z.string()) 177 .min(1, 'Server command must have at least one element (the command)') 178 .optional() 179 .describe( 180 'Command array [command, ...args] to match exactly for blocked stdio servers', 181 ), 182 serverUrl: z 183 .string() 184 .optional() 185 .describe( 186 'URL pattern with wildcard support (e.g., "https://*.example.com/*") for blocked remote MCP servers', 187 ), 188 // Future extensibility: reason, blockedSince, etc. 189 }) 190 .refine( 191 data => { 192 const defined = count( 193 [ 194 data.serverName !== undefined, 195 data.serverCommand !== undefined, 196 data.serverUrl !== undefined, 197 ], 198 Boolean, 199 ) 200 return defined === 1 201 }, 202 { 203 message: 204 'Entry must have exactly one of "serverName", "serverCommand", or "serverUrl"', 205 }, 206 ), 207 ) 208 209 /** 210 * Unified schema for settings files 211 * 212 * ⚠️ BACKWARD COMPATIBILITY NOTICE ⚠️ 213 * 214 * This schema defines the structure of user settings files (.claude/settings.json). 215 * We support backward-compatible changes! Here's how: 216 * 217 * ✅ ALLOWED CHANGES: 218 * - Adding new optional fields (always use .optional()) 219 * - Adding new enum values (keeping existing ones) 220 * - Adding new properties to objects 221 * - Making validation more permissive 222 * - Using union types for gradual migration (e.g., z.union([oldType, newType])) 223 * 224 * ❌ BREAKING CHANGES TO AVOID: 225 * - Removing fields (mark as deprecated instead) 226 * - Removing enum values 227 * - Making optional fields required 228 * - Making types more restrictive 229 * - Renaming fields without keeping the old name 230 * 231 * TO ENSURE BACKWARD COMPATIBILITY: 232 * 1. Run: npm run test:file -- test/utils/settings/backward-compatibility.test.ts 233 * 2. If tests fail, you've introduced a breaking change 234 * 3. When adding new fields, add a test to BACKWARD_COMPATIBILITY_CONFIGS 235 * 236 * The settings system handles backward compatibility automatically: 237 * - When updating settings, invalid fields are preserved in the file (see settings.ts lines 233-249) 238 * - Type coercion via z.coerce (e.g., env vars convert numbers to strings) 239 * - .passthrough() preserves unknown fields in permissions object 240 * - Invalid settings are simply not used, but remain in the file to be fixed by the user 241 */ 242 243 /** 244 * Surfaces lockable by `strictPluginOnlyCustomization`. Exported so the 245 * schema preprocess (below) and the runtime helper (pluginOnlyPolicy.ts) 246 * share one source of truth. 247 */ 248 export const CUSTOMIZATION_SURFACES = [ 249 'skills', 250 'agents', 251 'hooks', 252 'mcp', 253 ] as const 254 255 export const SettingsSchema = lazySchema(() => 256 z 257 .object({ 258 $schema: z 259 .literal(CLAUDE_CODE_SETTINGS_SCHEMA_URL) 260 .optional() 261 .describe('JSON Schema reference for Claude Code settings'), 262 apiKeyHelper: z 263 .string() 264 .optional() 265 .describe('Path to a script that outputs authentication values'), 266 awsCredentialExport: z 267 .string() 268 .optional() 269 .describe('Path to a script that exports AWS credentials'), 270 awsAuthRefresh: z 271 .string() 272 .optional() 273 .describe('Path to a script that refreshes AWS authentication'), 274 gcpAuthRefresh: z 275 .string() 276 .optional() 277 .describe( 278 'Command to refresh GCP authentication (e.g., gcloud auth application-default login)', 279 ), 280 // Gated so the SDK generator (which runs without CLAUDE_CODE_ENABLE_XAA) 281 // doesn't surface this in GlobalClaudeSettings. Read via getXaaIdpSettings(). 282 // .passthrough() on the outer object keeps an existing settings.json key 283 // alive across env-var-off sessions — it's just not schema-validated then. 284 ...(isEnvTruthy(process.env.CLAUDE_CODE_ENABLE_XAA) 285 ? { 286 xaaIdp: z 287 .object({ 288 issuer: z 289 .string() 290 .url() 291 .describe('IdP issuer URL for OIDC discovery'), 292 clientId: z 293 .string() 294 .describe("Claude Code's client_id registered at the IdP"), 295 callbackPort: z 296 .number() 297 .int() 298 .positive() 299 .optional() 300 .describe( 301 'Fixed loopback callback port for the IdP OIDC login. ' + 302 'Only needed if the IdP does not honor RFC 8252 port-any matching.', 303 ), 304 }) 305 .optional() 306 .describe( 307 'XAA (SEP-990) IdP connection. Configure once; all XAA-enabled MCP servers reuse this.', 308 ), 309 } 310 : {}), 311 fileSuggestion: z 312 .object({ 313 type: z.literal('command'), 314 command: z.string(), 315 }) 316 .optional() 317 .describe('Custom file suggestion configuration for @ mentions'), 318 respectGitignore: z 319 .boolean() 320 .optional() 321 .describe( 322 'Whether file picker should respect .gitignore files (default: true). ' + 323 'Note: .ignore files are always respected.', 324 ), 325 cleanupPeriodDays: z 326 .number() 327 .nonnegative() 328 .int() 329 .optional() 330 .describe( 331 '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.', 332 ), 333 env: EnvironmentVariablesSchema() 334 .optional() 335 .describe('Environment variables to set for Claude Code sessions'), 336 // Attribution for commits and PRs 337 attribution: z 338 .object({ 339 commit: z 340 .string() 341 .optional() 342 .describe( 343 'Attribution text for git commits, including any trailers. ' + 344 'Empty string hides attribution.', 345 ), 346 pr: z 347 .string() 348 .optional() 349 .describe( 350 'Attribution text for pull request descriptions. ' + 351 'Empty string hides attribution.', 352 ), 353 }) 354 .optional() 355 .describe( 356 'Customize attribution text for commits and PRs. ' + 357 'Each field defaults to the standard Claude Code attribution if not set.', 358 ), 359 includeCoAuthoredBy: z 360 .boolean() 361 .optional() 362 .describe( 363 'Deprecated: Use attribution instead. ' + 364 "Whether to include Claude's co-authored by attribution in commits and PRs (defaults to true)", 365 ), 366 includeGitInstructions: z 367 .boolean() 368 .optional() 369 .describe( 370 "Include built-in commit and PR workflow instructions in Claude's system prompt (default: true)", 371 ), 372 permissions: PermissionsSchema() 373 .optional() 374 .describe('Tool usage permissions configuration'), 375 model: z 376 .string() 377 .optional() 378 .describe('Override the default model used by Claude Code'), 379 // Enterprise allowlist of models 380 availableModels: z 381 .array(z.string()) 382 .optional() 383 .describe( 384 'Allowlist of models that users can select. ' + 385 'Accepts family aliases ("opus" allows any opus version), ' + 386 'version prefixes ("opus-4-5" allows only that version), ' + 387 'and full model IDs. ' + 388 'If undefined, all models are available. If empty array, only the default model is available. ' + 389 'Typically set in managed settings by enterprise administrators.', 390 ), 391 modelOverrides: z 392 .record(z.string(), z.string()) 393 .optional() 394 .describe( 395 'Override mapping from Anthropic model ID (e.g. "claude-opus-4-6") to provider-specific ' + 396 'model ID (e.g. a Bedrock inference profile ARN). Typically set in managed settings by ' + 397 'enterprise administrators.', 398 ), 399 // Whether to automatically approve all MCP servers in the project 400 enableAllProjectMcpServers: z 401 .boolean() 402 .optional() 403 .describe( 404 'Whether to automatically approve all MCP servers in the project', 405 ), 406 // List of approved MCP servers from .mcp.json 407 enabledMcpjsonServers: z 408 .array(z.string()) 409 .optional() 410 .describe('List of approved MCP servers from .mcp.json'), 411 // List of rejected MCP servers from .mcp.json 412 disabledMcpjsonServers: z 413 .array(z.string()) 414 .optional() 415 .describe('List of rejected MCP servers from .mcp.json'), 416 // Enterprise allowlist of MCP servers 417 allowedMcpServers: z 418 .array(AllowedMcpServerEntrySchema()) 419 .optional() 420 .describe( 421 'Enterprise allowlist of MCP servers that can be used. ' + 422 'Applies to all scopes including enterprise servers from managed-mcp.json. ' + 423 'If undefined, all servers are allowed. If empty array, no servers are allowed. ' + 424 'Denylist takes precedence - if a server is on both lists, it is denied.', 425 ), 426 // Enterprise denylist of MCP servers 427 deniedMcpServers: z 428 .array(DeniedMcpServerEntrySchema()) 429 .optional() 430 .describe( 431 'Enterprise denylist of MCP servers that are explicitly blocked. ' + 432 'If a server is on the denylist, it will be blocked across all scopes including enterprise. ' + 433 'Denylist takes precedence over allowlist - if a server is on both lists, it is denied.', 434 ), 435 hooks: HooksSchema() 436 .optional() 437 .describe('Custom commands to run before/after tool executions'), 438 worktree: z 439 .object({ 440 symlinkDirectories: z 441 .array(z.string()) 442 .optional() 443 .describe( 444 'Directories to symlink from main repository to worktrees to avoid disk bloat. ' + 445 'Must be explicitly configured - no directories are symlinked by default. ' + 446 'Common examples: "node_modules", ".cache", ".bin"', 447 ), 448 sparsePaths: z 449 .array(z.string()) 450 .optional() 451 .describe( 452 'Directories to include when creating worktrees, via git sparse-checkout (cone mode). ' + 453 'Dramatically faster in large monorepos — only the listed paths are written to disk.', 454 ), 455 }) 456 .optional() 457 .describe('Git worktree configuration for --worktree flag.'), 458 // Whether to disable all hooks and statusLine 459 disableAllHooks: z 460 .boolean() 461 .optional() 462 .describe('Disable all hooks and statusLine execution'), 463 // Which shell backs input-box `!` (see docs/design/ps-shell-selection.md §4.2) 464 defaultShell: z 465 .enum(['bash', 'powershell']) 466 .optional() 467 .describe( 468 'Default shell for input-box ! commands. ' + 469 "Defaults to 'bash' on all platforms (no Windows auto-flip).", 470 ), 471 // Only run hooks defined in managed settings (managed-settings.json) 472 allowManagedHooksOnly: z 473 .boolean() 474 .optional() 475 .describe( 476 'When true (and set in managed settings), only hooks from managed settings run. ' + 477 'User, project, and local hooks are ignored.', 478 ), 479 // Allowlist of URL patterns HTTP hooks may target (follows allowedMcpServers precedent) 480 allowedHttpHookUrls: z 481 .array(z.string()) 482 .optional() 483 .describe( 484 'Allowlist of URL patterns that HTTP hooks may target. ' + 485 'Supports * as a wildcard (e.g. "https://hooks.example.com/*"). ' + 486 'When set, HTTP hooks with non-matching URLs are blocked. ' + 487 'If undefined, all URLs are allowed. If empty array, no HTTP hooks are allowed. ' + 488 'Arrays merge across settings sources (same semantics as allowedMcpServers).', 489 ), 490 // Allowlist of env var names HTTP hooks may interpolate into headers 491 httpHookAllowedEnvVars: z 492 .array(z.string()) 493 .optional() 494 .describe( 495 'Allowlist of environment variable names HTTP hooks may interpolate into headers. ' + 496 "When set, each hook's effective allowedEnvVars is the intersection with this list. " + 497 'If undefined, no restriction is applied. ' + 498 'Arrays merge across settings sources (same semantics as allowedMcpServers).', 499 ), 500 // Only use permission rules defined in managed settings (managed-settings.json) 501 allowManagedPermissionRulesOnly: z 502 .boolean() 503 .optional() 504 .describe( 505 'When true (and set in managed settings), only permission rules (allow/deny/ask) from managed settings are respected. ' + 506 'User, project, local, and CLI argument permission rules are ignored.', 507 ), 508 // Only read MCP allowlist policy from managed settings 509 allowManagedMcpServersOnly: z 510 .boolean() 511 .optional() 512 .describe( 513 'When true (and set in managed settings), allowedMcpServers is only read from managed settings. ' + 514 'deniedMcpServers still merges from all sources, so users can deny servers for themselves. ' + 515 'Users can still add their own MCP servers, but only the admin-defined allowlist applies.', 516 ), 517 // Force customizations through plugins only (LinkedIn ask via GTM) 518 strictPluginOnlyCustomization: z 519 .preprocess( 520 // Forwards-compat: drop unknown surface names so a future enum 521 // value (e.g. 'commands') doesn't fail safeParse and null out the 522 // ENTIRE managed-settings file (settings.ts:101). ["skills", 523 // "commands"] on an old client → ["skills"] → locks what it knows, 524 // ignores what it doesn't. Degrades to less-locked, never to 525 // everything-unlocked. 526 v => 527 Array.isArray(v) 528 ? v.filter(x => 529 (CUSTOMIZATION_SURFACES as readonly string[]).includes(x), 530 ) 531 : v, 532 z.union([z.boolean(), z.array(z.enum(CUSTOMIZATION_SURFACES))]), 533 ) 534 .optional() 535 // Non-array invalid values ("skills" string, {object}) pass through 536 // the preprocess unchanged and would fail the union → null the whole 537 // managed-settings file. .catch drops the field to undefined instead. 538 // Degrades to unlocked-for-this-field, never to everything-broken. 539 // Doctor flags the raw value. 540 .catch(undefined) 541 .describe( 542 'When set in managed settings, blocks non-plugin customization sources for the listed surfaces. ' + 543 'Array form locks specific surfaces (e.g. ["skills", "hooks"]); `true` locks all four; `false` is an explicit no-op. ' + 544 'Blocked: ~/.claude/{surface}/, .claude/{surface}/ (project), settings.json hooks, .mcp.json. ' + 545 'NOT blocked: managed (policySettings) sources, plugin-provided customizations. ' + 546 'Composes with strictKnownMarketplaces for end-to-end admin control — plugins gated by ' + 547 'marketplace allowlist, everything else blocked here.', 548 ), 549 // Status line for custom status line display 550 statusLine: z 551 .object({ 552 type: z.literal('command'), 553 command: z.string(), 554 padding: z.number().optional(), 555 }) 556 .optional() 557 .describe('Custom status line display configuration'), 558 // Enabled plugins using marketplace-first format 559 enabledPlugins: z 560 .record( 561 z.string(), 562 z.union([z.array(z.string()), z.boolean(), z.undefined()]), 563 ) 564 .optional() 565 .describe( 566 'Enabled plugins using plugin-id@marketplace-id format. Example: { "formatter@anthropic-tools": true }. Also supports extended format with version constraints.', 567 ), 568 // Extra marketplaces for this repository (usually for project settings) 569 extraKnownMarketplaces: z 570 .record(z.string(), ExtraKnownMarketplaceSchema()) 571 .check(ctx => { 572 // For settings sources, key must equal source.name. diffMarketplaces 573 // looks up materialized state by dict key; addMarketplaceSource stores 574 // under marketplace.name (= source.name for settings). A mismatch means 575 // the reconciler never converges — every session: key-lookup misses → 576 // 'missing' → source-idempotency returns alreadyMaterialized but 577 // installed++ anyway → pointless cache clears. For github/git/url the 578 // name comes from a fetched marketplace.json (mismatch is expected and 579 // benign); for settings, both key and name are user-authored in the 580 // same JSON object. 581 for (const [key, entry] of Object.entries(ctx.value)) { 582 if ( 583 entry.source.source === 'settings' && 584 entry.source.name !== key 585 ) { 586 ctx.issues.push({ 587 code: 'custom', 588 input: entry.source.name, 589 path: [key, 'source', 'name'], 590 message: 591 `Settings-sourced marketplace name must match its extraKnownMarketplaces key ` + 592 `(got key "${key}" but source.name "${entry.source.name}")`, 593 }) 594 } 595 } 596 }) 597 .optional() 598 .describe( 599 'Additional marketplaces to make available for this repository. Typically used in repository .claude/settings.json to ensure team members have required plugin sources.', 600 ), 601 // Enterprise strict list of allowed marketplace sources (policy settings only) 602 // When set, ONLY these exact sources can be added. Check happens BEFORE download. 603 strictKnownMarketplaces: z 604 .array(MarketplaceSourceSchema()) 605 .optional() 606 .describe( 607 'Enterprise strict list of allowed marketplace sources. When set in managed settings, ' + 608 'ONLY these exact sources can be added as marketplaces. The check happens BEFORE ' + 609 'downloading, so blocked sources never touch the filesystem. ' + 610 'Note: this is a policy gate only — it does NOT register marketplaces. ' + 611 'To pre-register allowed marketplaces for users, also set extraKnownMarketplaces.', 612 ), 613 // Enterprise blocklist of marketplace sources (policy settings only) 614 // When set, these exact sources are blocked. Check happens BEFORE download. 615 blockedMarketplaces: z 616 .array(MarketplaceSourceSchema()) 617 .optional() 618 .describe( 619 'Enterprise blocklist of marketplace sources. When set in managed settings, ' + 620 'these exact sources are blocked from being added as marketplaces. The check happens BEFORE ' + 621 'downloading, so blocked sources never touch the filesystem.', 622 ), 623 // Force a specific login method: 'claudeai' for Claude Pro/Max, 'console' for Console billing 624 forceLoginMethod: z 625 .enum(['claudeai', 'console']) 626 .optional() 627 .describe( 628 'Force a specific login method: "claudeai" for Claude Pro/Max, "console" for Console billing', 629 ), 630 // Organization UUID to use for OAuth login (will be added as URL param to authorization URL) 631 forceLoginOrgUUID: z 632 .string() 633 .optional() 634 .describe('Organization UUID to use for OAuth login'), 635 otelHeadersHelper: z 636 .string() 637 .optional() 638 .describe('Path to a script that outputs OpenTelemetry headers'), 639 outputStyle: z 640 .string() 641 .optional() 642 .describe('Controls the output style for assistant responses'), 643 language: z 644 .string() 645 .optional() 646 .describe( 647 'Preferred language for Claude responses and voice dictation (e.g., "japanese", "spanish")', 648 ), 649 skipWebFetchPreflight: z 650 .boolean() 651 .optional() 652 .describe( 653 'Skip the WebFetch blocklist check for enterprise environments with restrictive security policies', 654 ), 655 sandbox: SandboxSettingsSchema().optional(), 656 feedbackSurveyRate: z 657 .number() 658 .min(0) 659 .max(1) 660 .optional() 661 .describe( 662 'Probability (0–1) that the session quality survey appears when eligible. 0.05 is a reasonable starting point.', 663 ), 664 spinnerTipsEnabled: z 665 .boolean() 666 .optional() 667 .describe('Whether to show tips in the spinner'), 668 spinnerVerbs: z 669 .object({ 670 mode: z.enum(['append', 'replace']), 671 verbs: z.array(z.string()), 672 }) 673 .optional() 674 .describe( 675 'Customize spinner verbs. mode: "append" adds verbs to defaults, "replace" uses only your verbs.', 676 ), 677 spinnerTipsOverride: z 678 .object({ 679 excludeDefault: z.boolean().optional(), 680 tips: z.array(z.string()), 681 }) 682 .optional() 683 .describe( 684 'Override spinner tips. tips: array of tip strings. excludeDefault: if true, only show custom tips (default: false).', 685 ), 686 syntaxHighlightingDisabled: z 687 .boolean() 688 .optional() 689 .describe('Whether to disable syntax highlighting in diffs'), 690 terminalTitleFromRename: z 691 .boolean() 692 .optional() 693 .describe( 694 'Whether /rename updates the terminal tab title (defaults to true). Set to false to keep auto-generated topic titles.', 695 ), 696 alwaysThinkingEnabled: z 697 .boolean() 698 .optional() 699 .describe( 700 'When false, thinking is disabled. When absent or true, thinking is ' + 701 'enabled automatically for supported models.', 702 ), 703 effortLevel: z 704 .enum( 705 process.env.USER_TYPE === 'ant' 706 ? ['low', 'medium', 'high', 'max'] 707 : ['low', 'medium', 'high'], 708 ) 709 .optional() 710 .catch(undefined) 711 .describe('Persisted effort level for supported models.'), 712 advisorModel: z 713 .string() 714 .optional() 715 .describe('Advisor model for the server-side advisor tool.'), 716 fastMode: z 717 .boolean() 718 .optional() 719 .describe( 720 'When true, fast mode is enabled. When absent or false, fast mode is off.', 721 ), 722 fastModePerSessionOptIn: z 723 .boolean() 724 .optional() 725 .describe( 726 'When true, fast mode does not persist across sessions. Each session starts with fast mode off.', 727 ), 728 promptSuggestionEnabled: z 729 .boolean() 730 .optional() 731 .describe( 732 'When false, prompt suggestions are disabled. When absent or true, ' + 733 'prompt suggestions are enabled.', 734 ), 735 showClearContextOnPlanAccept: z 736 .boolean() 737 .optional() 738 .describe( 739 'When true, the plan-approval dialog offers a "clear context" option. Defaults to false.', 740 ), 741 agent: z 742 .string() 743 .optional() 744 .describe( 745 'Name of an agent (built-in or custom) to use for the main thread. ' + 746 "Applies the agent's system prompt, tool restrictions, and model.", 747 ), 748 companyAnnouncements: z 749 .array(z.string()) 750 .optional() 751 .describe( 752 'Company announcements to display at startup (one will be randomly selected if multiple are provided)', 753 ), 754 pluginConfigs: z 755 .record( 756 z.string(), 757 z.object({ 758 mcpServers: z 759 .record( 760 z.string(), 761 z.record( 762 z.string(), 763 z.union([ 764 z.string(), 765 z.number(), 766 z.boolean(), 767 z.array(z.string()), 768 ]), 769 ), 770 ) 771 .optional() 772 .describe( 773 'User configuration values for MCP servers keyed by server name', 774 ), 775 options: z 776 .record( 777 z.string(), 778 z.union([ 779 z.string(), 780 z.number(), 781 z.boolean(), 782 z.array(z.string()), 783 ]), 784 ) 785 .optional() 786 .describe( 787 'Non-sensitive option values from plugin manifest userConfig, keyed by option name. Sensitive values go to secure storage instead.', 788 ), 789 }), 790 ) 791 .optional() 792 .describe( 793 'Per-plugin configuration including MCP server user configs, keyed by plugin ID (plugin@marketplace format)', 794 ), 795 remote: z 796 .object({ 797 defaultEnvironmentId: z 798 .string() 799 .optional() 800 .describe('Default environment ID to use for remote sessions'), 801 }) 802 .optional() 803 .describe('Remote session configuration'), 804 autoUpdatesChannel: z 805 .enum(['latest', 'stable']) 806 .optional() 807 .describe('Release channel for auto-updates (latest or stable)'), 808 ...(feature('LODESTONE') 809 ? { 810 disableDeepLinkRegistration: z 811 .enum(['disable']) 812 .optional() 813 .describe( 814 'Prevent claude-cli:// protocol handler registration with the OS', 815 ), 816 } 817 : {}), 818 minimumVersion: z 819 .string() 820 .optional() 821 .describe( 822 'Minimum version to stay on - prevents downgrades when switching to stable channel', 823 ), 824 plansDirectory: z 825 .string() 826 .optional() 827 .describe( 828 'Custom directory for plan files, relative to project root. ' + 829 'If not set, defaults to ~/.claude/plans/', 830 ), 831 ...(process.env.USER_TYPE === 'ant' 832 ? { 833 classifierPermissionsEnabled: z 834 .boolean() 835 .optional() 836 .describe( 837 'Enable AI-based classification for Bash(prompt:...) permission rules', 838 ), 839 } 840 : {}), 841 ...(feature('PROACTIVE') || feature('KAIROS') 842 ? { 843 minSleepDurationMs: z 844 .number() 845 .nonnegative() 846 .int() 847 .optional() 848 .describe( 849 'Minimum duration in milliseconds that the Sleep tool must sleep for. ' + 850 'Useful for throttling proactive tick frequency.', 851 ), 852 maxSleepDurationMs: z 853 .number() 854 .int() 855 .min(-1) 856 .optional() 857 .describe( 858 'Maximum duration in milliseconds that the Sleep tool can sleep for. ' + 859 'Set to -1 for indefinite sleep (waits for user input). ' + 860 'Useful for limiting idle time in remote/managed environments.', 861 ), 862 } 863 : {}), 864 ...(feature('VOICE_MODE') 865 ? { 866 voiceEnabled: z 867 .boolean() 868 .optional() 869 .describe('Enable voice mode (hold-to-talk dictation)'), 870 } 871 : {}), 872 ...(feature('KAIROS') 873 ? { 874 assistant: z 875 .boolean() 876 .optional() 877 .describe( 878 'Start Claude in assistant mode (custom system prompt, brief view, scheduled check-in skills)', 879 ), 880 assistantName: z 881 .string() 882 .optional() 883 .describe( 884 'Display name for the assistant, shown in the claude.ai session list', 885 ), 886 } 887 : {}), 888 // Teams/Enterprise opt-IN for channel notifications. Default OFF. 889 // MCP servers that declare the claude/channel capability can push 890 // inbound messages into the conversation; for managed orgs this only 891 // works when explicitly enabled. Which servers can connect at all is 892 // still governed by allowedMcpServers/deniedMcpServers. Not 893 // feature-spread: KAIROS_CHANNELS is external:true, and the spread 894 // wrecks type inference for allowedChannelPlugins (the .passthrough() 895 // catch-all gives {} instead of the array type). 896 channelsEnabled: z 897 .boolean() 898 .optional() 899 .describe( 900 'Teams/Enterprise opt-in for channel notifications (MCP servers with the ' + 901 'claude/channel capability pushing inbound messages). Default off. ' + 902 'Set true to allow; users then select servers via --channels.', 903 ), 904 // Org-level channel plugin allowlist. When set, REPLACES the 905 // Anthropic ledger — admin owns the trust decision. Undefined means 906 // fall back to the ledger. Plugin-only entry shape (same as the 907 // ledger); server-kind entries still need the dev flag. 908 allowedChannelPlugins: z 909 .array( 910 z.object({ 911 marketplace: z.string(), 912 plugin: z.string(), 913 }), 914 ) 915 .optional() 916 .describe( 917 'Teams/Enterprise allowlist of channel plugins. When set, ' + 918 'replaces the default Anthropic allowlist — admins decide which ' + 919 'plugins may push inbound messages. Undefined falls back to the default. ' + 920 'Requires channelsEnabled: true.', 921 ), 922 ...(feature('KAIROS') || feature('KAIROS_BRIEF') 923 ? { 924 defaultView: z 925 .enum(['chat', 'transcript']) 926 .optional() 927 .describe( 928 'Default transcript view: chat (SendUserMessage checkpoints only) or transcript (full)', 929 ), 930 } 931 : {}), 932 prefersReducedMotion: z 933 .boolean() 934 .optional() 935 .describe( 936 'Reduce or disable animations for accessibility (spinner shimmer, flash effects, etc.)', 937 ), 938 autoMemoryEnabled: z 939 .boolean() 940 .optional() 941 .describe( 942 'Enable auto-memory for this project. When false, Claude will not read from or write to the auto-memory directory.', 943 ), 944 autoMemoryDirectory: z 945 .string() 946 .optional() 947 .describe( 948 '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/.', 949 ), 950 autoDreamEnabled: z 951 .boolean() 952 .optional() 953 .describe( 954 'Enable background memory consolidation (auto-dream). When set, overrides the server-side default.', 955 ), 956 showThinkingSummaries: z 957 .boolean() 958 .optional() 959 .describe( 960 'Show thinking summaries in the transcript view (ctrl+o). Default: false.', 961 ), 962 skipDangerousModePermissionPrompt: z 963 .boolean() 964 .optional() 965 .describe( 966 'Whether the user has accepted the bypass permissions mode dialog', 967 ), 968 ...(feature('TRANSCRIPT_CLASSIFIER') 969 ? { 970 skipAutoPermissionPrompt: z 971 .boolean() 972 .optional() 973 .describe( 974 'Whether the user has accepted the auto mode opt-in dialog', 975 ), 976 useAutoModeDuringPlan: z 977 .boolean() 978 .optional() 979 .describe( 980 'Whether plan mode uses auto mode semantics when auto mode is available (default: true)', 981 ), 982 autoMode: z 983 .object({ 984 allow: z 985 .array(z.string()) 986 .optional() 987 .describe('Rules for the auto mode classifier allow section'), 988 soft_deny: z 989 .array(z.string()) 990 .optional() 991 .describe('Rules for the auto mode classifier deny section'), 992 ...(process.env.USER_TYPE === 'ant' 993 ? { 994 // Back-compat alias for ant users; external users use soft_deny 995 deny: z.array(z.string()).optional(), 996 } 997 : {}), 998 environment: z 999 .array(z.string()) 1000 .optional() 1001 .describe( 1002 'Entries for the auto mode classifier environment section', 1003 ), 1004 }) 1005 .optional() 1006 .describe('Auto mode classifier prompt customization'), 1007 } 1008 : {}), 1009 disableAutoMode: z 1010 .enum(['disable']) 1011 .optional() 1012 .describe('Disable auto mode'), 1013 sshConfigs: z 1014 .array( 1015 z.object({ 1016 id: z 1017 .string() 1018 .describe( 1019 'Unique identifier for this SSH config. Used to match configs across settings sources.', 1020 ), 1021 name: z.string().describe('Display name for the SSH connection'), 1022 sshHost: z 1023 .string() 1024 .describe( 1025 'SSH host in format "user@hostname" or "hostname", or a host alias from ~/.ssh/config', 1026 ), 1027 sshPort: z 1028 .number() 1029 .int() 1030 .optional() 1031 .describe('SSH port (default: 22)'), 1032 sshIdentityFile: z 1033 .string() 1034 .optional() 1035 .describe('Path to SSH identity file (private key)'), 1036 startDirectory: z 1037 .string() 1038 .optional() 1039 .describe( 1040 'Default working directory on the remote host. ' + 1041 'Supports tilde expansion (e.g. ~/projects). ' + 1042 'If not specified, defaults to the remote user home directory. ' + 1043 'Can be overridden by the [dir] positional argument in `claude ssh <config> [dir]`.', 1044 ), 1045 }), 1046 ) 1047 .optional() 1048 .describe( 1049 'SSH connection configurations for remote environments. ' + 1050 'Typically set in managed settings by enterprise administrators ' + 1051 'to pre-configure SSH connections for team members.', 1052 ), 1053 claudeMdExcludes: z 1054 .array(z.string()) 1055 .optional() 1056 .describe( 1057 'Glob patterns or absolute paths of CLAUDE.md files to exclude from loading. ' + 1058 'Patterns are matched against absolute file paths using picomatch. ' + 1059 'Only applies to User, Project, and Local memory types (Managed/policy files cannot be excluded). ' + 1060 'Examples: "/home/user/monorepo/CLAUDE.md", "**/code/CLAUDE.md", "**/some-dir/.claude/rules/**"', 1061 ), 1062 pluginTrustMessage: z 1063 .string() 1064 .optional() 1065 .describe( 1066 'Custom message to append to the plugin trust warning shown before installation. ' + 1067 'Only read from policy settings (managed-settings.json / MDM). ' + 1068 'Useful for enterprise administrators to add organization-specific context ' + 1069 '(e.g., "All plugins from our internal marketplace are vetted and approved.").', 1070 ), 1071 }) 1072 .passthrough(), 1073 ) 1074 1075 /** 1076 * Internal type for plugin hooks - includes plugin context for execution. 1077 * Not a Zod schema since it's not user-facing (plugins provide native hooks). 1078 */ 1079 export type PluginHookMatcher = { 1080 matcher?: string 1081 hooks: HookCommand[] 1082 pluginRoot: string 1083 pluginName: string 1084 pluginId: string // format: "pluginName@marketplaceName" 1085 } 1086 1087 /** 1088 * Internal type for skill hooks - includes skill context for execution. 1089 * Not a Zod schema since it's not user-facing (skills provide native hooks). 1090 */ 1091 export type SkillHookMatcher = { 1092 matcher?: string 1093 hooks: HookCommand[] 1094 skillRoot: string 1095 skillName: string 1096 } 1097 1098 export type AllowedMcpServerEntry = z.infer< 1099 ReturnType<typeof AllowedMcpServerEntrySchema> 1100 > 1101 export type DeniedMcpServerEntry = z.infer< 1102 ReturnType<typeof DeniedMcpServerEntrySchema> 1103 > 1104 export type SettingsJson = z.infer<ReturnType<typeof SettingsSchema>> 1105 1106 /** 1107 * Type guard for MCP server entry with serverName 1108 */ 1109 export function isMcpServerNameEntry( 1110 entry: AllowedMcpServerEntry | DeniedMcpServerEntry, 1111 ): entry is { serverName: string } { 1112 return 'serverName' in entry && entry.serverName !== undefined 1113 } 1114 1115 /** 1116 * Type guard for MCP server entry with serverCommand 1117 */ 1118 export function isMcpServerCommandEntry( 1119 entry: AllowedMcpServerEntry | DeniedMcpServerEntry, 1120 ): entry is { serverCommand: string[] } { 1121 return 'serverCommand' in entry && entry.serverCommand !== undefined 1122 } 1123 1124 /** 1125 * Type guard for MCP server entry with serverUrl 1126 */ 1127 export function isMcpServerUrlEntry( 1128 entry: AllowedMcpServerEntry | DeniedMcpServerEntry, 1129 ): entry is { serverUrl: string } { 1130 return 'serverUrl' in entry && entry.serverUrl !== undefined 1131 } 1132 1133 /** 1134 * User configuration values for MCPB MCP servers 1135 */ 1136 export type UserConfigValues = Record< 1137 string, 1138 string | number | boolean | string[] 1139 > 1140 1141 /** 1142 * Plugin configuration stored in settings.json 1143 */ 1144 export type PluginConfig = { 1145 mcpServers?: { 1146 [serverName: string]: UserConfigValues 1147 } 1148 }