File size: 44,339 Bytes
064bfd6 94f44a2 346a09e 0524ab0 94f44a2 064bfd6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 | 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)'),
voiceProvider: z
.enum(['groq'])
.default('groq')
.describe('Voice STT backend: "groq" (Groq Whisper API).'),
voiceAutoTTS: z
.boolean()
.optional()
.describe('Automatically speak assistant replies in voice mode'),
voiceRecordKey: z
.string()
.optional()
.describe(
'Keyboard shortcut for push-to-talk voice input (e.g. "Space" or "ctrl+shift+space")',
),
voiceTTSProvider: z
.enum(['edge', 'openai', 'elevenlabs', 'command'])
.optional()
.describe('Text-to-speech backend for spoken replies'),
voiceTTSVoice: z
.string()
.optional()
.describe('Preferred TTS voice identifier (provider-specific)'),
voiceTTSCommand: z
.string()
.optional()
.describe(
'Shell command template for command-based TTS. Supports {input}, {input_path}, and {output_path} placeholders.',
),
voiceLanguage: z
.string()
.optional()
.describe(
'Preferred language for voice dictation (e.g. "en", "ja", "zh-CN")',
),
voiceModel: z
.string()
.optional()
.describe(
'Optional whisper model or binary path override for local STT',
),
}
: {}),
...(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
}
}
|