Why this matters
Before you study the registry chapter or the permission chapter, you need one
simple mental model: every concrete tool in Claude Code plugs into the same
contract. Tool.ts is the file that defines that contract.
Big picture first
Tool.ts does four jobs that later chapters build on:
- it defines the session-wide permission shape with
ToolPermissionContext - it defines the per-call runtime backpack with
ToolUseContext - it defines the shared
Toolinterface and theToolscollection type - it exports
buildTool()so tool authors can start from safe defaults
The companion file types/permissions.ts holds the permission decision types
that Tool.ts imports to avoid circular dependencies. That split matters later
when you trace permission flow, but the first-pass lesson is simpler: tools,
contexts, and permission outcomes are designed to fit together.
Start with the two recurring context objects
ToolPermissionContext
The session-level policy object shared by every tool call in the current session.
export type ToolPermissionContext = DeepImmutable<{
mode: PermissionMode
additionalWorkingDirectories: Map<string, AdditionalWorkingDirectory>
alwaysAllowRules: ToolPermissionRulesBySource
alwaysDenyRules: ToolPermissionRulesBySource
alwaysAskRules: ToolPermissionRulesBySource
isBypassPermissionsModeAvailable: boolean
isAutoModeAvailable?: boolean
strippedDangerousRules?: ToolPermissionRulesBySource
shouldAvoidPermissionPrompts?: boolean
awaitAutomatedChecksBeforeDialog?: boolean
prePlanMode?: PermissionMode
}>export const getEmptyToolPermissionContext: () => ToolPermissionContext =
() => ({
mode: 'default',
additionalWorkingDirectories: new Map(),
alwaysAllowRules: {},
alwaysDenyRules: {},
alwaysAskRules: {},
isBypassPermissionsModeAvailable: false,
}) This is not one tool call. It is the policy environment around all tool calls. If a session changes mode or adds allow rules, this object changes and the same tool may behave differently.
ToolUseContext
The runtime bundle that one concrete tool invocation receives.
export type ToolUseContext = {
options: {
commands: Command[]
debug: boolean
mainLoopModel: string
tools: Tools
verbose: boolean
thinkingConfig: ThinkingConfig
mcpClients: MCPServerConnection[]
mcpResources: Record<string, ServerResource[]>
isNonInteractiveSession: boolean
agentDefinitions: AgentDefinitionsResult
maxBudgetUsd?: number
customSystemPrompt?: string
appendSystemPrompt?: string
querySource?: QuerySource
refreshTools?: () => Tools
}
abortController: AbortController
readFileState: FileStateCache
getAppState(): AppState
setAppState(f: (prev: AppState) => AppState): void
}The exported type keeps going after messages: Message[]. The opening slice
above shows the front of the object. Later in the same type, Tool.ts
continues with fields for transcript state and guardrails:
messages: Message[]
fileReadingLimits?: {
maxTokens?: number
maxSizeBytes?: number
}
globLimits?: {
maxResults?: number
}
toolDecisions?: Map<
string,
{
source: string
decision: 'accept' | 'reject'
timestamp: number
}
> Think of ToolUseContext as the backpack that travels with one call. It carries
the tool list, the current app state, the abort signal, and the current
conversation messages into that call. The full exported type keeps adding more
session hooks, progress callbacks, and execution limits after these slices.
The contract every tool satisfies
The full Tool interface is large because it covers execution, permissions, and
UI rendering. These two source excerpts show the parts to learn first:
export type Tool<
Input extends AnyObject = AnyObject,
Output = unknown,
P extends ToolProgressData = ToolProgressData,
> = {
call(
args: z.infer<Input>,
context: ToolUseContext,
canUseTool: CanUseToolFn,
parentMessage: AssistantMessage,
onProgress?: ToolCallProgress<P>,
): Promise<ToolResult<Output>>
description(
input: z.infer<Input>,
options: {
isNonInteractiveSession: boolean
toolPermissionContext: ToolPermissionContext
tools: Tools
},
): Promise<string>
readonly inputSchema: Input
readonly inputJSONSchema?: ToolInputJSONSchema
outputSchema?: z.ZodType<unknown>
inputsEquivalent?(a: z.infer<Input>, b: z.infer<Input>): boolean
isConcurrencySafe(input: z.infer<Input>): boolean
isEnabled(): boolean
isReadOnly(input: z.infer<Input>): boolean
isDestructive?(input: z.infer<Input>): boolean
checkPermissions(
input: z.infer<Input>,
context: ToolUseContext,
): Promise<PermissionResult>
prompt(options: {
getToolPermissionContext: () => Promise<ToolPermissionContext>
tools: Tools
agents: AgentDefinition[]
allowedAgentTypes?: string[]
}): Promise<string>
userFacingName(input: Partial<z.infer<Input>> | undefined): string
readonly name: string
maxResultSizeChars: number
readonly strict?: boolean
Tool.ts also defines:
export type Tools = readonly Tool[]
That one-line alias matters because it makes “the tool pool for this session” feel like a first-class concept instead of an unnamed array.
| Field | Meaning | First Pass |
|---|---|---|
name | Stable identifier used when the model asks to call a tool. | Very important |
inputSchema | The Zod schema that defines the JSON input the model is allowed to send. | Very important |
call | The function that does the actual work after validation and permission checks. | Very important |
checkPermissions | Tool-specific permission logic layered on top of the shared permission system. | Very important |
prompt | The prompt fragment that tells the model how to use the tool correctly. | Helpful after the basics |
maxResultSizeChars | The cutoff that decides when a large tool result should be persisted to disk instead of sent in full. | Helpful after the basics |
Why buildTool() exists
Concrete tool files usually do not want to re-implement the same default
behavior over and over. Tool.ts handles that with ToolDef, TOOL_DEFAULTS,
and buildTool():
export type ToolDef<
Input extends AnyObject = AnyObject,
Output = unknown,
P extends ToolProgressData = ToolProgressData,
> = Omit<Tool<Input, Output, P>, DefaultableToolKeys> &
Partial<Pick<Tool<Input, Output, P>, DefaultableToolKeys>>
const TOOL_DEFAULTS = {
isEnabled: () => true,
isConcurrencySafe: (_input?: unknown) => false,
isReadOnly: (_input?: unknown) => false,
isDestructive: (_input?: unknown) => false,
checkPermissions: (
input: { [key: string]: unknown },
_ctx?: ToolUseContext,
): Promise<PermissionResult> =>
Promise.resolve({ behavior: 'allow', updatedInput: input }),
toAutoClassifierInput: (_input?: unknown) => '',
userFacingName: (_input?: unknown) => '',
}
export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
return {
...TOOL_DEFAULTS,
userFacingName: () => def.name,
...def,
} as BuiltTool<D>
}
This is the key idea: tool authors write the interesting parts, while
buildTool() fills in the boring defaults. That keeps the contract consistent
across dozens of tool files.
Takeaways
- Tool.ts is the shared vocabulary file for the entire tools subsystem.
- ToolPermissionContext is session policy, while ToolUseContext is the runtime backpack for one call.
- buildTool() keeps tool implementations smaller by filling in default methods consistently.