Building Claude Code from Scratch: A Minimal Agent in 393 Lines of C++ MoonieCode, a minimal AI coding assistant built in 393 lines of C++23 that communicates with Claude via HTTP requests. Its core logic is a single while loop that sends user prompts as JSON to an LLM API, then handles two possible responses: direct text answers or tool call requests for file operations and shell commands. The project uses C++'s `std::variant` and the `overloaded` pattern to enforce type-safe handling of both response types at compile time. An AI coding assistant that reads your files, writes code, and runs shell commands. The core logic? A single while loop. I thought it was bullshit too, until I built one myself. The project is called MoonieCode, and the code lives here: https://github.com/Tenaryo/MoonieCode https://github.com/Tenaryo/MoonieCode . Written in C++23, clocking in at 393 lines of source 637 if you count tests . Here's what it looks like in action: $ ./moonie-code -p "list all .cpp files in the project" A few seconds later Claude spits back your file list. What just happened? You gave it a sentence, it threw that sentence into an HTTP request, shipped it off to a Claude Haiku model somewhere in the cloud, Claude decided it needed to run find , MoonieCode ran it for Claude, fed the output back, and Claude formatted it into something human-readable. That first step wasn't running bash. First it had to talk to the LLM. So let's start there: how do you get C++ and Claude to shake hands? Shaking Hands with Claude Talking to an LLM boils down to two moves: you HTTP POST a blob of JSON at it, and it sends a blob of JSON back. MoonieCode's HttpClient is a 25-line class whose guts are basically this: cpr::Response response = cpr::Post cpr::Url{base url + "/chat/completions"}, cpr::Header{{"Authorization", "Bearer " + api key }, {"Content-Type", "application/json"}}, cpr::Body{request body.dump } ; cpr is a C++ wrapper around libcurl that handles the HTTP plumbing so you don't have to. You stuff your API key into the Authorization header, pack your JSON into the body, and POST to OpenRouter, an LLM API gateway that forwards the request to Claude for you. So what's in that JSON? Two things: messages and tools . messages is an array holding the conversation history between you and Claude. At the start it's just one entry: {"role": "user", "content": "list all .cpp files in the project"} tools is another array that tells Claude "here's what you have at your disposal." Each tool is a JSON object with a name, a description, and a parameter schema. Claude scans the list and goes, alright, I can ask this program to read files, write files, and run commands for me. After you fire off the request, Claude sends back a JSON response. And here's where it gets fun: Claude's response comes in exactly two flavors. Flavor one, straight text. You ask "what's 1+1" and it just answers: {"choices": {"message": {"content": "1+1 equals 2"}} } Flavor two, tool call. You ask it to "list all cpp files" and it can't answer directly, so it asks for help: {"choices": {"message": {"tool calls": { "id": "call abc123", "function": { "name": "Bash", "arguments": "{\"command\": \"find . -name ' .cpp'\"}" } } }} } It's saying "I can't do this myself, but run this command for me and I'll take it from there." Notice arguments is a string containing more JSON, Claude packed a shell command inside it. Now the hard part: how does your code tell these two cases apart? If Claude gives you text, print it. If it wants a tool run, execute the tool. You need those two paths separated cleanly. MoonieCode solves this with a very C++ move: using ParsedResponse = std::variant