claude-code icon indicating copy to clipboard operation
claude-code copied to clipboard

[Docs] Missing `tool_input` and `tool_response` Schemes for Hook Development

Open GowayLee opened this issue 6 months ago • 5 comments

In claude-code official doc, the reference says PreToolUse Hook and PostToolUse Hook can handle various tool-using events. And input scheme for different tool-using is different. However, I failed to find any reference to these kinds of tool input/response schemes.

As the key document for building with claude-code, it should be improved as soon as possible. Thank you for your efforts.

GowayLee avatar Jul 17 '25 02:07 GowayLee

I agree, the docs should specify tool_input and tool_response.

PbVrCt avatar Sep 01 '25 03:09 PbVrCt

I have to ask claude to read the cli.js file to get the tool response of edit...

Complete Edit Tool Response Schema

Based on analysis of the Claude Code JavaScript source (cli.js), here are all the fields in the Edit tool's tool_response:

{
  filePath: string,           // Path to the file that was edited
  oldString: string,          // The original text that was replaced  
  newString: string,          // The new text that replaced the old text
  originalFile: string,       // The complete original file contents before editing
  structuredPatch: Array<{    // Structured diff showing the changes
    oldStart: number,
    oldLines: number,
    newStart: number,
    newLines: number,
    lines: string[]
  }>,
  userModified: boolean,      // Whether the user modified Claude's proposed changes
  replaceAll: boolean         // Whether all occurrences were replaced (not just first)
}

Summary

The Edit tool response contains 7 fields total, not just the 2 (filePath and success) shown in basic documentation examples:

  1. filePath - the edited file path
  2. oldString - what was replaced
  3. newString - what it was replaced with
  4. originalFile - full original file content
  5. structuredPatch - detailed diff information as an array of hunks
  6. userModified - boolean flag if user changed Claude's proposal before accepting
  7. replaceAll - boolean whether all occurrences were replaced

Note: There is no success field - the presence of the response itself indicates success. Errors are handled through exceptions/error responses rather than a success flag.

Pentadome avatar Oct 01 '25 10:10 Pentadome

For reference:

Not sure how accurate this is.


Complete Claude Code Built-in Tool Schemas

Based on direct analysis of the Claude Code JavaScript source (cli.js), here are all the built-in tool input and output schemas:

1. Edit Tool

Input:

{
  file_path: string,     // The absolute path to the file to modify
  old_string: string,    // The text to replace
  new_string: string,    // The text to replace it with (must be different from old_string)
  replace_all?: boolean  // Replace all occurrences of old_string (default: false)
}

Output:

{
  filePath: string,           // The file path that was edited
  oldString: string,          // The original string that was replaced
  newString: string,          // The new string that replaced it
  originalFile: string,       // The original file contents before editing
  structuredPatch: Array<{    // Diff patch showing the changes
    oldStart: number,
    oldLines: number,
    newStart: number,
    newLines: number,
    lines: string[]
  }>,
  userModified: boolean,      // Whether the user modified the proposed changes
  replaceAll: boolean         // Whether all occurrences were replaced
}

2. Write Tool

Input:

{
  file_path: string,  // The absolute path to the file to write (must be absolute, not relative)
  content: string     // The content to write to the file
}

Output:

{
  type: "create" | "update",  // Whether a new file was created or an existing file was updated
  filePath: string,           // The path to the file that was written
  content: string,            // The content that was written to the file
  structuredPatch: Array<{    // Diff patch showing the changes (empty array for create)
    oldStart: number,
    oldLines: number,
    newStart: number,
    newLines: number,
    lines: string[]
  }>
}

3. Read Tool

Input:

{
  file_path: string,  // The absolute path to the file to read
  offset?: number,    // The line number to start reading from (only if file too large)
  limit?: number      // The number of lines to read (only if file too large)
}

Output:

// For text files:
{
  type: "text",
  file: {
    filePath: string,    // The path to the file that was read
    content: string,     // The file contents
    numLines: number,    // Number of lines returned
    startLine: number,   // Starting line number (1-based)
    totalLines: number   // Total lines in the file
  }
}

