nanoAgent: A 100-Line Python Agent Loop You Can Read in One Sitting
If you can read ~100 lines of Python, you understand agents.
At a glance
- What is it?
- nanoAgent wraps OpenAI function calling in roughly 100 lines and exposes three tools: bash, read_file, write_file. It is a teaching artifact and a base to fork, not a sandboxed runtime, and the README is honest about the missing pieces.
- Who is it for?
- Adopt nanoAgent if you want to read a complete agent loop in one sitting or need a base to fork for a narrow task. Do not adopt it as-is for anything touching untrusted input or a shared machine: execute_bash runs commands directly on the host with no sandbox, no allowlist, and no confirmation step, and the README documents no such control.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Activity is slowing. The repository last received commits 6 months ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
What nanoAgent Actually Is, and Who Should Read It
nanoAgent is a single-file Python program that turns a natural language instruction into a sequence of shell commands and file operations, executed on the machine where you run it. The README frames the project with a Thoreau quote and a one-line promise: enough code to understand agents, not enough code to hide anything. Its author points readers toward a companion repository, sanbuphy/nanoMCP, for anyone who wants to see how tools are fetched in a more modern way.
The intended reader is someone who has used an LLM API and wants to know what the word agent means mechanically. The README claims the whole thing is about 100 lines. That claim is the product. If the code were 3,000 lines, the repository would have no reason to exist, because the loop it implements is not novel. What is being sold is legibility: you can open agent.py, read every line, and hold the entire control flow in your head.
That makes it a poor fit for anyone who wants a production runtime. There is no plugin registry, no retry policy beyond what the loop does, no observability layer, no permission model. The README does not present any of those as goals. It presents three tools and a for loop. Judge it on that basis.
The Loop: Model Call, Tool Dispatch, Message Append
The mechanism is OpenAI function calling, and the README shows the skeleton directly. Tools are declared as a list of JSON schema objects under the key tools. The loop runs for a bounded number of iterations, which the README writes as for _ in range(max_iterations). Each pass sends the accumulated messages list plus the tools list to client.chat.completions.create.
If the response contains no tool_calls, the loop returns the message content and the agent stops. If it does contain tool calls, the code looks up the function by name in a dict called available_functions, calls it with the parsed arguments, and appends the result to messages as a message with role tool. Then the next iteration begins. The model sees its own tool output and decides what to do next, including deciding that it is done.
The data flow is entirely in-process. There is no queue, no worker, no state file. Conversation history lives in a Python list and dies when the process exits. That means a long task cannot be resumed after a crash, and there is no way to inspect a past run. For a teaching tool this is the right call, because persistence would add code that obscures the loop. For anything you intend to run unattended, it is a real constraint.
The README also notes a hardening change: malformed JSON in tool arguments, or a tool name that does not exist in available_functions, is now returned to the model as an explicit tool error instead of crashing the process. That detail matters more than it looks. It means a model that hallucinates a function name gets a chance to correct itself on the next iteration rather than taking down the run.
Three Tools, and the Blast Radius of execute_bash
The capability list is short: execute_bash, read_file, write_file. The README describes execute_bash as running any bash command. Not a subset. Not a command allowlist. Any command.
The consequence is that the security boundary of nanoAgent is the same as the security boundary of your shell. If the model decides the task requires rm -rf on a directory, nothing in the described architecture stops it. There is no confirmation prompt mentioned in the README, no dry-run mode, no path restriction on read_file or write_file, and no container. The agent runs with your user's permissions against your real filesystem.
This is not a bug in the sense of a coding mistake. It is the design, and it is the same design most minimal agent demos use, because adding a permission layer would triple the line count and defeat the point. But it defines the deployment envelope precisely. Run nanoAgent on a scratch machine, in a disposable container, or on a directory tree you are willing to lose. Do not point it at a machine holding credentials you care about, and do not wire it to an input channel that strangers can reach, because prompt injection through file contents is a live path: read_file pulls text into the model's context, and that text can contain instructions.
The README does not discuss prompt injection. That omission is worth naming, because the tool set makes the project exposed to it in a way that a read-only agent would not be.
Getting It Running: Commands and Environment Variables
Installation is a single pip command against the repository's requirements file:
pip install -r requirements.txt
Configuration is entirely through environment variables, and the README gives platform-specific syntax. On macOS or Linux you export OPENAI_API_KEY, and optionally OPENAI_BASE_URL and OPENAI_MODEL. On Windows PowerShell the same names are set with $env:. On Windows CMD they are set with set. The README's examples use https://api.openai.com/v1 as the base URL and gpt-4o-mini as the model, and marks both as optional.
The optional base URL is the interesting one. Because it is overridable, nanoAgent is not tied to OpenAI's endpoint. Any service that speaks the same chat completions API with function calling support can be substituted by changing that variable. The README does not list which providers have been checked, so treat compatibility as something you verify rather than assume, particularly around tool call formatting.
Invocation is a single positional argument, the task in quotes:
python agent.py "list all python files in current directory" python agent.py "create a file called hello.txt with 'Hello World'" python agent.py "read the contents of README.md"
There is no interactive mode described, no config file, no flags. One process, one task, then exit. The README's examples include a combined task, finding all .py files and counting total lines of code, which is the kind of request that forces multiple loop iterations and therefore exercises the tool-calling path rather than a single round trip.
The Bounded Iteration Count Is a Design Choice, Not a Detail
The loop is capped by max_iterations. The README does not state the default value or whether it is configurable from outside the source. This is the kind of thing you check before relying on the tool, because the cap is what stands between a confused model and an infinite loop of tool calls that burn API spend.
Consider the failure mode. A model that misreads a task can call execute_bash repeatedly, each time receiving output that does not resolve the ambiguity, each time trying again. Without the cap, that run continues until you notice. With the cap, it terminates and returns whatever the model last produced, which may be an error message or a partial result. The README's hardening note covers malformed arguments and unknown tool names, but it does not claim to detect a model that is looping productively-looking commands. The iteration cap is the only backstop described.
There is a second, quieter failure mode: the agent can report success on a task it did not complete. The loop returns the model's final text as the answer. Nothing in the described architecture verifies that the file was actually written or the command actually succeeded. Exit codes from execute_bash are not mentioned as being inspected. So if you build automation on top of nanoAgent, parse the tool results yourself rather than trusting the final sentence.
How It Differs From LangChain-Style Frameworks
The obvious alternative is a general agent framework such as LangChain, which offers tool abstractions, memory backends, callback systems for tracing, and a catalogue of prebuilt integrations. The difference is not quality. It is where the abstraction sits.
LangChain inserts layers between your code and the API call: an agent executor, a tool interface, a memory object, a callback manager. Those layers buy you swapping components without rewriting the loop, and they cost you the ability to see the whole request path in one file. When something goes wrong in a framework agent, you debug across several packages. When something goes wrong in nanoAgent, you read agent.py.
For a team shipping an agent into production with tracing, retries, and multiple tool providers, the framework is the better starting point, and rewriting nanoAgent to reach that point means rebuilding most of what the framework already has. For someone learning the mechanics, or building a narrow tool that does one thing on one machine, the framework's layers are overhead you will spend a day removing. The README's pointer to sanbuphy/nanoMCP suggests the author sees the tool-fetching problem as the natural next step past this repository, which is a reasonable place to draw the line.
Maintenance, Licence, and What to Verify Before You Depend On It
The licence is MIT, stated in the README and in the repository metadata. MIT permits commercial use, modification, and redistribution provided the copyright notice and permission notice are retained. It also disclaims warranty, which for a tool that executes arbitrary bash commands on your machine is worth reading literally rather than skimming. Nothing here is legal advice; if you are folding the code into a product, have someone check the notice requirements.
The repository has no releases. The README lists no version number, so there is no tagged artifact to pin. Installation pulls dependencies from requirements.txt at whatever versions are current, which means a fresh install months from now may resolve differently than one today. If you fork it, pin your dependency versions yourself.
Maintenance cost is low in absolute terms because the surface is small, but that cuts both ways: there is no changelog to follow, and the last push timestamp is the only signal of activity. Before depending on it, read agent.py end to end, since the README describes the architecture but not every line. Confirm the default max_iterations and how to change it. Check whether execute_bash captures stderr and the exit code or only stdout. And decide, before the first run, which machine you are willing to let it operate on, because that decision is the one the code will not make for you.
Editorial conclusion
Adopt nanoAgent if you want to read a complete agent loop in one sitting or need a base to fork for a narrow task. Do not adopt it as-is for anything touching untrusted input or a shared machine: execute_bash runs commands directly on the host with no sandbox, no allowlist, and no confirmation step, and the README documents no such control. Before committing, verify the contents of agent.py yourself, check whether max_iterations is configurable or hardcoded, and confirm that every tool error path returns a message to the model rather than raising.
Community notes