Chapter 3 System Design

Tool System — Factory Pattern

How 45+ tools share one interface and get composed at runtime

src/Tool.tsLines 362450
362
export type Tool<
363
  Input extends AnyObject = AnyObject,
364
  Output = unknown,
365
  P extends ToolProgressData = ToolProgressData,
366
> = {
367
  /**
368
   * Optional aliases for backwards compatibility when a tool is renamed.
369
   * The tool can be looked up by any of these names in addition to its primary name.
370
   */
371
  aliases?: string[]
372
  /**
373
   * One-line capability phrase used by ToolSearch for keyword matching.
374
   * Helps the model find this tool via keyword search when it's deferred.
375
   * 3–10 words, no trailing period.
376
   * Prefer terms not already in the tool name (e.g. 'jupyter' for NotebookEdit).
377
   */
378
  searchHint?: string
379
  call(
380
    args: z.infer<Input>,
381
    context: ToolUseContext,
382
    canUseTool: CanUseToolFn,
383
    parentMessage: AssistantMessage,
384
    onProgress?: ToolCallProgress<P>,
385
  ): Promise<ToolResult<Output>>
386
  description(
387
    input: z.infer<Input>,
388
    options: {
389
      isNonInteractiveSession: boolean
390
      toolPermissionContext: ToolPermissionContext
391
      tools: Tools
392
    },
393
  ): Promise<string>
394
  readonly inputSchema: Input
395
  // Type for MCP tools that can specify their input schema directly in JSON Schema format
396
  // rather than converting from Zod schema
397
  readonly inputJSONSchema?: ToolInputJSONSchema
398
  // Optional because TungstenTool doesn't define this. TODO: Make it required.
399
  // When we do that, we can also go through and make this a bit more type-safe.
400
  outputSchema?: z.ZodType<unknown>
401
  inputsEquivalent?(a: z.infer<Input>, b: z.infer<Input>): boolean
402
  isConcurrencySafe(input: z.infer<Input>): boolean
403
  isEnabled(): boolean
404
  isReadOnly(input: z.infer<Input>): boolean
405
  /** Defaults to false. Only set when the tool performs irreversible operations (delete, overwrite, send). */
406
  isDestructive?(input: z.infer<Input>): boolean
407
  /**
408
   * What should happen when the user submits a new message while this tool
409
   * is running.
410
   *
411
   * - `'cancel'` — stop the tool and discard its result
412
   * - `'block'`  — keep running; the new message waits
413
   *
414
   * Defaults to `'block'` when not implemented.
415
   */
416
  interruptBehavior?(): 'cancel' | 'block'
417
  /**
418
   * Returns information about whether this tool use is a search or read operation
419
   * that should be collapsed into a condensed display in the UI. Examples include
420
   * file searching (Grep, Glob), file reading (Read), and bash commands like find,
421
   * grep, wc, etc.
422
   *
423
   * Returns an object indicating whether the operation is a search or read operation:
424
   * - `isSearch: true` for search operations (grep, find, glob patterns)
425
   * - `isRead: true` for read operations (cat, head, tail, file read)
426
   * - `isList: true` for directory-listing operations (ls, tree, du)
427
   * - All can be false if the operation shouldn't be collapsed
428
   */
429
  isSearchOrReadCommand?(input: z.infer<Input>): {
430
    isSearch: boolean
431
    isRead: boolean
432
    isList?: boolean
433
  }
434
  isOpenWorld?(input: z.infer<Input>): boolean
435
  requiresUserInteraction?(): boolean
436
  isMcp?: boolean
437
  isLsp?: boolean
438
  /**
439
   * When true, this tool is deferred (sent with defer_loading: true) and requires
440
   * ToolSearch to be used before it can be called.
441
   */
442
  readonly shouldDefer?: boolean
443
  /**
444
   * When true, this tool is never deferred — its full schema appears in the
445
   * initial prompt even when ToolSearch is enabled. For MCP tools, set via
446
   * `_meta['anthropic/alwaysLoad']`. Use for tools the model must see on
447
   * turn 1 without a ToolSearch round-trip.
448
   */
449
  readonly alwaysLoad?: boolean
450
  /**
Annotations (click the dots)

The Tool interface in src/Tool.ts is the backbone of Claude Code's extensibility. Every capability — from running bash commands to reading PDFs to spawning subagents — implements the same generic interface.

🔑Key Insight

This is the Factory + Strategy pattern: a single interface that any tool can implement, loaded into a registry at startup. Adding a new tool means implementing `Tool<MyInput, MyOutput>` — nothing else changes.

The inputSchema field is a Zod schema. When Claude returns a tool call in JSON, the schema validates and types the input before call() is invoked. Bad inputs from the model are rejected before they can cause runtime errors.

ℹ️Info

`isConcurrencySafe()` takes the tool's input as an argument — the same tool can be safe in some configurations and unsafe in others. For example, reading a file is always safe; writing is not.

KEY TAKEAWAYS
  • Every tool implements the same generic Tool<Input, Output, Progress> interface
  • Tools are registered in a central registry (src/tools.ts) and loaded at startup
  • isConcurrencySafe() determines whether a tool can run in parallel with others
  • isReadOnly() and isDestructive() drive the permission UI — no special-casing needed
  • inputSchema uses Zod for runtime validation of all tool inputs from the AI
AI Assistant

Ask anything about Tool System — Factory Pattern

Powered by Groq · Enter to send, Shift+Enter for newline