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.

James' Growth Diary
James' Growth Diary
James' Growth Diary
Deep Dive into the buildTool Factory and Its Fail‑Closed Default Values

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 example

The 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_DEFAULTS

and 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Code GenerationTypeScriptSecurityFactory PatternZodTool DesignFail-Closed
James' Growth Diary
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.