Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/tools/AgentTool/AgentTool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.')
Expand Down
6 changes: 5 additions & 1 deletion src/tools/BashTool/BashTool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
14 changes: 11 additions & 3 deletions src/tools/FileEditTool/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
6 changes: 5 additions & 1 deletion src/tools/FileReadTool/FileReadTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
),
Expand Down
11 changes: 9 additions & 2 deletions src/tools/FileWriteTool/FileWriteTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof inputSchema>
Expand Down
6 changes: 5 additions & 1 deletion src/tools/GlobTool/GlobTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 3 additions & 1 deletion src/tools/GrepTool/GrepTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
21 changes: 20 additions & 1 deletion src/tools/SkillTool/SkillTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
}),
)
Expand Down Expand Up @@ -352,6 +359,18 @@ export const SkillTool: Tool<InputSchema, Output, Progress> = buildTool({
toAutoClassifierInput: ({ skill }) => skill ?? '',

async validateInput({ skill }, context): Promise<ValidationResult> {
// 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) {
Expand Down
5 changes: 5 additions & 0 deletions src/tools/SkillTool/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
`
})

Expand Down
12 changes: 10 additions & 2 deletions src/tools/WebFetchTool/WebFetchTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof inputSchema>
Expand Down
6 changes: 5 additions & 1 deletion src/tools/WebSearchTool/WebSearchTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down