GitHub - hexcreator/claude-code-extensions: Add custom tools to Claude Code with simple YAML definitions. No MCP required.

5 min read Original article ↗

Claude Code Extension Framework

Claude Code Extensions

Add custom tools to Claude Code with simple YAML definitions

Quick StartHow It WorksCreate ExtensionsBuilt-in ToolsCLI Reference

License Node PRs Welcome


✨ Demo

$ claude "use cowsay to say 'Hello from custom extensions!'"

 __________________________________
< Hello from custom extensions!    >
 ----------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
$ claude "roll 4d6 for my D&D character"

Rolled 4d6: [6, 4, 5, 2] = 17
$ claude "what time is it right now?"

It's Monday, January 27, 2025 at 10:42:53 AM.

These tools don't exist in Claude Code. They were added in 30 seconds.


🚀 Quick Start

1. Install

git clone https://github.com/hexcreator/claude-code-extensions.git
cd claude-code-extensions
npm install
npm link  # Makes 'claude-ext' available globally

2. Initialize & Start

claude-ext init    # Creates ~/.claude-extensions/
claude-ext start   # Starts the extension proxy

3. Use with Claude Code

ANTHROPIC_BASE_URL=http://localhost:8892 claude "roll a d20"

That's it! Claude Code now has access to custom tools.


🎯 Why This Exists

Claude Code ships with ~30 built-in tools, but what if you need:

Need Solution
📢 Slack notifications claude-ext create SlackNotify
🎲 Dice for D&D Built-in Dice tool
⏰ Actual current time Built-in Timestamp tool
🔌 Custom API calls Built-in HTTPRequest tool
🐄 ASCII art cows Built-in Cowsay tool

No MCP servers. No complex protocols. Just YAML + a handler.


⚙️ How It Works

┌─────────────────┐     ┌───────────────────────────────────────┐     ┌─────────────────┐
│                 │     │         Extension Proxy               │     │                 │
│   Claude Code   │────▶│  ┌─────────────────────────────────┐  │────▶│  Anthropic API  │
│                 │     │  │ 1. Inject custom tool schemas   │  │     │                 │
│  "roll a d20"   │     │  │ 2. Forward to Anthropic         │  │     │  claude-sonnet  │
│                 │◀────│  │ 3. Intercept tool calls         │  │◀────│                 │
│  "You rolled    │     │  │ 4. Execute local handlers       │  │     │  tool_use:Dice  │
│   17!"          │     │  │ 5. Inject results               │  │     │                 │
│                 │     │  └─────────────────────────────────┘  │     │                 │
└─────────────────┘     └───────────────────────────────────────┘     └─────────────────┘
                                        │
                                        ▼
                        ┌───────────────────────────────┐
                        │   ~/.claude-extensions/       │
                        │   ├── tools/                  │
                        │   │   ├── dice.yaml          │
                        │   │   └── cowsay.yaml        │
                        │   └── handlers/              │
                        │       ├── dice.js            │
                        │       └── cowsay.js          │
                        └───────────────────────────────┘

The proxy intercepts Claude Code's API requests and:

  1. Adds your tools to the request's tool list
  2. Watches responses for tool calls
  3. Executes your handlers locally
  4. Injects results back into the conversation

Claude thinks the tools are built-in. They're not—they're running on your machine.


📝 Creating Extensions

Method 1: CLI (Recommended)

This scaffolds:

  • ~/.claude-extensions/tools/mytool.yaml - Tool definition
  • ~/.claude-extensions/handlers/mytool.js - Handler implementation

Method 2: Manual

1. Create the definition:

# ~/.claude-extensions/tools/slack.yaml
name: SlackPost
description: Post a message to a Slack channel
handler: slack.js

schema:
  properties:
    channel:
      type: string
      description: Channel name (e.g., #general)
    message:
      type: string
      description: Message to post
  required:
    - channel
    - message

2. Create the handler:

// ~/.claude-extensions/handlers/slack.js
module.exports = async function(input, context) {
  const { channel, message } = input;
  const { env, fetch } = context;

  await fetch('https://slack.com/api/chat.postMessage', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${env.SLACK_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ channel, text: message })
  });

  return `Message posted to ${channel}`;
};

3. Use it:

claude "post 'Build complete!' to #dev-notifications"

🧰 Built-in Tools

🐄 Cowsay

ASCII art cow messages.

schema:
  message: string   # What the cow says
  style: enum       # default, think, yell, dead, etc.

🎲 Dice

Random number generation.

schema:
  sides: number     # Die sides (default: 6)
  count: number     # Number of dice (default: 1)
  # OR
  min: number       # Range minimum
  max: number       # Range maximum
  # OR
  pick: array       # Items to randomly select from

⏰ Timestamp

Current date and time.

schema:
  format: enum      # iso, unix, human, date, time
  timezone: string  # e.g., "America/New_York"

🌐 HTTPRequest

Make HTTP requests to any URL.

schema:
  url: string       # Full URL (required)
  method: enum      # GET, POST, PUT, DELETE
  headers: object   # Key-value headers
  body: string      # Request body

💻 CLI Reference

claude-ext <command> [options]
Command Description
init Initialize ~/.claude-extensions/ directory
start Start the extension proxy
start --watch Start with hot reload
list List installed extensions
create <name> Create a new extension
test <name> [json] Test a handler directly
help Show help

Examples

# Create and test a new tool
claude-ext create WeatherAPI
claude-ext test weatherapi '{"city": "San Francisco"}'

# Start proxy with auto-reload
claude-ext start --watch

# Use with Claude Code
ANTHROPIC_BASE_URL=http://localhost:8892 claude "your prompt"

🔧 Handler Interface

JavaScript

module.exports = async function(input, context) {
  // input: Parameters from tool call
  // context: { env, log, fetch }

  const { myParam } = input;
  const { env, log, fetch } = context;

  log('Processing:', myParam);

  return 'Result string';
};

Python

import os, json

input_data = json.loads(os.environ['EXTENSION_INPUT'])
print(f"Result: {input_data['myParam']}")

Shell

#!/bin/bash
# Input params are UPPERCASE env vars
echo "Received: $MYPARAM"

🔒 Security Notes

  • Handlers run with full system access
  • Environment variables can contain secrets
  • No sandboxing by default
  • Only install extensions you trust

🆚 Comparison with MCP

MCP This Framework
Setup JSON-RPC server YAML + handler
Protocol Must implement None
Learning curve Steep Minimal
Time to first tool Hours Minutes
Best for Production Quick tools

Use MCP for robust, production-grade integrations. Use this when you just want a tool and want to move on.


🤝 Contributing

Contributions welcome! Feel free to:

  • Add new built-in tools
  • Improve the proxy
  • Fix bugs
  • Enhance documentation

📄 License

MIT © hexcreator


Built by reverse-engineering Claude Code's architecture
See the full research →