// For notebook files (.ipynb):
{
  type: "notebook",
  file: {
    filePath: string,
    cells: Array<{...}>  // Notebook cells
  }
}

// For image files:
{
  type: "image",
  file: {
    base64: string,      // Base64-encoded image data
    type: string         // MIME type (e.g., "image/png")
  }
}

// For PDF files:
{
  type: "pdf",
  file: {
    filePath: string,
    base64: string,
    originalSize: number
  }
}

4. MultiEdit Tool

Input:

{
  file_path: string,  // The absolute path to the file to modify
  edits: Array<{      // Array of edit operations to perform sequentially (min 1 required)
    old_string: string,
    new_string: string,
    replace_all?: boolean  // default: false
  }>
}

Output:

{
  filePath: string,              // The file path that was edited
  originalFileContents: string,  // The original file contents before edits
  structuredPatch: Array<{       // Array of diff hunks showing changes
    oldStart: number,
    oldLines: number,
    newStart: number,
    newLines: number,
    lines: string[]
  }>,
  edits: Array<{                 // The edits that were applied
    old_string: string,
    new_string: string,
    replace_all: boolean
  }>,
  userModified: boolean          // Whether user modified the changes
}

5. Bash Tool

Input:

{
  command: string,            // The command to execute
  timeout?: number,           // Optional timeout in milliseconds (max 600000)
  description?: string,       // Clear, concise description in 5-10 words
  run_in_background?: boolean // Set to true to run in background
}

Output:

{
  stdout: string,                     // Standard output
  stderr: string,                     // Standard error
  summary?: string,                   // AI-generated summary (if enabled)
  rawOutputPath?: string,             // Path to raw output file (if summarized)
  interrupted: boolean,               // Whether the command was interrupted
  isImage: boolean,                   // Whether output contains image data
  returnCodeInterpretation?: string,  // Human-readable interpretation of return code
  backgroundTaskId?: string,          // ID if run in background
  dangerouslyOverrideSandbox?: boolean // Whether sandbox was overridden
}

6. BashOutput Tool

Input:

{
  bash_id: string,  // The ID of the background shell to retrieve output from
  filter?: string   // Optional regex to filter output lines
}

Output:

{
  stdout: string,  // Standard output from background process
  stderr: string   // Standard error from background process
}

7. Glob Tool

Input:

{
  pattern: string,  // The glob pattern to match files against
  path?: string     // Directory to search in (defaults to cwd if omitted)
}

Output:

{
  filenames: string[],  // Array of matching file paths
  durationMs: number,   // Time taken to perform the search
  numFiles: number,     // Number of files matched
  truncated: boolean    // Whether results were truncated
}

8. Grep Tool

Input:

{
  pattern: string,                                  // Regex pattern to search for
  path?: string,                                    // File or directory to search in
  glob?: string,                                    // Glob pattern to filter files
  output_mode?: "content" | "files_with_matches" | "count",  // Default: "files_with_matches"
  "-B"?: number,                                    // Lines before match (content mode only)
  "-A"?: number,                                    // Lines after match (content mode only)
  "-C"?: number,                                    // Lines before and after (content mode only)
  "-n"?: boolean,                                   // Show line numbers (content mode only)
  "-i"?: boolean,                                   // Case insensitive search
  type?: string,                                    // File type (js, py, rust, etc.)
  head_limit?: number,                              // Limit output to first N entries
  multiline?: boolean                               // Enable multiline mode (default: false)
}

Output:

{
  mode: "content" | "files_with_matches" | "count",
  numFiles: number,
  filenames: string[],
  content?: string,      // For content mode
  numLines?: number,     // For content mode
  numMatches?: number    // Number of matches found
}

9. TodoWrite Tool

Input:

{
  todos: Array<{
    content: string,                               // The todo description
    status: "pending" | "in_progress" | "completed",
    activeForm: string                             // Present continuous form (e.g., "Running tests")
  }>
}

Output:

{
  success: boolean  // Whether the todos were successfully updated
}

10. WebFetch Tool

Input:

{
  url: string,    // The URL to fetch content from
  prompt: string  // The prompt to run on the fetched content
}

Output:

