diff --git a/src/tools/AgentTool/AgentTool.tsx b/src/tools/AgentTool/AgentTool.tsx index 16ef0c5f..44bc8e6f 100644 --- a/src/tools/AgentTool/AgentTool.tsx +++ b/src/tools/AgentTool/AgentTool.tsx @@ -80,8 +80,16 @@ function getAutoBackgroundMs(): number { // Base input schema without multi-agent parameters const baseInputSchema = lazySchema(() => z.object({ - description: z.string().describe('A short (3-5 word) description of the task'), - prompt: z.string().describe('The task for the agent to perform'), + description: z.string().describe( + 'REQUIRED. A short (3-5 word) description of the task. ' + + 'This parameter must always be provided — never omit it. ' + + 'Example: "search for files" or "fix the bug in login".' + ), + prompt: z.string().describe( + 'REQUIRED. The full task description for the agent to perform. ' + + 'This parameter must always be provided — never omit it. ' + + 'Example: "Find all TypeScript files that import from react and list their paths."' + ), subagent_type: z.string().optional().describe('The type of specialized agent to use for this task'), model: z.enum(['sonnet', 'opus', 'haiku']).optional().describe("Optional model override for this agent. Takes precedence over the agent definition's model frontmatter. If omitted, uses the agent definition's model, or inherits from the parent."), run_in_background: z.boolean().optional().describe('Set to true to run this agent in the background. You will be notified when it completes.') diff --git a/src/tools/BashTool/BashTool.tsx b/src/tools/BashTool/BashTool.tsx index 4ad8bcf3..c0526b6b 100644 --- a/src/tools/BashTool/BashTool.tsx +++ b/src/tools/BashTool/BashTool.tsx @@ -225,7 +225,11 @@ const isBackgroundTasksDisabled = // eslint-disable-next-line custom-rules/no-process-env-top-level -- Intentional: schema must be defined at module load isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS); const fullInputSchema = lazySchema(() => z.strictObject({ - command: z.string().describe('The command to execute'), + command: z.string().describe( + 'REQUIRED. The shell command to execute. ' + + 'This parameter must always be provided — never omit it. ' + + 'Example: "ls -la" or "npm install" or "git status".' + ), timeout: semanticNumber(z.number().optional()).describe(`Optional timeout in milliseconds (max ${getMaxTimeoutMs()})`), description: z.string().optional().describe(`Clear, concise description of what this command does in active voice. Never use words like "complex" or "risk" in the description - just describe what it does. diff --git a/src/tools/FileEditTool/types.ts b/src/tools/FileEditTool/types.ts index 86be2683..0e6d26f3 100644 --- a/src/tools/FileEditTool/types.ts +++ b/src/tools/FileEditTool/types.ts @@ -5,12 +5,20 @@ import { semanticBoolean } from '../../utils/semanticBoolean.js' // The input schema with optional replace_all const inputSchema = lazySchema(() => z.strictObject({ - file_path: z.string().describe('The absolute path to the file to modify'), - old_string: z.string().describe('The text to replace'), + file_path: z.string().describe( + 'REQUIRED. The absolute path to the file to modify (must be absolute, not relative). ' + + 'Example: "/home/user/project/src/index.ts" or "C:\\\\Users\\\\user\\\\project\\\\index.ts". ' + + 'This parameter must always be provided — never omit it.' + ), + old_string: z.string().describe( + 'REQUIRED. The exact text to find and replace in the file. ' + + 'This parameter must always be provided — never omit it.' + ), new_string: z .string() .describe( - 'The text to replace it with (must be different from old_string)', + 'REQUIRED. The text to replace old_string with (must be different from old_string). ' + + 'This parameter must always be provided — never omit it.', ), replace_all: semanticBoolean( z.boolean().default(false).optional(), diff --git a/src/tools/FileReadTool/FileReadTool.ts b/src/tools/FileReadTool/FileReadTool.ts index 1ceed46d..d7e6f34e 100644 --- a/src/tools/FileReadTool/FileReadTool.ts +++ b/src/tools/FileReadTool/FileReadTool.ts @@ -226,7 +226,11 @@ function detectSessionFileType( const inputSchema = lazySchema(() => z.strictObject({ - file_path: z.string().describe('The absolute path to the file to read'), + file_path: z.string().describe( + 'REQUIRED. The absolute path to the file to read (must be absolute, not relative). ' + + 'Example: "/home/user/project/src/index.ts" or "C:\\\\Users\\\\user\\\\project\\\\index.ts". ' + + 'This parameter must always be provided — never omit it.' + ), offset: semanticNumber(z.number().int().nonnegative().optional()).describe( 'The line number to start reading from. Only provide if the file is too large to read at once', ), diff --git a/src/tools/FileWriteTool/FileWriteTool.ts b/src/tools/FileWriteTool/FileWriteTool.ts index 6da0afc8..e04f9bb5 100644 --- a/src/tools/FileWriteTool/FileWriteTool.ts +++ b/src/tools/FileWriteTool/FileWriteTool.ts @@ -58,9 +58,16 @@ const inputSchema = lazySchema(() => file_path: z .string() .describe( - 'The absolute path to the file to write (must be absolute, not relative)', + 'REQUIRED. The absolute path to the file to write (must be absolute, not relative). ' + + 'Example: "/home/user/project/src/index.ts" or "C:\\\\Users\\\\user\\\\project\\\\index.ts". ' + + 'This parameter must always be provided — never omit it.', + ), + content: z + .string() + .describe( + 'REQUIRED. The full content to write to the file. ' + + 'This parameter must always be provided — never omit it.', ), - content: z.string().describe('The content to write to the file'), }), ) type InputSchema = ReturnType diff --git a/src/tools/GlobTool/GlobTool.ts b/src/tools/GlobTool/GlobTool.ts index 7b24554d..745f57f2 100644 --- a/src/tools/GlobTool/GlobTool.ts +++ b/src/tools/GlobTool/GlobTool.ts @@ -25,7 +25,11 @@ import { const inputSchema = lazySchema(() => z.strictObject({ - pattern: z.string().describe('The glob pattern to match files against'), + pattern: z.string().describe( + 'REQUIRED. The glob pattern to match files against. ' + + 'This parameter must always be provided — never omit it. ' + + 'Example: "**/*.ts" or "src/**/*.tsx" or "*.json".' + ), path: z .string() .optional() diff --git a/src/tools/GrepTool/GrepTool.ts b/src/tools/GrepTool/GrepTool.ts index 9bdd024a..4acab7b4 100644 --- a/src/tools/GrepTool/GrepTool.ts +++ b/src/tools/GrepTool/GrepTool.ts @@ -35,7 +35,9 @@ const inputSchema = lazySchema(() => pattern: z .string() .describe( - 'The regular expression pattern to search for in file contents', + 'REQUIRED. The regular expression pattern to search for in file contents. ' + + 'This parameter must always be provided — never omit it. ' + + 'Example: "function\\\\s+\\\\w+" or "import.*from" or "TODO".', ), path: z .string() diff --git a/src/tools/SkillTool/SkillTool.ts b/src/tools/SkillTool/SkillTool.ts index befde568..eff0e0bd 100644 --- a/src/tools/SkillTool/SkillTool.ts +++ b/src/tools/SkillTool/SkillTool.ts @@ -292,7 +292,14 @@ export const inputSchema = lazySchema(() => z.object({ skill: z .string() - .describe('The skill name. E.g., "commit", "review-pr", or "pdf"'), + .describe( + 'The skill name WITHOUT the leading slash. ' + + 'When the user types /skill-name, pass skill: "skill-name" (strip the slash). ' + + 'Examples: user types "/commit" → skill: "commit", ' + + 'user types "/review-pr" → skill: "review-pr", ' + + 'user types "/cufa-equity-report" → skill: "cufa-equity-report". ' + + 'For namespaced skills use a colon: "ms-office-suite:pdf".', + ), args: z.string().optional().describe('Optional arguments for the skill'), }), ) @@ -352,6 +359,18 @@ export const SkillTool: Tool = buildTool({ toAutoClassifierInput: ({ skill }) => skill ?? '', async validateInput({ skill }, context): Promise { + // Guard against null/undefined from non-Claude function calling (e.g., GPT-5/Codex). + // OpenAI function calling may omit required parameters that Claude tool use always provides. + if (!skill || typeof skill !== 'string') { + return { + result: false, + message: + 'Missing skill name. Pass the slash command name as the skill parameter ' + + '(e.g., skill: "commit" for /commit, skill: "review-pr" for /review-pr).', + errorCode: 1, + } + } + // Skills are just skill names, no arguments const trimmed = skill.trim() if (!trimmed) { diff --git a/src/tools/SkillTool/prompt.ts b/src/tools/SkillTool/prompt.ts index b05a58f3..b4bbedb2 100644 --- a/src/tools/SkillTool/prompt.ts +++ b/src/tools/SkillTool/prompt.ts @@ -192,6 +192,11 @@ Important: - Do not invoke a skill that is already running - Do not use this tool for built-in CLI commands (like /help, /clear, etc.) - If you see a <${COMMAND_NAME_TAG}> tag in the current conversation turn, the skill has ALREADY been loaded - follow the instructions directly instead of calling this tool again + +IMPORTANT for OpenAI/GPT function calling: +- The \`skill\` parameter is REQUIRED — never omit it or pass null/undefined +- Strip the leading \`/\`: "/commit" → skill: "commit", "/review-pr" → skill: "review-pr" +- When the user types \`/skill-name\`, always call this tool with skill: "skill-name" ` }) diff --git a/src/tools/WebFetchTool/WebFetchTool.ts b/src/tools/WebFetchTool/WebFetchTool.ts index 258e1c2b..1aa7b525 100644 --- a/src/tools/WebFetchTool/WebFetchTool.ts +++ b/src/tools/WebFetchTool/WebFetchTool.ts @@ -35,8 +35,16 @@ async function scrapeWithFirecrawl(url: string): Promise<{ markdown: string; byt const inputSchema = lazySchema(() => z.strictObject({ - url: z.string().url().describe('The URL to fetch content from'), - prompt: z.string().describe('The prompt to run on the fetched content'), + url: z.string().url().describe( + 'REQUIRED. The URL to fetch content from. ' + + 'This parameter must always be provided — never omit it. ' + + 'Example: "https://example.com/page" or "https://docs.example.com/api".' + ), + prompt: z.string().describe( + 'REQUIRED. The prompt/question to answer using the fetched content. ' + + 'This parameter must always be provided — never omit it. ' + + 'Example: "What are the main features described on this page?" or "Summarize the API documentation."' + ), }), ) type InputSchema = ReturnType diff --git a/src/tools/WebSearchTool/WebSearchTool.ts b/src/tools/WebSearchTool/WebSearchTool.ts index fa9b5360..b2a85af4 100644 --- a/src/tools/WebSearchTool/WebSearchTool.ts +++ b/src/tools/WebSearchTool/WebSearchTool.ts @@ -30,7 +30,11 @@ import { const inputSchema = lazySchema(() => z.strictObject({ - query: z.string().min(2).describe('The search query to use'), + query: z.string().min(2).describe( + 'REQUIRED. The search query to use (minimum 2 characters). ' + + 'This parameter must always be provided — never omit it. ' + + 'Example: "TypeScript best practices 2024" or "how to fix CORS error".' + ), allowed_domains: z .array(z.string()) .optional()