How Tools Are Registered and Loaded in OpenClaude: A Complete Developer Guide

OpenClaude registers tools dynamically by scanning the src/tools/ directory at startup, loading each tool class into a central ToolRegistry, and associating tool-use results with unique IDs during execution via registerToolResult in the micro-compact service.

OpenClaude is a modular CLI framework where every capability—from sending messages to running verification agents—is implemented as a tool. Understanding how these tools are registered and loaded is essential for extending the system or debugging execution flows. This guide walks through the complete lifecycle based on the OpenClaude source code.


Tool Architecture Overview

OpenClaude's tool system rests on three core abstractions:

  1. Tool – an interface requiring name, description, parameters (JSON Schema), and an async run() method
  2. ToolRegistry – a global registry that maps tool names to their implementing classes
  3. AgentTool – a specialized tool type that bundles multiple tools into reusable agents

Each tool lives as a standalone module under src/tools/, making the system inherently extensible.


Stage 1: Tool Definition

Developers create tools by implementing the Tool interface in a dedicated file. The SendMessageTool exemplifies this pattern.

In src/tools/SendMessageTool/SendMessageTool.ts, a tool defines its contract:

import { Tool, ToolResult } from '../../utils/tool-types';

export default class SendMessageTool implements Tool {
  name = 'send_message';
  description = 'Sends a message to a specified recipient.';
  parameters = {
    type: 'object',
    properties: {
      recipient: { type: 'string' },
      content: { type: 'string' }
    },
    required: ['recipient', 'content']
  };

  async run(args: { recipient: string; content: string }): Promise<ToolResult> {
    // Implementation logic
    return { output: `Message sent to ${args.recipient}` };
  }
}

Key requirements:

  • Default export of the tool class
  • Unique name property used for registry lookup
  • JSON Schema compliance in parameters for LLM compatibility

Stage 2: Tool Loading at Startup

OpenClaude discovers and loads tools through the AgentTool subsystem. The entry point is src/tools/AgentTool/loadAgentsDir.ts.

The loadAgentsDir function performs filesystem crawling and dynamic imports:

import { loadAgentsDir } from '../tools/AgentTool/loadAgentsDir';

async function initializeToolRegistry() {
  // Scans src/tools/AgentTool/built-in/ and registers each export
  await loadAgentsDir();
  // Registry now contains all built-in agents and their tools
}

This process:

  • Walks src/tools/AgentTool/built-in/ for built-in agents
  • Accepts additional user-provided agent directories via configuration
  • Dynamically imports each module using import()
  • Registers discovered tools with the global ToolRegistry

Built-in agents like verificationAgent.ts demonstrate how agents bundle related tools for complex workflows.


Stage 3: Tool Registration During Execution

When the LLM generates a tool_use block, the micro-compact executor handles registration. The critical functions reside in src/services/compact/cachedMicrocompact.ts.

The execution flow uses registerToolResult to persist outcomes:

import { registerToolResult } from '../../services/compact/cachedMicrocompact';

async function handleToolUse(state: CompactState, block: ToolUseBlock) {
  // block.tool_use_id: unique identifier from LLM plan
  // block.name: tool name to look up
  const tool = toolRegistry.get(block.name);
  
  if (!tool) {
    throw new Error(`Unknown tool: ${block.name}`);
  }
  
  const result = await tool.run(block.input);
  
  // Associate result with the tool-use ID for later retrieval
  registerToolResult(state, block.tool_use_id, result);
}

Two registration helpers exist:

  • registerToolResult(state, tool_use_id, result) – stores execution output
  • registerToolMessage(state, tool_use_id, message) – stores intermediate messages

This stateful registration enables multi-step plans where later steps reference earlier tool outputs via their tool_use_id.


Complete Runtime Flow


┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   CLI Startup   │────▶│  loadAgentsDir  │────▶│  ToolRegistry   │
│                 │     │  (src/tools/)   │     │  (name→class)   │
└─────────────────┘     └─────────────────┘     └─────────────────┘
                                                         │
┌─────────────────┐     ┌─────────────────┐              │
│  LLM generates  │────▶│  micro-compact  │──────────────┘
│  tool_use block │     │    executor     │
└─────────────────┘     └─────────────────┘
                               │
                               ▼
                        ┌─────────────────┐
                        │  tool.run()     │
                        │  registerTool   │
                        │  Result(state)  │
                        └─────────────────┘


Extending the Tool System

To add a custom tool:

  1. Create the implementation in src/tools/YourTool/YourTool.ts
  2. Export as default – export default class YourTool
  3. Rebuild – loadAgentsDir picks up new tools automatically on restart

For agent-level bundling, place related tools in a subdirectory under src/tools/AgentTool/built-in/ and export them from an index file.


Summary

  • Tools implement a strict interface with name, description, parameters, and run() in src/tools/
  • Discovery happens via loadAgentsDir() in src/tools/AgentTool/loadAgentsDir.ts, which crawls directories and populates the global ToolRegistry
  • Runtime registration uses registerToolResult() in src/services/compact/cachedMicrocompact.ts to associate tool-use IDs with execution results
  • Extension requires only adding new files to src/tools/—no registry edits needed

Frequently Asked Questions

What is the ToolRegistry in OpenClaude?

The ToolRegistry is a global mapping structure that associates tool names (strings) with their implementing class constructors. It enables runtime lookup when the micro-compact executor processes a tool_use block from the LLM. The registry is populated during startup by loadAgentsDir() and consulted during execution via toolRegistry.get(name).

How does OpenClaude handle unknown tool names?

If toolRegistry.get(block.name) returns undefined, the executor throws an error indicating an unknown tool. This typically occurs when a tool is referenced in an LLM plan but not properly exported from a file in src/tools/ or not included in the scanned directories.

Can I register tools dynamically without restarting OpenClaude?

No. OpenClaude's current architecture loads and registers tools once at startup through loadAgentsDir(). Dynamic reloading would require modifying the executor to re-scan directories or implementing a file watcher that triggers registry updates.

Where is the tool result actually stored during execution?

Tool results are stored in the compact state object maintained by the micro-compact executor. The registerToolResult() function in src/services/compact/cachedMicrocompact.ts writes to this state, using the tool_use_id as the lookup key. This state persists across execution steps in a single plan.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →