File size: 5,464 Bytes
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 | import type { ZodIssueCode } from 'zod/v4'
// v4 ZodIssueCode is a value, not a type - use typeof to get the type
type ZodIssueCodeType = (typeof ZodIssueCode)[keyof typeof ZodIssueCode]
export type ValidationTip = {
suggestion?: string
docLink?: string
}
export type TipContext = {
path: string
code: ZodIssueCodeType | string
expected?: string
received?: unknown
enumValues?: string[]
message?: string
value?: unknown
}
type TipMatcher = {
matches: (context: TipContext) => boolean
tip: ValidationTip
}
const DOCUMENTATION_BASE = 'https://code.claude.com/docs/en'
const TIP_MATCHERS: TipMatcher[] = [
{
matches: (ctx): boolean =>
ctx.path === 'permissions.defaultMode' && ctx.code === 'invalid_value',
tip: {
suggestion:
'Valid modes: "acceptEdits" (ask before file changes), "plan" (analysis only), "bypassPermissions" (auto-accept all), or "default" (standard behavior)',
docLink: `${DOCUMENTATION_BASE}/iam#permission-modes`,
},
},
{
matches: (ctx): boolean =>
ctx.path === 'apiKeyHelper' && ctx.code === 'invalid_type',
tip: {
suggestion:
'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"',
},
},
{
matches: (ctx): boolean =>
ctx.path === 'cleanupPeriodDays' &&
ctx.code === 'too_small' &&
ctx.expected === '0',
tip: {
suggestion:
'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.',
},
},
{
matches: (ctx): boolean =>
ctx.path.startsWith('env.') && ctx.code === 'invalid_type',
tip: {
suggestion:
'Environment variables must be strings. Wrap numbers and booleans in quotes. Example: "DEBUG": "true", "PORT": "3000"',
docLink: `${DOCUMENTATION_BASE}/settings#environment-variables`,
},
},
{
matches: (ctx): boolean =>
(ctx.path === 'permissions.allow' || ctx.path === 'permissions.deny') &&
ctx.code === 'invalid_type' &&
ctx.expected === 'array',
tip: {
suggestion:
'Permission rules must be in an array. Format: ["Tool(specifier)"]. Examples: ["Bash(npm run build)", "Edit(docs/**)", "Read(~/.zshrc)"]. Use * for wildcards.',
},
},
{
matches: (ctx): boolean =>
ctx.path.includes('hooks') && ctx.code === 'invalid_type',
tip: {
suggestion:
// gh-31187 / CC-282: prior example showed {"matcher": {"tools": ["BashTool"]}}
// — an object format that never existed in the schema (matcher is z.string(),
// always has been). Users copied the tip's example and got the same validation
// error again. See matchesPattern() in hooks.ts: matcher is exact-match,
// pipe-separated ("Edit|Write"), or regex. Empty/"*" matches all.
'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"}]}]}',
},
},
{
matches: (ctx): boolean =>
ctx.code === 'invalid_type' && ctx.expected === 'boolean',
tip: {
suggestion:
'Use true or false without quotes. Example: "includeCoAuthoredBy": true',
},
},
{
matches: (ctx): boolean => ctx.code === 'unrecognized_keys',
tip: {
suggestion:
'Check for typos or refer to the documentation for valid fields',
docLink: `${DOCUMENTATION_BASE}/settings`,
},
},
{
matches: (ctx): boolean =>
ctx.code === 'invalid_value' && ctx.enumValues !== undefined,
tip: {
suggestion: undefined,
},
},
{
matches: (ctx): boolean =>
ctx.code === 'invalid_type' &&
ctx.expected === 'object' &&
ctx.received === null &&
ctx.path === '',
tip: {
suggestion:
'Check for missing commas, unmatched brackets, or trailing commas. Use a JSON validator to identify the exact syntax error.',
},
},
{
matches: (ctx): boolean =>
ctx.path === 'permissions.additionalDirectories' &&
ctx.code === 'invalid_type',
tip: {
suggestion:
'Must be an array of directory paths. Example: ["~/projects", "/tmp/workspace"]. You can also use --add-dir flag or /add-dir command',
docLink: `${DOCUMENTATION_BASE}/iam#working-directories`,
},
},
]
const PATH_DOC_LINKS: Record<string, string> = {
permissions: `${DOCUMENTATION_BASE}/iam#configuring-permissions`,
env: `${DOCUMENTATION_BASE}/settings#environment-variables`,
hooks: `${DOCUMENTATION_BASE}/hooks`,
}
export function getValidationTip(context: TipContext): ValidationTip | null {
const matcher = TIP_MATCHERS.find(m => m.matches(context))
if (!matcher) return null
const tip: ValidationTip = { ...matcher.tip }
if (
context.code === 'invalid_value' &&
context.enumValues &&
!tip.suggestion
) {
tip.suggestion = `Valid values: ${context.enumValues.map(v => `"${v}"`).join(', ')}`
}
// Add documentation link based on path prefix
if (!tip.docLink && context.path) {
const pathPrefix = context.path.split('.')[0]
if (pathPrefix) {
tip.docLink = PATH_DOC_LINKS[pathPrefix]
}
}
return tip
}
|