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:
- Tool – an interface requiring
name,description,parameters(JSON Schema), and an asyncrun()method - ToolRegistry – a global registry that maps tool names to their implementing classes
- 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
nameproperty used for registry lookup - JSON Schema compliance in
parametersfor 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 outputregisterToolMessage(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:
- Create the implementation in
src/tools/YourTool/YourTool.ts - Export as default –
export default class YourTool - Rebuild –
loadAgentsDirpicks 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, andrun()insrc/tools/ - Discovery happens via
loadAgentsDir()insrc/tools/AgentTool/loadAgentsDir.ts, which crawls directories and populates the global ToolRegistry - Runtime registration uses
registerToolResult()insrc/services/compact/cachedMicrocompact.tsto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →