Building Custom Tools with Claude Code Agent SDK
Why Build Custom Tools
Why Build Custom Tools
Claude Code out of the box handles the common development workflows well. But every team and every product has unique needs that generic tools cannot address. The Claude Code Agent SDK lets you build custom tools that extend the agent's capabilities specifically for your domain.
As a fractional frontend architect, I have built custom tools for clients that automate their specific workflows — from design system audits to accessibility compliance checks to deployment pipelines. These tools turn Claude Code from a general-purpose assistant into a specialized team member who knows your domain.
Understanding the Agent SDK
The Claude Code Agent SDK provides the building blocks for creating tools that Claude Code can discover and use. At its core, the SDK offers:
Tool Registration — Define new capabilities with descriptions, parameter schemas, and handler functions. Claude Code reads the descriptions to decide when to use your tools.
Context Access — Your tools can access the conversation context, the project directory, and the agent's understanding of the current task.
Composability — Custom tools can call other tools, both built-in and custom, enabling complex workflows.
Type Safety — The SDK is TypeScript-native, so your tools are fully typed from parameter validation to return values.
Your First Custom Tool: A Bundle Size Checker
Let me walk through building a practical tool from scratch. This tool checks the bundle size impact of code changes — something I want Claude Code to do automatically before every commit.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
// Define the tool
const bundleSizeTool = {
name: "check_bundle_size",
description:
"Analyzes the production bundle size and compares it to the baseline. " +
"Use this tool after making changes that add new dependencies or large " +
"components to ensure bundle size stays within limits.",
input_schema: {
type: "object" as const,
properties: {
baseline_path: {
type: "string",
description: "Path to the baseline bundle stats JSON file",
},
current_path: {
type: "string",
description: "Path to the current bundle stats JSON file",
},
threshold_kb: {
type: "number",
description:
"Maximum allowed increase in KB (default: 50)",
},
},
required: ["baseline_path", "current_path"],
},
};
// Tool handler
async function handleBundleSize(input: {
baseline_path: string;
current_path: string;
threshold_kb?: number;
}): Promise<string> {
const fs = await import("fs/promises");
const baseline = JSON.parse(
await fs.readFile(input.baseline_path, "utf-8")
);
const current = JSON.parse(
await fs.readFile(input.current_path, "utf-8")
);
const threshold = input.threshold_kb ?? 50;
const baselineTotal = baseline.assets.reduce(
(sum: number, a: any) => sum + a.size,
0
);
const currentTotal = current.assets.reduce(
(sum: number, a: any) => sum + a.size,
0
);
const diffKB = (currentTotal - baselineTotal) / 1024;
const passed = diffKB <= threshold;
const newChunks = current.assets.filter(
(a: any) =>
!baseline.assets.find((b: any) => b.name === a.name)
);
const grownChunks = current.assets
.filter((a: any) => {
const base = baseline.assets.find(
(b: any) => b.name === a.name
);
return base && a.size > base.size * 1.1;
})
.map((a: any) => {
const base = baseline.assets.find(
(b: any) => b.name === a.name
);
return {
name: a.name,
growth: ((a.size - base.size) / 1024).toFixed(1),
};
});
return JSON.stringify(
{
passed,
baselineKB: (baselineTotal / 1024).toFixed(1),
currentKB: (currentTotal / 1024).toFixed(1),
diffKB: diffKB.toFixed(1),
threshold,
newChunks: newChunks.map((c: any) => c.name),
grownChunks,
recommendation: passed
? "Bundle size is within acceptable limits."
: "Bundle size exceeds threshold. Review new dependencies and consider code splitting.",
},
null,
2
);
}
Building a Design System Audit Tool
Here is a more sophisticated tool that audits components against your design system:
const designAuditTool = {
name: "audit_design_system",
description:
"Audits React components for design system compliance. Checks for " +
"hardcoded colors, non-standard spacing, direct DOM styling, and " +
"missing design tokens. Use after creating or modifying UI components.",
input_schema: {
type: "object" as const,
properties: {
component_path: {
type: "string",
description: "Path to the component file to audit",
},
design_tokens_path: {
type: "string",
description: "Path to tailwind.config.ts or design tokens file",
},
},
required: ["component_path"],
},
};
async function handleDesignAudit(input: {
component_path: string;
design_tokens_path?: string;
}): Promise<string> {
const fs = await import("fs/promises");
const content = await fs.readFile(
input.component_path,
"utf-8"
);
const issues: Array<{
type: string;
line: number;
detail: string;
severity: string;
}> = [];
const lines = content.split("\n");
lines.forEach((line, index) => {
// Check for hardcoded colors
const hexPattern = /#[0-9a-fA-F]{3,8}/g;
const rgbPattern = /rgb\([^)]+\)/g;
if (hexPattern.test(line) || rgbPattern.test(line)) {
issues.push({
type: "hardcoded-color",
line: index + 1,
detail:
"Hardcoded color value found. Use Tailwind color tokens instead.",
severity: "warning",
});
}
// Check for inline styles
if (line.includes("style={{") || line.includes("style={")) {
issues.push({
type: "inline-style",
line: index + 1,
detail:
"Inline style found. Use Tailwind utility classes.",
severity: "error",
});
}
// Check for hardcoded pixel values in className
const pxInClassName = /className.*\d+px/;
if (pxInClassName.test(line)) {
issues.push({
type: "hardcoded-spacing",
line: index + 1,
detail:
"Hardcoded pixel value in className. Use Tailwind spacing scale.",
severity: "warning",
});
}
// Check for non-standard z-index
const zIndexPattern = /z-\[(\d+)\]/;
const zMatch = line.match(zIndexPattern);
if (zMatch) {
const value = parseInt(zMatch[1]);
const standard = [0, 10, 20, 30, 40, 50];
if (!standard.includes(value)) {
issues.push({
type: "non-standard-z-index",
line: index + 1,
detail: `z-index ${value} is not standard. Use z-0, z-10, z-20, z-30, z-40, or z-50.`,
severity: "warning",
});
}
}
});
return JSON.stringify(
{
file: input.component_path,
totalIssues: issues.length,
errors: issues.filter((i) => i.severity === "error")
.length,
warnings: issues.filter((i) => i.severity === "warning")
.length,
issues,
summary:
issues.length === 0
? "Component passes design system audit."
: `Found ${issues.length} design system violations.`,
},
null,
2
);
}
Building an Accessibility Checker Tool
Accessibility is non-negotiable in my projects. This tool checks common a11y issues:
const a11yCheckTool = {
name: "check_accessibility",
description:
"Checks a React component for common accessibility issues. " +
"Verifies ARIA attributes, keyboard navigation support, " +
"color contrast indicators, and semantic HTML usage.",
input_schema: {
type: "object" as const,
properties: {
component_path: {
type: "string",
description: "Path to the component to check",
},
},
required: ["component_path"],
},
};
async function handleA11yCheck(input: {
component_path: string;
}): Promise<string> {
const fs = await import("fs/promises");
const content = await fs.readFile(
input.component_path,
"utf-8"
);
const issues: Array<{
rule: string;
line: number;
detail: string;
fix: string;
}> = [];
const lines = content.split("\n");
lines.forEach((line, index) => {
// Check images without alt
if (
line.includes("<img") &&
!line.includes("alt=") &&
!line.includes("alt =")
) {
issues.push({
rule: "img-alt",
line: index + 1,
detail: "Image without alt attribute.",
fix: 'Add alt="descriptive text" or alt="" for decorative images.',
});
}
// Check buttons without accessible name
if (
line.includes("<button") &&
!line.includes("aria-label") &&
!line.includes("aria-labelledby")
) {
// Check if button has text content (simplified check)
if (line.includes("/>") || line.includes("<IconOnly")) {
issues.push({
rule: "button-name",
line: index + 1,
detail:
"Icon-only button may lack accessible name.",
fix: "Add aria-label to describe the button's action.",
});
}
}
// Check for onClick on non-interactive elements
const nonInteractive = ["<div", "<span", "<p", "<section"];
for (const tag of nonInteractive) {
if (line.includes(tag) && line.includes("onClick")) {
if (
!line.includes('role="button"') &&
!line.includes("role='button'")
) {
issues.push({
rule: "click-handler",
line: index + 1,
detail: `onClick on non-interactive element (${tag.replace("<", "")}).`,
fix: 'Use <button> instead, or add role="button" and tabIndex={0} with keyboard handler.',
});
}
}
}
// Check for missing form labels
if (
(line.includes("<input") || line.includes("<select") || line.includes("<textarea")) &&
!line.includes("aria-label") &&
!line.includes("id=")
) {
issues.push({
rule: "form-label",
line: index + 1,
detail: "Form element may lack associated label.",
fix: "Add aria-label, or use id with a <label htmlFor>.",
});
}
});
return JSON.stringify(
{
file: input.component_path,
totalIssues: issues.length,
issues,
summary:
issues.length === 0
? "No accessibility issues detected."
: `Found ${issues.length} potential accessibility issues.`,
},
null,
2
);
}
Orchestrating Tools with the Agent Loop
The real power comes when you combine tools into an agent that orchestrates them automatically:
async function runDevelopmentAgent(task: string) {
const tools = [bundleSizeTool, designAuditTool, a11yCheckTool];
let messages: Anthropic.MessageParam[] = [
{ role: "user", content: task },
];
// Agent loop
while (true) {
const response = await client.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
tools,
messages,
});
// Check if done
if (response.stop_reason === "end_turn") {
const textContent = response.content.find(
(c) => c.type === "text"
);
return textContent
? (textContent as { text: string }).text
: "Done.";
}
// Handle tool calls
if (response.stop_reason === "tool_use") {
const toolResults: Anthropic.MessageParam[] = [];
for (const block of response.content) {
if (block.type === "tool_use") {
let result: string;
switch (block.name) {
case "check_bundle_size":
result = await handleBundleSize(
block.input as any
);
break;
case "audit_design_system":
result = await handleDesignAudit(
block.input as any
);
break;
case "check_accessibility":
result = await handleA11yCheck(
block.input as any
);
break;
default:
result = "Unknown tool";
}
toolResults.push({
role: "user",
content: [
{
type: "tool_result",
tool_use_id: block.id,
content: result,
},
],
});
}
}
messages.push({ role: "assistant", content: response.content });
messages.push(...toolResults);
}
}
}
Now you can invoke the agent with natural language:
const result = await runDevelopmentAgent(
"Audit the new PricingCard component at components/PricingCard.tsx. " +
"Check design system compliance and accessibility. " +
"Then check if the bundle size increased by more than 20KB."
);
The agent decides which tools to call, in what order, and how to interpret the results. It might run the design audit first, find that there are inline styles, then check accessibility, find missing ARIA labels, and compile a comprehensive report with all findings.
Real-World Applications
Pre-Commit Quality Gate
I built a custom tool that runs before every commit:
- Design system audit on all changed component files
- Accessibility check on all changed component files
- Bundle size comparison against the last release
- Custom lint rules specific to the project
If any check fails, it reports the issues with specific fix instructions. This catches problems before they enter the codebase.
Client-Specific Workflows
For one client, I built a tool that generates component documentation automatically — reading the component props, extracting usage patterns from across the codebase, and generating a markdown page that matches their documentation site format.
For another client, I built a tool that audits API service functions against their OpenAPI spec, flagging mismatches between the frontend's expected types and the backend's actual schema.
Performance Budget Tool
A tool that tracks performance metrics over time:
- Runs Lighthouse CI on specified pages
- Compares against baseline metrics
- Flags any Core Web Vitals regressions
- Generates a report with specific optimization suggestions
The Bigger Picture
Custom tools transform Claude Code from a general-purpose coding assistant into a specialized system that understands your domain, your quality standards, and your workflows. The investment in building these tools pays for itself within weeks.
The Agent SDK is designed to be approachable. If you can write a TypeScript function, you can build a Claude Code tool. Start with one tool that addresses your biggest pain point, see the impact, and expand from there.
The frontier of AI-assisted development is not just about better models. It is about better tools that make those models useful for your specific work. Custom tools are how you get there.