{
  bytes: number,       // Size of the fetched content in bytes
  code: number,        // HTTP response code
  codeText: string,    // HTTP response code text
  result: string,      // Processed result from applying the prompt to the content
  durationMs: number,  // Time taken to fetch and process the content
  url: string          // The URL that was fetched
}

11. WebSearch Tool

Input:

{
  query: string,              // The search query to use
  allowed_domains?: string[], // Only include results from these domains
  blocked_domains?: string[]  // Never include results from these domains
}

Output:

{
  results: Array<{
    title: string,
    url: string,
    snippet: string
  }>
}

12. NotebookEdit Tool

Input:

{
  notebook_path: string,                      // Absolute path to .ipynb file
  cell_id?: string,                           // ID of cell to edit
  new_source: string,                         // New source for the cell
  cell_type?: "code" | "markdown",            // Cell type (required for insert mode)
  edit_mode?: "replace" | "insert" | "delete" // Default: "replace"
}

Output:

{
  notebook: {
    filePath: string,
    cells: Array<{...}>
  }
}

13. Task Tool (Agent)

Input:

{
  description: string,    // A short (3-5 word) description of the task
  prompt: string,         // The task for the agent to perform
  subagent_type: string   // The type of specialized agent to use
}

Output:

{
  result: string  // The agent's final report/result
}

14. ExitPlanMode Tool

Input:

{
  plan: string  // The plan to present to user for approval (supports markdown)
}

Output:

undefined  // No output - this tool exits plan mode

15. KillShell Tool

Input:

{
  shell_id: string  // The ID of the background shell to kill
}

Output:

{
  success: boolean  // Whether the shell was successfully killed
}

16. ListMcpResources Tool

Input:

{
  server?: string  // Optional server name to filter resources by
}

Output:

{
  resources: Array<{
    uri: string,
    name: string,
    description?: string,
    mimeType?: string,
    server: string
  }>
}

17. ReadMcpResource Tool

Input:

{
  server: string,  // The MCP server name
  uri: string      // The resource URI to read
}

Output:

{
  contents: Array<{
    uri: string,
    mimeType?: string,
    text?: string,
    blob?: string  // Base64-encoded binary data
  }>
}

18. SlashCommand Tool

Input:

{
  command: string  // The slash command to execute with its arguments, e.g., "/review-pr 123"
}

Output:

{
  success: boolean,     // Whether the slash command is valid
  commandName: string   // The name of the slash command
}

Common Pattern: structuredPatch

Many file-editing tools return a structuredPatch array with this structure:

Array<{
  oldStart: number,   // Starting line number in old file
  oldLines: number,   // Number of lines in old file
  newStart: number,   // Starting line number in new file
  newLines: number,   // Number of lines in new file
  lines: string[]     // The actual diff lines
}>

Notes

  1. No success field (mostly): Most built-in tools don't return a success boolean. The presence of the response indicates success; errors are thrown as exceptions. TodoWrite, KillShell, and SlashCommand are exceptions that do return success.
  2. File paths: All file paths must be absolute, not relative.
  3. User modifications: Some tools track whether the user modified Claude's proposed changes before accepting them (userModified field).
  4. Structured diffs: File modification tools return structured diff information for tracking changes.
  5. Read tool variants: The Read tool returns different response structures based on file type (text, notebook, image, PDF).
  6. Grep numMatches: The Grep tool includes an undocumented numMatches field in its output that counts the total number of matches found.
  7. SlashCommand tool: This tool is used internally to execute custom slash commands defined in .claude/commands/. It validates the command and returns whether it's valid along with the command name.
As typescript code

type HookEvent<Event extends string = string> = {
 // Common fields
 session_id: string;
 transcript_path: string; // Path to conversation JSON
 cwd: string;
 hook_event_name: Event;
};

/**
* All built-in Claude Code tool names
*/
export type ClaudeCodeToolName =
 | 'Edit'
 | 'Write'
 | 'Read'
 | 'MultiEdit'
 | 'Bash'
 | 'BashOutput'
 | 'KillShell'
 | 'Glob'
 | 'Grep'
 | 'TodoWrite'
 | 'WebFetch'
 | 'WebSearch'
 | 'NotebookEdit'
 | 'Task'
 | 'ExitPlanMode'
 | 'ListMcpResources'
 | 'ReadMcpResource'
 | 'SlashCommand';

