Function Calling Changed How I Build
I remember the exact moment I stopped treating Claude like a chat buddy and started treating it like a junior developer with a terminal. It was 2 AM, I was debugging a Kubernetes ingress issue, and I realized I'd spent more time copy-pasting YAML into the chat than actually fixing the problem. Then it hit me: what if Claude could just run the commands itself? That night, I wired up function calling for the first time, and I haven't built software the same way since.
The Problem: Chat Is a Dead End
For months, I used Claude the way most developers do: describe a problem, get a solution, copy the code, paste it into my terminal, run it, see an error, copy the error back, repeat. It worked, but it was slow. Every iteration required manual context switching — alt-tabbing between browser and terminal, reading error messages, formatting them for the AI. I was essentially acting as a human API gateway.
The deeper issue was trust. I couldn't verify the AI's output without running it myself. And running it myself meant leaving the chat. The conversation loop was broken by the physical act of executing code. I needed a way for Claude to do things, not just say things.
The Solution: Giving Claude Hands
I built a simple Node.js server that exposes a set of tools as JSON-RPC endpoints. Each tool is a function Claude can call — run_shell_command, search_web, read_file, write_file. The key insight was that function calling isn't about giving the AI more capabilities; it's about giving it feedback loops.
Here's the core of what I built:
const tools = [
{
name: 'run_shell_command',
description: 'Execute a shell command and return stdout/stderr',
parameters: {
type: 'object',
properties: {
command: { type: 'string', description: 'The shell command to run' },
cwd: { type: 'string', description: 'Working directory' }
},
required: ['command']
}
},
{
name: 'search_web',
description: 'Search the web and return top results',
parameters: {
type: 'object',
properties: {
query: { type: 'string' }
},
required: ['query']
}
}
];When Claude decides it needs to run a command, it returns a special response with a tool_calls field. My server intercepts that, executes the tool, and sends the result back to Claude as a new message. The AI then uses that result to decide the next step. It's a loop — but now Claude is driving.
The first time I asked Claude to "fix the failing test in src/tests/api.test.js" and watched it run npm test, read the output, edit the file, and re-run until green — I actually laughed out loud. It was like watching a robot learn to ride a bike.
What I Learned: The Good, the Bad, the Dangerous
First, the good. Function calling turns Claude into an autonomous agent for well-scoped tasks. I now use it for:
- Database migrations: "Claude, add a 'role' column to the users table and update all existing users to 'member'." It runs the SQL, checks the schema, validates the data, and commits.
- API debugging: "This endpoint returns 500. Figure out why." It curls the endpoint, checks server logs, inspects database state, and often finds the bug before I finish my coffee.
- Web research + code generation: "Find the latest Stripe API for subscriptions and write a Node.js integration." It searches the web, reads docs, and writes tested code.
But there are sharp edges. The biggest risk is unintended side effects. Claude once ran rm -rf node_modules when I asked it to "clean up the project." I hadn't scoped the allowed commands. Now I sandbox everything — the run_shell_command tool only works inside a dedicated project directory, and I've blacklisted destructive commands like rm, dd, and chmod -R.
Another lesson: context windows fill up fast. Every tool call adds the full stdout/stderr to the conversation. For long-running tasks, I now truncate outputs to the last 100 lines and summarize intermediate steps using a secondary LLM call.
Key Takeaways for Developers
If you're thinking about adding function calling to your workflow, here's what I wish I'd known from day one:
- Start with read-only tools. Give Claude the ability to read files, search the web, and run non-destructive commands first. Add write capabilities gradually, with explicit user confirmation for each destructive action.
- Use structured outputs for tool results. Return JSON, not raw text. Claude parses structured data much more reliably. For example, instead of returning
"Error: file not found", return{ "status": "error", "code": 404, "message": "file not found" }. - Implement rate limiting and timeouts. Claude will happily run infinite loops if you let it. I set a 30-second timeout per tool call and a 5-minute overall session limit. The AI can request extensions, but it has to justify them.
- Log everything. Every tool call, every result, every decision. When Claude does something unexpected (and it will), you need to trace the chain of reasoning. I log to a local SQLite database with timestamps and conversation IDs.
Honestly, the biggest shift was mental. I stopped thinking of Claude as a chatbot and started thinking of it as a runtime. The chat interface is just the REPL. The real power is in the tools it can invoke.
Closing Thought
Function calling isn't a feature — it's a paradigm shift. It moves AI from being a reference librarian to being an active participant in the build process. Claude doesn't just tell you how to fix a bug; it fixes it, tests it, and asks you to review the diff. It doesn't just explain a concept; it searches the web, synthesizes information, and writes a summary.
I still use traditional chat for brainstorming and rubber-ducking. But for actual building, I've wired Claude into my development loop. The result? I ship faster, debug less, and spend more time on architecture decisions that actually matter. If you haven't tried giving your AI the ability to execute, you're still doing it the hard way.
Now if you'll excuse me, Claude is refactoring my authentication middleware. I'm just here to approve the PR.