Deep Dive into the buildTool Factory and Its Fail‑Closed Default Values
The article explains how the buildTool factory injects conservative default safety flags (Fail‑Closed), dramatically reduces boilerplate for the 30‑plus methods required by Claude Code's Tool interface, and combines TypeScript compile‑time checks with Zod runtime validation, illustrated with GlobTool, BashTool and FileEditTool examples, while discussing trade‑offs and design recommendations.
01 – Introduction: A Safe Number
Claude Code allows 0 fields for "forgotten declaration permissions", a stark contrast to most frameworks that adopt a Fail‑Open default (write access unless explicitly read‑only). The buildTool factory flips this to Fail‑Closed: every safety flag defaults to the most conservative value, requiring developers to explicitly declare a tool as safe.
02 – Problem Statement
The src/Tool.ts file defines a 793‑line interface with over 30 methods (identity, schema, execution core, permission pipeline, behavior flags, UI rendering, lifecycle hooks). Implementing all methods for each tool leads to code bloat and the risk of forgetting to declare safety attributes, which could unintentionally grant write permissions.
03 – Source Location
Key files:
src/Tool.ts # Interface definition + buildTool factory (793 lines)
src/tools/BashTool/BashTool.ts # Complex tool example
src/tools/GlobTool/GlobTool.ts # Simple read‑only tool example
src/tools/FileEditTool/FileEditTool.ts # Write‑operation tool exampleThe core factory signature (reconstructed from dist/cli.js) merges TOOL_DEFAULTS with the developer‑provided definition:
export type ToolDefinition<TInput, TOutput, TProgress = void> = {
name: string;
aliases?: string[];
description: string;
inputSchema: ZodSchema<TInput>;
// ...other fields
};
const TOOL_DEFAULTS = {
isConcurrencySafe: false, // default serial execution
isReadOnly: false, // default write operation
needsSandbox: false,
isIdempotent: false,
isEnabled: () => true,
aliases: [],
searchHint: undefined,
};
export function buildTool<TInput, TOutput, TProgress = void>(definition: Partial<Tool<TInput, TOutput, TProgress>> & { name: string; description: string; inputSchema: ZodSchema<TInput>; call: Tool<TInput, TOutput, TProgress>['call']; }): Tool<TInput, TOutput, TProgress> {
const base = { ...TOOL_DEFAULTS };
const tool = { ...base, ...definition };
return tool as Tool<TInput, TOutput, TProgress>;
}04 – Core Implementation Walk‑through
4.1 Factory Pattern Basics
The factory spreads TOOL_DEFAULTS first, then the developer’s partial definition, and finally relies on TypeScript’s compile‑time checks to ensure required fields are present.
// Pseudo‑code of buildTool core logic
export function buildTool<I, O, P = void>(partial: RequiredFields<I, O, P> & OptionalFields<I, O, P>) {
// 1️⃣ Inject TOOL_DEFAULTS (all conservative)
const base = { ...TOOL_DEFAULTS };
// 2️⃣ Override with developer definition
const tool = { ...base, ...partial };
// 3️⃣ TypeScript validation (compile‑time)
return tool as Tool<I, O, P>;
}4.2 GlobTool – Minimal Example
export const GlobTool = buildTool({
name: 'Glob',
description: 'Find files by pattern matching',
inputSchema: z.object({
pattern: z.string(),
path: z.string().optional(),
}),
// Explicitly upgrade safety flags
isConcurrencySafe: true, // ✅ can run in parallel
isReadOnly: true, // ✅ read‑only, no permission needed
// Unspecified fields fall back to TOOL_DEFAULTS (e.g., needsSandbox: false)
async call({ pattern, path }, ctx) {
const files = await globFiles(pattern, { cwd: path ?? ctx.cwd });
return files.slice(0, 100).join('
');
},
});Only the schema, safety flags, and call implementation are written; the remaining 20+ methods are supplied by the factory.
4.3 BashTool – Dynamic Safety Flags
export const BashTool = buildTool({
name: 'Bash',
description: 'Execute bash commands',
inputSchema: z.object({
command: z.string(),
timeout: z.number().optional(),
description: z.string().optional(),
}),
// isConcurrencySafe omitted → defaults to false (serial execution)
// isReadOnly is a function that decides based on the command
isReadOnly(input) {
// ls, cat, grep → read‑only; rm, mv, write → write operation
return isCommandReadOnly(input.command);
},
// needsSandbox is also a function
needsSandbox(input) {
return shouldRunInSandbox(input.command);
},
async call({ command, timeout }, ctx) {
// Execute bash command…
},
});The design insight is that safety flags can be methods, allowing runtime decisions based on input.
4.4 FileEditTool – Explicitly Unsafe Flags
export const FileEditTool = buildTool({
name: 'Edit',
description: 'Edit files using string replacement',
inputSchema: z.object({
file_path: z.string(),
old_string: z.string(),
new_string: z.string(),
replace_all: z.boolean().optional(),
}),
// No explicit isReadOnly or isConcurrencySafe → defaults false (write, serial)
isIdempotent: false, // explicitly non‑idempotent
async call({ file_path, old_string, new_string, replace_all }, ctx) {
// 1. Check FileStateCache
// 2. Run 8 validations
// 3. Perform string replacement
// 4. Update FileStateCache
},
});Because defaults are already safe, developers who forget to declare flags cannot introduce security holes.
05 – Design Insights
Default values are a safety net: The most secure code is the one that still works when a developer forgets to write a flag.
Boolean flags vs. capability methods: Methods allow dynamic permission decisions (e.g., BashTool.isReadOnly(input)) that static booleans cannot express.
Two‑gate safety model: TypeScript provides compile‑time guarantees (missing call or type mismatches cause errors), while Zod schemas enforce runtime validation of inputs.
Factory + DEFAULTS as lightweight config management: Embedding configuration in TypeScript ensures type safety, tree‑shaking friendliness, and no extra loading overhead.
06 – Critical Perspectives
Trade‑off 1: Static defaults cannot react to runtime context
TOOL_DEFAULTSand buildTool are evaluated at module load time, so they cannot adapt to user permissions, environment variables, or organizational policies. Claude Code handles such dynamics in the execution layer ( StreamingToolExecutor) and permission pipeline, which introduces a separation between definition‑time and execution‑time security.
Trade‑off 2: Fail‑Closed may hinder developer experience
New developers who forget to set isConcurrencySafe: true will see read‑only tools forced into serial execution, which is safe but confusing. A possible improvement is a lint rule that warns when a tool appears read‑only but lacks an explicit isReadOnly: true declaration.
Trade‑off 3: No versioning for TOOL_DEFAULTS
Changing a default (e.g., making needsSandbox default to true) would affect every tool that does not override the flag, requiring extensive regression testing in a large codebase.
07 – Practical Recommendations
Pattern 1: Start with Fail‑Closed defaults
const TOOL_DEFAULTS = {
requiresConfirmation: true, // default requires user confirmation
canRunInParallel: false, // default serial execution
isReversible: false,
maxRetries: 1,
};
function buildTool<I, O>(definition: ToolDefinition<I, O>) {
return { ...TOOL_DEFAULTS, ...definition };
}Pattern 2: Use functions for dynamic flags, literals for static ones
const ExecuteCodeTool = buildTool({
name: 'execute_code',
// ❌ static false would be wrong for read‑only code
// ✅ dynamic function based on code content
requiresConfirmation(input) { return hasWriteSideEffects(input.code); },
});Pattern 3: Combine compile‑time types with runtime Zod validation
type ToolDefinition<I, O> = {
name: string;
call: (input: I, ctx: Context) => Promise<O>;
} & Partial<ToolMetadata>;
function buildTool<I, O>(definition: ToolDefinition<I, O>) {
return {
...TOOL_DEFAULTS,
...definition,
call: async (rawInput: unknown, ctx: Context) => {
const parsed = definition.inputSchema.parse(rawInput);
return definition.call(parsed, ctx);
},
};
}Conclusion
The buildTool factory does more than eliminate boilerplate; it encodes security semantics directly into the type system, enforcing a Fail‑Closed posture that minimizes the risk of forgotten safety declarations. Combining TypeScript’s compile‑time checks with Zod’s runtime validation creates a robust two‑gate safety mechanism, and the lightweight factory‑plus‑DEFAULTS pattern offers an elegant configuration solution for large‑scale tool ecosystems.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
James' Growth Diary
I am James, focusing on AI Agent learning and growth. I continuously update two series: “AI Agent Mastery Path,” which systematically outlines core theories and practices of agents, and “Claude Code Design Philosophy,” which deeply analyzes the design thinking behind top AI tools. Helping you build a solid foundation in the AI era.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