type Satisfies<Constraint, Target extends Constraint> = Target;
type ToolIOMapDef = {
 [key in ClaudeCodeToolName]: {
   input: {};
   output: {} | undefined;
 };
};

type ToolIOMap = Satisfies<
 ToolIOMapDef,
 {
   Edit: {
     input: {
       /** The absolute path to the file to modify */
       file_path: string;
       /** The text to replace */
       old_string: string;
       /** The text to replace it with (must be different from old_string) */
       new_string: string;
       /** Replace all occurrences of old_string (default: false) */
       replace_all?: boolean;
     };
     output: {
       /** The file path that was edited */
       filePath: string;
       /** The original string that was replaced */
       oldString: string;
       /** The new string that replaced it */
       newString: string;
       /** The original file contents before editing */
       originalFile: string;
       /** Diff patch showing the changes */
       structuredPatch: Array<{
         /** Starting line number in old file */
         oldStart: number;
         /** Number of lines in old file */
         oldLines: number;
         /** Starting line number in new file */
         newStart: number;
         /** Number of lines in new file */
         newLines: number;
         /** The actual diff lines */
         lines: string[];
       }>;
       /** Whether the user modified the proposed changes */
       userModified: boolean;
       /** Whether all occurrences were replaced */
       replaceAll: boolean;
     };
   };
   Write: {
     input: {
       /** The absolute path to the file to write (must be absolute, not relative) */
       file_path: string;
       /** The content to write to the file */
       content: string;
     };
     output: {
       /** Whether a new file was created or an existing file was updated */
       type: 'create' | 'update';
       /** The path to the file that was written */
       filePath: string;
       /** The content that was written to the file */
       content: string;
       /** Diff patch showing the changes (empty array for create) */
       structuredPatch: Array<{
         /** Starting line number in old file */
         oldStart: number;
         /** Number of lines in old file */
         oldLines: number;
         /** Starting line number in new file */
         newStart: number;
         /** Number of lines in new file */
         newLines: number;
         /** The actual diff lines */
         lines: string[];
       }>;
     };
   };
   Read: {
     input: {
       /** The absolute path to the file to read */
       file_path: string;
       /** The line number to start reading from (only if file too large) */
       offset?: number;
       /** The number of lines to read (only if file too large) */
       limit?: number;
     };
     output: {
       /** File type: text, notebook, image, or pdf */
       type: 'text' | 'notebook' | 'image' | 'pdf';
       file: {
         /** The path to the file that was read */
         filePath: string;
         /** The file contents (for text files) */
         content?: string;
         /** Number of lines returned (for text files) */
         numLines?: number;
         /** Starting line number, 1-based (for text files) */
         startLine?: number;
         /** Total lines in the file (for text files) */
         totalLines?: number;
         /** Base64-encoded data (for images and PDFs) */
         base64?: string;
         /** Notebook cells (for .ipynb files) */
         cells?: Array<any>;
         /** Original file size in bytes (for PDFs) */
         originalSize?: number;
       };
     };
   };
   MultiEdit: {
     input: {
       /** The absolute path to the file to modify */
       file_path: string;
       /** Array of edit operations to perform sequentially (min 1 required) */
       edits: Array<{
         /** The text to replace */
         old_string: string;
         /** The text to replace it with */
         new_string: string;
         /** Replace all occurrences (default: false) */
         replace_all?: boolean;
       }>;
     };
     output: {
       /** The file path that was edited */
       filePath: string;
       /** The original file contents before edits */
       originalFileContents: string;
       /** Array of diff hunks showing changes */
       structuredPatch: Array<{
         /** Starting line number in old file */
         oldStart: number;
         /** Number of lines in old file */
         oldLines: number;
         /** Starting line number in new file */
         newStart: number;
         /** Number of lines in new file */
         newLines: number;
         /** The actual diff lines */
         lines: string[];
       }>;
       /** The edits that were applied */
       edits: Array<{
         old_string: string;
         new_string: string;
         replace_all: boolean;
       }>;
       /** Whether user modified the changes */
       userModified: boolean;
     };
   };
   Bash: {
     input: {
       /** The command to execute */
       command: string;
       /** Optional timeout in milliseconds (max 600000) */
       timeout?: number;
       /** Clear, concise description in 5-10 words */
       description?: string;
       /** Set to true to run in background */
       run_in_background?: boolean;
     };
     output: {
       /** Standard output */
       stdout: string;
       /** Standard error */
       stderr: string;
       /** AI-generated summary (if enabled) */
       summary?: string;
       /** Path to raw output file (if summarized) */
       rawOutputPath?: string;
       /** Whether the command was interrupted */
       interrupted: boolean;
       /** Whether output contains image data */
       isImage: boolean;
       /** Human-readable interpretation of return code */
       returnCodeInterpretation?: string;
       /** ID if run in background */
       backgroundTaskId?: string;
       /** Whether sandbox was overridden */
       dangerouslyOverrideSandbox?: boolean;
     };
   };
   BashOutput: {
     input: {
       /** The ID of the background shell to retrieve output from */
       bash_id: string;
       /** Optional regex to filter output lines */
       filter?: string;
     };
     output: {
       /** Standard output from background process */
       stdout: string;
       /** Standard error from background process */
       stderr: string;
     };
   };
   KillShell: {
     input: {
       /** The ID of the background shell to kill */
       shell_id: string;
     };
     output: {
       /** Whether the shell was successfully killed */
       success: boolean;
     };
   };
   Glob: {
     input: {
       /** The glob pattern to match files against */
       pattern: string;
       /** Directory to search in (defaults to cwd if omitted) */
       path?: string;
     };
     output: {
       /** Array of matching file paths */
       filenames: string[];
       /** Time taken to perform the search */
       durationMs: number;
       /** Number of files matched */
       numFiles: number;
       /** Whether results were truncated */
       truncated: boolean;
     };
   };
   Grep: {
     input: {
       /** Regex pattern to search for */
       pattern: string;
       /** File or directory to search in */
       path?: string;
       /** Glob pattern to filter files */
       glob?: string;
       /** Output mode (default: "files_with_matches") */
       output_mode?: 'content' | 'files_with_matches' | 'count';
       /** Lines before match (content mode only) */
       '-B'?: number;
       /** Lines after match (content mode only) */
       '-A'?: number;
       /** Lines before and after (content mode only) */
       '-C'?: number;
       /** Show line numbers (content mode only) */
       '-n'?: boolean;
       /** Case insensitive search */
       '-i'?: boolean;
       /** File type (js, py, rust, etc.) */
       type?: string;
       /** Limit output to first N entries */
       head_limit?: number;
       /** Enable multiline mode (default: false) */
       multiline?: boolean;
     };
     output: {
       /** Output mode used */
       mode: 'content' | 'files_with_matches' | 'count';
       /** Number of files with matches */
       numFiles: number;
       /** Array of filenames with matches */
       filenames: string[];
       /** Matched content (for content mode) */
       content?: string;
       /** Number of lines (for content mode) */
       numLines?: number;
       /** Number of matches found */
       numMatches?: number;
     };
   };
   TodoWrite: {
     input: {
       /** The updated todo list */
       todos: Array<{
         /** The todo description */
         content: string;
         /** Todo status */
         status: 'pending' | 'in_progress' | 'completed';
         /** Present continuous form (e.g., "Running tests") */
         activeForm: string;
       }>;
     };
     output: {
       /** Whether the todos were successfully updated */
       success: boolean;
     };
   };
   WebFetch: {
     input: {
       /** The URL to fetch content from */
       url: string;
       /** The prompt to run on the fetched content */
       prompt: string;
     };
     output: {
       /** Size of the fetched content in bytes */
       bytes: number;
       /** HTTP response code */
       code: number;
       /** HTTP response code text */
       codeText: string;
       /** Processed result from applying the prompt to the content */
       result: string;
       /** Time taken to fetch and process the content */
       durationMs: number;
       /** The URL that was fetched */
       url: string;
     };
   };
   WebSearch: {
     input: {
       /** The search query to use */
       query: string;
       /** Only include results from these domains */
       allowed_domains?: string[];
       /** Never include results from these domains */
       blocked_domains?: string[];
     };
     output: {
       /** Search results */
       results: Array<{
         /** Result title */
         title: string;
         /** Result URL */
         url: string;
         /** Result snippet/description */
         snippet: string;
       }>;
     };
   };
   NotebookEdit: {
     input: {
       /** Absolute path to .ipynb file */
       notebook_path: string;
       /** ID of cell to edit */
       cell_id?: string;
       /** New source for the cell */
       new_source: string;
       /** Cell type (required for insert mode) */
       cell_type?: 'code' | 'markdown';
       /** Edit mode (default: "replace") */
       edit_mode?: 'replace' | 'insert' | 'delete';
     };
     output: {
       /** The updated notebook */
       notebook: {
         /** Path to the notebook file */
         filePath: string;
         /** Notebook cells */
         cells: Array<any>;
       };
     };
   };
   Task: {
     input: {
       /** A short (3-5 word) description of the task */
       description: string;
       /** The task for the agent to perform */
       prompt: string;
       /** The type of specialized agent to use */
       subagent_type: string;
     };
     output: {
       /** The agent's final report/result */
       result: string;
     };
   };
   ExitPlanMode: {
     input: {
       /** The plan to present to user for approval (supports markdown) */
       plan: string;
     };
     /** No output - this tool exits plan mode */
     output: undefined;
   };
   ListMcpResources: {
     input: {
       /** Optional server name to filter resources by */
       server?: string;
     };
     output: {
       /** Available MCP resources */
       resources: Array<{
         /** Resource URI */
         uri: string;
         /** Resource name */
         name: string;
         /** Resource description */
         description?: string;
         /** MIME type */
         mimeType?: string;
         /** MCP server name */
         server: string;
       }>;
     };
   };
   ReadMcpResource: {
     input: {
       /** The MCP server name */
       server: string;
       /** The resource URI to read */
       uri: string;
     };
     output: {
       /** Resource contents */
       contents: Array<{
         /** Resource URI */
         uri: string;
         /** MIME type */
         mimeType?: string;
         /** Text content */
         text?: string;
         /** Base64-encoded binary data */
         blob?: string;
       }>;
     };
   };
   SlashCommand: {
     input: {
       /** The slash command to execute with its arguments (e.g., "/review-pr 123") */
       command: string;
     };
     output: {
       /** Whether the slash command is valid */
       success: boolean;
       /** The name of the slash command */
       commandName: string;
     };
   };
 }
