A coding agent is six functions in a trenchcoat
Tidy design principles
SubscribeSign in
A coding agent is six functions in a trenchcoat
Hadley Wickham<br>Jun 19, 2026
34
Share
Coding agents like Claude Code, Cursor, and Codex have taken the software engineering field by storm. Since November 2025 they have radically changed the practice of software development for many programmers (including me), and in this week’s post I want to dive into what makes them tick.
We’ve now talked about tools (functions) and harnesses (a system for running tools on behalf of the LLM). And you know that an agent is a harness with tools for reading and writing. So what makes an agent a coding agent? The answer is the specific tools that it supplies: ones that let an LLM explore and edit a codebase the same way a human would.<br>Most coding agents provide some variation on the following tools:<br>Read file : read the contents of a file, or a subset of one.
Write file : create a new file with fresh content.
Edit file : make a targeted change to an existing file, usually by replacing one chunk of text with another.
List files : show the files and directories at a given path so the agent can orient itself in the project structure.
Search : find lines matching a pattern across the codebase so the agent can locate relevant code quickly.
Run command : execute an arbitrary shell command, which allows the agent to do anything else it might need.
A good coding agent also usually contains a big prompt with advice about good practices. While these prompts are not open source, they are fairly easy to sniff out, so various folks have extracted them. It’s informative to take a look just to get a sense of how complex they are.<br>Building a minimal coding agent
Of the six tools above, only three are really essential for a basic coding agent. To prove it, I’m going to build a tiny coding agent in R with ellmer. We’ll start ruthlessly minimal — with just read file, write file, and run command — and then work our way up. We’ll lose some niceties, but in exchange we get something you can read in one sitting.<br>First, the three tool functions. I’ve written them with deliberately boring R:<br>read_file Each function returns a string which is fed back to the LLM as the result of the tool call: read_file returns the file’s contents, write_file confirms what it did, and run_command returns whatever the command printed to the console.<br>Next we create a chat and register the three functions as tools. As before, the descriptions matter: they tell the LLM when and how to reach for each tool. We also include a very basic prompt.<br>library(ellmer)<br>#> Warning: replacing previous import 'S7:::=' by 'rlang:::=' when loading<br>#> 'ellmer'<br>chat Using model = "claude-sonnet-4-5-20250929".
chat$register_tool(tool(<br>read_file,<br>description = "Read the entire contents of a text file.",<br>arguments = list(<br>path = type_string("Path to the file, relative to the working directory.")<br>))
chat$register_tool(tool(<br>write_file,<br>description = "Write content to a file, overwriting it if it already exists.<br>Always read the file first if you only want to change part of it.",<br>arguments = list(<br>path = type_string("Path to the file, relative to the working directory."),<br>content = type_string("The full new contents of the file.")<br>))
chat$register_tool(tool(<br>run_command,<br>description = "Run a shell command and return its output. Use this to list<br>files (ls), search code (grep), run tests, or use git.",<br>arguments = list(<br>command = type_string("The shell command to run.")<br>))And that’s the whole agent!<br>The shell tool is our “get out of jail free” card because if you can run a shell command you can do anything: you can call ls to list directories, grep to search for code, Rscript to run R code, and git for git. (Technically we don’t even need read and write tools because the agent could use echo and cat, but we’re going to throw the agent a bone here.) The shell tool is also rather dangerous; we’ll come back to that later.<br>You can now use this agent for a simple task:<br>chat$chat("Find the function that parses dates and add a unit test for it.")Behind the scenes the model might use run_command to grep for the function then read_file to study it. Next, to create the test, it will need to read the existing test files with read_file, then write_file to add the new test, and finish up by using run_command to run the test suite. It’s no Claude Code, but it’s in the same galaxy.<br>Finding files: list and search
Our minimal agent can already list and search files via run_command, but leaning on the shell for everything has downsides. Shell commands vary between platforms (Windows doesn’t have ls or grep) and their output is noisy. More importantly, handing the model a general-purpose shell is a security nightmare: it’s very difficult to tell if a specific shell command is safe or dangerous. It’s much easier to add safeguards to stricter tools.<br>Let’s see what that might look like by implementing list and...