Claude Code is powerful. But what if you could teach it new tricks—without waiting for Anthropic to ship them?
I built a framework that lets you add custom tools to Claude Code using simple YAML files. No MCP servers. No complex protocols. Just define what you want, write a handler, and it works.
Here's a cow saying hello through Claude Code:
$ claude "use cowsay to say hello"
_______
< hello >
-------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
That tool doesn't exist in Claude Code. I added it in 30 seconds.
The Problem
Claude Code ships with ~30 built-in tools: Bash, Read, Write, Grep, etc. They're powerful, but limited to what Anthropic decided to include.
Want to:
- Post to Slack when a build finishes?
- Query your company's internal API?
- Roll dice for your D&D session?
- Get the actual current time?
You're out of luck. Unless you:
- Build an MCP server - Requires understanding JSON-RPC 2.0, implementing the protocol, managing processes
- Wait for Anthropic - Hope they add your feature someday
- Use this framework - Define a YAML file, write 10 lines of code, done
The Solution: YAML-Defined Tools
Here's how you add a new tool:
Step 1: Define It (YAML)
# ~/.claude-extensions/tools/timestamp.yaml
name: Timestamp
description: Get the current date and time. Use when asked what time it is.
handler: timestamp.js
schema:
properties:
format:
type: string
enum: [iso, unix, human]
required: []
Step 2: Implement It (JavaScript)
// ~/.claude-extensions/handlers/timestamp.js
module.exports = function(input) {
const now = new Date();
switch (input.format) {
case 'iso': return now.toISOString();
case 'unix': return String(Math.floor(now.getTime() / 1000));
default: return now.toString();
}
};
Step 3: Use It
$ claude "what time is it?"
It's Monday, January 27, 2025 at 10:42:53 AM.
That's it. No compilation. No deployment. No configuration files. Claude now knows about your tool and uses it when appropriate.
How It Works
The framework runs as a transparent proxy between Claude Code and Anthropic's API:
Claude Code → Extension Proxy → Anthropic API
│
├── Injects your tool schemas
├── Detects when Claude uses them
├── Runs your handler locally
└── Returns results to Claude
When Claude Code sends a request, the proxy:
- Adds your tools to the request's tool list
- Forwards to Anthropic
- Watches the response for tool calls
- Intercepts calls to your tools
- Executes your handler
- Injects the result
Claude thinks the tool is built-in. Your handler runs locally. Everyone's happy.
Real Examples
1. Cowsay (Fun)
name: Cowsay
description: Display a message with ASCII art of a cow
handler: cowsay.js
schema:
properties:
message: { type: string }
style: { type: string, enum: [default, think, yell] }
module.exports = function({ message, style }) {
const eyes = style === 'dead' ? 'xx' : 'oo';
return `
${'_'.repeat(message.length + 2)}
< ${message} >
${'-'.repeat(message.length + 2)}
\\ ^__^
\\ (${eyes})\\_______
(__)\\ )\\/\\
||----w |
|| ||`;
};
2. Dice Roller (Utility)
name: Dice
description: Roll dice for games or random selection
handler: dice.js
schema:
properties:
sides: { type: number }
count: { type: number }
module.exports = function({ sides = 6, count = 1 }) {
const rolls = Array.from({ length: count },
() => Math.floor(Math.random() * sides) + 1
);
return `Rolled ${count}d${sides}: [${rolls.join(', ')}] = ${rolls.reduce((a,b) => a+b)}`;
};
$ claude "roll 4d6 for my character stats"
Rolled 4d6: [5, 3, 6, 2] = 16
3. HTTP Client (Power)
name: HTTPRequest
description: Make HTTP requests to any API
handler: http.js
schema:
properties:
url: { type: string }
method: { type: string, enum: [GET, POST, PUT, DELETE] }
body: { type: string }
module.exports = async function({ url, method = 'GET', body }) {
const response = await fetch(url, {
method,
body,
headers: { 'Content-Type': 'application/json' }
});
return await response.text();
};
$ claude "fetch the GitHub API status"
GitHub API is operational. All systems normal.
The CLI
Managing extensions is simple:
# Create a new extension (scaffolds YAML + handler)
claude-ext create SlackNotify
# List installed extensions
claude-ext list
# Test a handler directly
claude-ext test dice '{"sides": 20, "count": 3}'
# Start the proxy
claude-ext start
# Start with hot reload (auto-reload on file changes)
claude-ext start --watch
Why Not Just Use MCP?
MCP (Model Context Protocol) is Anthropic's official extension system. It's powerful but complex:
| Aspect | MCP | This Framework |
|---|---|---|
| Setup | JSON-RPC server, process management | YAML file + JS function |
| Protocol | Full JSON-RPC 2.0 implementation | None |
| Learning curve | Steep | Minimal |
| Time to first tool | Hours | Minutes |
| Best for | Production systems | Quick tools, prototypes |
MCP is the right choice for robust, production-grade integrations. This framework is for when you just want to add a tool and move on.
Under the Hood
The magic is in how Claude Code's API works. Every request includes a tools array:
{
"model": "claude-opus-4-5-20251101",
"tools": [
{ "name": "Bash", "description": "...", "input_schema": {...} },
{ "name": "Read", "description": "...", "input_schema": {...} },
// ... 28 more built-in tools
]
}
The proxy simply appends your tools:
{
"tools": [
// ... built-in tools ...
{ "name": "Cowsay", "description": "...", "input_schema": {...} },
{ "name": "Dice", "description": "...", "input_schema": {...} }
]
}
Anthropic's API doesn't validate tool names against a whitelist. It accepts whatever you send. Claude sees your tools alongside the built-ins and uses them when appropriate.
When Claude calls your tool, the response includes:
{
"type": "tool_use",
"name": "Cowsay",
"input": { "message": "hello" }
}
The proxy intercepts this, runs your handler, and injects the result into the next request. Claude receives the output and continues naturally.
Getting Started
1. Clone & Install
git clone https://github.com/hexcreator/claude-code-extensions
cd claude-code-extensions
npm install
npm link # Makes 'claude-ext' available globally
2. Initialize
claude-ext init
3. Create Your First Tool
claude-ext create MyTool
4. Edit the Files
vim ~/.claude-extensions/tools/mytool.yaml
vim ~/.claude-extensions/handlers/mytool.js
5. Start the Proxy
claude-ext start
6. Use It
ANTHROPIC_BASE_URL=http://localhost:8892 claude "use MyTool"
What Can You Build?
The framework supports any tool that:
- Takes JSON input
- Returns a string result
- Can be implemented in JS, Python, or Shell
Ideas:
- Slack/Discord notifications - Post messages when tasks complete
- Database queries - Query your local or remote databases
- API integrations - Connect to any REST API
- File processors - Transform, convert, analyze files
- DevOps tools - Deploy, monitor, manage infrastructure
- Fun stuff - Games, generators, creative tools
Limitations
- Local execution only - Handlers run on your machine
- No streaming - Results are returned all at once
- Trust required - You're running arbitrary code
- Proxy dependency - Must route through the proxy
The Bigger Picture
This framework emerged from reverse-engineering Claude Code's architecture. By intercepting API traffic, I discovered:
- Tool schemas are sent with every request
- The API accepts any valid schema
- Tool execution happens client-side
- Results are injected via
tool_resultmessages
These insights enabled building a transparent extension layer that works without modifying Claude Code itself.
Try It
The framework is open source and ready to use:
GitHub: claude-code-extensions
Create your first custom tool in under a minute. See what Claude can do when you teach it new tricks.
Have questions or built something cool? Share in the comments!
TL;DR
- Claude Code's tools can be extended via a simple proxy
- Define tools in YAML, implement in JS/Python/Shell
- No MCP complexity, no protocol knowledge required
- Works transparently with existing Claude Code installation
- Open source, ready to use today
# From zero to custom tool in 4 commands
claude-ext init
claude-ext create MyTool
# edit files
claude-ext start
Now go build something.