validationTips.ts
1 import type { ZodIssueCode } from 'zod/v4' 2 3 // v4 ZodIssueCode is a value, not a type - use typeof to get the type 4 type ZodIssueCodeType = (typeof ZodIssueCode)[keyof typeof ZodIssueCode] 5 6 export type ValidationTip = { 7 suggestion?: string 8 docLink?: string 9 } 10 11 export type TipContext = { 12 path: string 13 code: ZodIssueCodeType | string 14 expected?: string 15 received?: unknown 16 enumValues?: string[] 17 message?: string 18 value?: unknown 19 } 20 21 type TipMatcher = { 22 matches: (context: TipContext) => boolean 23 tip: ValidationTip 24 } 25 26 const DOCUMENTATION_BASE = 'https://code.claude.com/docs/en' 27 28 const TIP_MATCHERS: TipMatcher[] = [ 29 { 30 matches: (ctx): boolean => 31 ctx.path === 'permissions.defaultMode' && ctx.code === 'invalid_value', 32 tip: { 33 suggestion: 34 'Valid modes: "acceptEdits" (ask before file changes), "plan" (analysis only), "bypassPermissions" (auto-accept all), or "default" (standard behavior)', 35 docLink: `${DOCUMENTATION_BASE}/iam#permission-modes`, 36 }, 37 }, 38 { 39 matches: (ctx): boolean => 40 ctx.path === 'apiKeyHelper' && ctx.code === 'invalid_type', 41 tip: { 42 suggestion: 43 'Provide a shell command that outputs your API key to stdout. The script should output only the API key. Example: "/bin/generate_temp_api_key.sh"', 44 }, 45 }, 46 { 47 matches: (ctx): boolean => 48 ctx.path === 'cleanupPeriodDays' && 49 ctx.code === 'too_small' && 50 ctx.expected === '0', 51 tip: { 52 suggestion: 53 'Must be 0 or greater. Set a positive number for days to retain transcripts (default is 30). Setting 0 disables session persistence entirely: no transcripts are written and existing transcripts are deleted at startup.', 54 }, 55 }, 56 { 57 matches: (ctx): boolean => 58 ctx.path.startsWith('env.') && ctx.code === 'invalid_type', 59 tip: { 60 suggestion: 61 'Environment variables must be strings. Wrap numbers and booleans in quotes. Example: "DEBUG": "true", "PORT": "3000"', 62 docLink: `${DOCUMENTATION_BASE}/settings#environment-variables`, 63 }, 64 }, 65 { 66 matches: (ctx): boolean => 67 (ctx.path === 'permissions.allow' || ctx.path === 'permissions.deny') && 68 ctx.code === 'invalid_type' && 69 ctx.expected === 'array', 70 tip: { 71 suggestion: 72 'Permission rules must be in an array. Format: ["Tool(specifier)"]. Examples: ["Bash(npm run build)", "Edit(docs/**)", "Read(~/.zshrc)"]. Use * for wildcards.', 73 }, 74 }, 75 { 76 matches: (ctx): boolean => 77 ctx.path.includes('hooks') && ctx.code === 'invalid_type', 78 tip: { 79 suggestion: 80 // gh-31187 / CC-282: prior example showed {"matcher": {"tools": ["BashTool"]}} 81 // — an object format that never existed in the schema (matcher is z.string(), 82 // always has been). Users copied the tip's example and got the same validation 83 // error again. See matchesPattern() in hooks.ts: matcher is exact-match, 84 // pipe-separated ("Edit|Write"), or regex. Empty/"*" matches all. 85 'Hooks use a matcher + hooks array. The matcher is a string: a tool name ("Bash"), pipe-separated list ("Edit|Write"), or empty to match all. Example: {"PostToolUse": [{"matcher": "Edit|Write", "hooks": [{"type": "command", "command": "echo Done"}]}]}', 86 }, 87 }, 88 { 89 matches: (ctx): boolean => 90 ctx.code === 'invalid_type' && ctx.expected === 'boolean', 91 tip: { 92 suggestion: 93 'Use true or false without quotes. Example: "includeCoAuthoredBy": true', 94 }, 95 }, 96 { 97 matches: (ctx): boolean => ctx.code === 'unrecognized_keys', 98 tip: { 99 suggestion: 100 'Check for typos or refer to the documentation for valid fields', 101 docLink: `${DOCUMENTATION_BASE}/settings`, 102 }, 103 }, 104 { 105 matches: (ctx): boolean => 106 ctx.code === 'invalid_value' && ctx.enumValues !== undefined, 107 tip: { 108 suggestion: undefined, 109 }, 110 }, 111 { 112 matches: (ctx): boolean => 113 ctx.code === 'invalid_type' && 114 ctx.expected === 'object' && 115 ctx.received === null && 116 ctx.path === '', 117 tip: { 118 suggestion: 119 'Check for missing commas, unmatched brackets, or trailing commas. Use a JSON validator to identify the exact syntax error.', 120 }, 121 }, 122 { 123 matches: (ctx): boolean => 124 ctx.path === 'permissions.additionalDirectories' && 125 ctx.code === 'invalid_type', 126 tip: { 127 suggestion: 128 'Must be an array of directory paths. Example: ["~/projects", "/tmp/workspace"]. You can also use --add-dir flag or /add-dir command', 129 docLink: `${DOCUMENTATION_BASE}/iam#working-directories`, 130 }, 131 }, 132 ] 133 134 const PATH_DOC_LINKS: Record<string, string> = { 135 permissions: `${DOCUMENTATION_BASE}/iam#configuring-permissions`, 136 env: `${DOCUMENTATION_BASE}/settings#environment-variables`, 137 hooks: `${DOCUMENTATION_BASE}/hooks`, 138 } 139 140 export function getValidationTip(context: TipContext): ValidationTip | null { 141 const matcher = TIP_MATCHERS.find(m => m.matches(context)) 142 143 if (!matcher) return null 144 145 const tip: ValidationTip = { ...matcher.tip } 146 147 if ( 148 context.code === 'invalid_value' && 149 context.enumValues && 150 !tip.suggestion 151 ) { 152 tip.suggestion = `Valid values: ${context.enumValues.map(v => `"${v}"`).join(', ')}` 153 } 154 155 // Add documentation link based on path prefix 156 if (!tip.docLink && context.path) { 157 const pathPrefix = context.path.split('.')[0] 158 if (pathPrefix) { 159 tip.docLink = PATH_DOC_LINKS[pathPrefix] 160 } 161 } 162 163 return tip 164 }