Three Gating Mechanisms for Tool Registration and Conditional Loading

Claude Code uses a single-source tool registry and three layered gating mechanisms—Feature Flags for experimental tools, environment‑variable SIMPLE mode for minimal tool sets, and fine‑grained permission filtering—to securely control tool visibility, initialization side‑effects, and ordering, ensuring stable prompt caching and safe schema exposure.

James' Growth Diary
James' Growth Diary
James' Growth Diary
Three Gating Mechanisms for Tool Registration and Conditional Loading

01 | Tool Registration: A Single Source of Truth

Problem: In many agent frameworks tool registration is scattered across the codebase, making it hard to know how many tools exist.

Claude Code solves this by exposing a single function getAllBaseTools() that returns an array of all built‑in tool classes. The function does no filtering or permission checks, acting as a true "single source of truth".

export function getAllBaseTools(): Tools {
  return [
    AgentTool,          // create sub‑Agent
    TaskOutputTool,    // view task output
    BashTool,          // execute Bash commands
    GlobTool,          // file pattern search
    GrepTool,          // content search (ripgrep)
    FileReadTool,      // read files
    FileEditTool,      // edit files
    FileWriteTool,     // write files
    WebFetchTool,      // fetch web pages
    TodoWriteTool,     // Todo management
    WebSearchTool,     // web search
    SkillTool,         // run Markdown skills
    AskUserQuestionTool, // ask user
    EnterPlanModeTool, // enter planning mode
    // ... conditional tools in section 02
  ];
}

The dependency graph shows tools.ts at the core, linking to concrete tool implementations, type definitions, constant lists, permission utilities, and environment utilities.

Tool registration overview diagram
Tool registration overview diagram

02 | First Gating Mechanism: Feature Flag (Compile‑time Pruning)

Problem: How to safely roll out experimental or privileged tools?

Tools like SleepTool and WorkflowTool are wrapped with a feature() call that evaluates at module load time. If the flag is on, the tool module is required; otherwise the constant is null, and later filtering skips it.

const SleepTool = feature('PROACTIVE') || feature('KAIROS')
  ? require('./tools/SleepTool/SleepTool.js').SleepTool
  : null;

const WorkflowTool = feature('WORKFLOW_SCRIPTS')
  ? (() => {
      // side‑effect: initialize built‑in workflows
      require('./tools/WorkflowTool/bundled/index.js').initBundledWorkflows();
      return require('./tools/WorkflowTool/WorkflowTool.js').WorkflowTool;
    })()
  : null;
feature()

runs once during module loading, turning the flag into a compile‑time decision and avoiding runtime overhead. WorkflowTool binds its initialization side‑effect to the loading condition, guaranteeing that the tool is ready whenever the flag is enabled.

When the constant is null, subsequent filtering simply omits the entry.

Feature Flag conditional loading diagram
Feature Flag conditional loading diagram

The design insight: initialization side‑effects are bound to the loading condition instead of a separate init() call, eliminating the risk of a flag being on while the tool remains uninitialized.

03 | Second Gating Mechanism: Environment Variable (Runtime Simplified Mode)

Problem: How to expose only a minimal tool set in embedded or CI environments?

Claude Code defines a CLAUDE_CODE_SIMPLE mode. When the environment variable is truthy, getTools() returns only three core tools; otherwise it returns the full set filtered by deny rules.

export const getTools = (permissionContext: ToolPermissionContext): Tools => {
  // Simple mode: only three tools
  if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
    return filterToolsByDenyRules(
      [BashTool, FileReadTool, FileEditTool],
      permissionContext
    );
  }

  // Full mode: all base tools, then apply deny rules
  return filterToolsByDenyRules(getAllBaseTools(), permissionContext);
};

The three‑tool minimal set is chosen deliberately: BashTool – core execution capability. FileReadTool – needed to understand context. FileEditTool – core code‑modification action.

Tools such as FileWriteTool, WebFetchTool, or AgentTool are omitted, making the set sufficient for reading, editing, and executing code.

Simple mode vs full mode comparison
Simple mode vs full mode comparison

The name CLAUDE_CODE_SIMPLE signals a reduction in overall complexity rather than merely removing tools. The same effect can be triggered via the --bare CLI flag; both are treated equivalently by the helper D5() function.

function D5(): boolean {
  return U6(process.env.CLAUDE_CODE_SIMPLE) || process.argv.includes("--bare");
}