>;

export type PreToolEvent<Name extends ClaudeCodeToolName> =
 HookEvent<'PreToolUse'> & {
   tool_name: Name;
   tool_input: ToolIOMap[Name]['input'];
 };

export type PostToolEvent<Name extends ClaudeCodeToolName> =
 HookEvent<'PostToolUse'> & {
   tool_name: Name;
   tool_input: ToolIOMap[Name]['input'];
   tool_response: ToolIOMap[Name]['output'];
 };

export type HookOutput = {
 continue?: boolean; // Whether Claude should continue after hook execution (default: true)
 stopReason?: string; // Message shown when continue is false
 suppressOutput?: boolean; // Hide stdout from transcript mode (default: false)
 systemMessage?: string; // Optional warning message shown to the user
 decision?: 'block' | undefined;
 reason?: string; // reason for decision.
 hookSpecificOutput?: {
   hookEventName?: string;
   additionalContext?: string;
 };
};

export type PreToolUseHookOutput<
 ToolName extends ClaudeCodeToolName = ClaudeCodeToolName,
> = HookOutput & {
 hookSpecificOutput?: {
   hookEventName: 'PreToolUse';
   permissionDecision?: 'allow' | 'deny' | 'ask';
   permissionDecisionReason?: string;
   updatedInput?: ToolIOMap[ToolName]['input'];
 };
};

Pentadome avatar Oct 01 '25 12:10 Pentadome

This issue has been inactive for 30 days. If the issue is still occurring, please comment to let us know. Otherwise, this issue will be automatically closed in 30 days for housekeeping purposes.

github-actions[bot] avatar Dec 07 '25 10:12 github-actions[bot]