04 | Third Gating Mechanism: Permission Filtering (Fine‑grained Runtime Control)

Problem: How to disable a tool for a specific user or project?

Permission filtering runs after the previous two layers. The function filterToolsByDenyRules iterates over the tool list and removes any tool that matches a deny rule in the provided ToolPermissionContext.

export function filterToolsByDenyRules<T>(tools: readonly T[], permissionContext: ToolPermissionContext): T[] {
  return tools.filter(tool => !getDenyRuleForTool(permissionContext, tool));
}

The ToolPermissionContext can contain global deny lists, path‑based allow/deny arrays, read‑only flags, and other policy settings. If a tool is denied, it never reaches the model schema, preventing the model from attempting to use an unavailable tool.

Permission filtering flow diagram
Permission filtering flow diagram

This three‑layer approach—Feature Flag, SIMPLE mode, and Permission Filtering—forms a depth‑defense strategy.

Three gating mechanisms comparison diagram
Three gating mechanisms comparison diagram

05 | Assemble Tool Pool: MCP Tool Merging

Claude Code also merges tools discovered from Model Context Protocol (MCP) servers. The function assembleToolPool first obtains the base tools (already filtered by permissions), then collects MCP tools, and finally merges them, giving precedence to built‑in tools.

async function assembleToolPool(
  mcpServers: MCPServer[],
  permissionContext: ToolPermissionContext
): Promise<Tools> {
  const baseTools = getTools(permissionContext);
  const mcpTools = await collectMCPTools(mcpServers);
  // MCP tools do not overwrite built‑in tools (built‑in wins)
  return mergeToolPools(baseTools, mcpTools);
}

To keep prompt‑cache stability, MCP tools are sorted by server name before merging.

const sortedMCPTools = mcpTools.sort((a, b) =>
  a.serverName.localeCompare(b.serverName)
);

Built‑in priority prevents malicious MCP servers from hijacking tool names.

MCP tool merging and ordering diagram
MCP tool merging and ordering diagram

06 | Critical Perspective: Design Boundaries

Static registration vs. dynamic discovery: getAllBaseTools() is static; adding a new tool requires code changes, unlike frameworks such as LangChain that support runtime registration.

SIMPLE mode hard‑codes the three‑tool list: extending the minimal set (e.g., adding WebFetchTool) requires a source change.

Feature Flag reliance on GrowthBook: flags need network access to fetch the latest state; local development may see a mismatch, and the service falls back to a closed state for safety.

07 | Reuse Recommendations for Your Own Agent

1. Single tool‑registry function: create a getAllTools() that lists every possible tool in one place.

export function getAllTools(): Tool[] {
  return [
    searchTool,
    calculatorTool,
    // experimental tools
    ...(process.env.ENABLE_EXPERIMENTAL ? [experimentalTool] : []),
  ];
}

2. Three‑layer filtering pipeline:

export function getToolsForSession(userId: string, sessionConfig: SessionConfig): Tool[] {
  const allTools = getAllTools();
  const featureFiltered = allTools.filter(t =>
    !t.requiredFeature || isFeatureEnabled(t.requiredFeature, userId)
  );
  const modeFiltered = sessionConfig.simple
    ? featureFiltered.filter(t => MINIMAL_TOOLS.includes(t.name))
    : featureFiltered;
  return modeFiltered.filter(t => !isDenied(t.name, sessionConfig.permissions));
}

3. Stable ordering for dynamic parts: when merging tools from external sources, sort them deterministically to keep prompt‑cache hits high.

// Bad: order may vary each request
const tools = await Promise.all(mcpServers.map(s => s.getTools()))
  .then(r => r.flat());

// Good: deterministic sort
const tools = await Promise.all(mcpServers.map(s => s.getTools()))
  .then(r => r.flat())
  .then(t => t.sort((a, b) => a.name.localeCompare(b.name)));

Conclusion

getAllBaseTools()

provides a single source of truth for tool registration, simplifying audit and maintenance. The three gating mechanisms—Feature Flag, SIMPLE mode, and Permission Filtering—operate at different layers to achieve depth‑defense. Removing a tool via permission filtering is safer than merely denying its use, because the model never sees the tool in its schema. Finally, stable ordering of MCP‑provided tools is essential for prompt‑cache efficiency and cost control.

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.

TypeScriptAI agentsfeature flagsenvironment variablesTool Registrationpermission filtering
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.