AI · Agent Engineering
Terminal Coding Agents: How CLI-Based AI Agents Work
Why a shell — not a bespoke sandbox — is the environment that makes coding agents genuinely useful, and how the propose-execute-observe loop underneath them actually works.
A growing category of AI coding tool doesn't live in a chat window at all — it lives in your terminal, next to the shell you already use to build, test, and ship software. These terminal coding agents read your files, run your commands, and edit your code directly, in the same environment a human engineer works in. That choice of environment — the terminal, rather than a narrower, purpose-built sandbox — turns out to matter as much as anything about the underlying model.
Why the terminal, specifically
A sandboxed "run some Python" tool gives a model one capability. A shell gives it all of them, for free. The same terminal that a human developer uses to check out a branch, install a dependency, run a linter, execute the test suite, and inspect a diff is available to the agent unmodified. Nothing about git, npm/pip/cargo, pytest, grep, or the build system needs a bespoke integration — they're just programs the shell already knows how to run.
That matters because real engineering tasks are rarely a single isolated action. Fixing a bug might mean reproducing it, reading three files to find the cause, editing one, re-running the tests, noticing a lint failure, and fixing that too. A terminal agent composes these steps the same way a person would — one command at a time, each informed by the last one's output — instead of being limited to whatever fixed set of capabilities a narrower sandbox happened to expose.
The core loop: propose, execute, observe
Strip away the branding and every terminal coding agent runs the same loop. The model proposes an action — run this command, read that file, apply this edit. A harness sitting between the model and the machine executes the action against a real (or sandboxed) shell and filesystem. Whatever comes back — stdout and stderr, a diff, a file's new contents, an exit code — becomes the next observation, appended to the transcript the model sees on its next turn. The loop repeats until the model judges the task complete, or a human stops it.
while not task_done:
# 1. Model proposes the next action, given the running transcript
action = model.next_action(transcript)
# 2. Harness checks the action against the permission policy
if policy.requires_confirmation(action):
approved = ask_user(action)
if not approved:
transcript.append(denied(action))
continue
# 3. Harness executes it against the real shell/filesystem
result = shell.execute(action) # run_tests, edit_file, read_file...
# 4. The output becomes the next observation
transcript.append(observation(action, result))
task_done = model.judge_done(transcript)Nothing here is exotic — it's the same read-eval loop a human debugging a failing build runs by hand. The interesting engineering is almost entirely in step 2 and 3: how faithfully the harness executes what the model asked for, and how much of the real environment's output it feeds back.
Two ways an action actually gets executed
Models can express "run this command" in two different ways, and the difference has real consequences for reliability.
With structured tool-calling, the model emits a well-defined function call — a tool name plus typed arguments, like run_command(cmd: string, cwd: string) — that the harness parses deterministically and executes directly. There's no guessing involved: the boundary of the command, its arguments, and its intent are all explicit in the structure of the call itself.
With free-form text output, the model instead writes something like a shell command inside a code block or a sentence, and the harness has to pattern-match or parse that text to extract the actual command before running it. This works, and it's how early agent harnesses often operated, but it introduces an ambiguous parsing step — a stray character, an unexpected format, or a command that spans multiple lines can all break the extraction. Structured tool-calling has become the preferred approach for exactly this reason: it removes an entire class of parsing failures between "the model decided what to do" and "the harness did it."
Permissions: the blast radius is the whole machine
A model confined to a narrow sandbox — say, a tool that can only execute a snippet of Python in an isolated interpreter — has a naturally bounded blast radius. A model with real shell access does not. The same terminal that lets it run your tests also lets it delete a directory, force-push over history, or install arbitrary packages. That capability is precisely what makes the terminal powerful, and precisely what makes unsupervised use of it risky.
Permission tiers aren't optional
Because a shell can do almost anything a human at that terminal could do, terminal coding agents typically classify actions into tiers — freely allowed (reading a file, running a test), requires confirmation (writing a file, installing a dependency, anything with side effects outside the project), and blocked (destructive commands, credentials, network calls to unapproved destinations). Getting this policy right is as much a part of the system's design as the model choice itself — the smartest model is still dangerous without a sensible boundary around what it's allowed to do without asking.
Why iteration speed is the real unlock
A single call to a model that generates code in one shot has to be right the first time, or close to it — there's no feedback available to correct course. A terminal agent doesn't have that constraint. It can make an edit, run the actual test suite, read the actual failure, and try again — as many times as the loop allows.
What makes this loop effective rather than just chatty is that the feedback is concrete and fast: a compiler either accepts the code or it doesn't; a test either passes or fails with a specific assertion message; a linter names the exact line it objects to. That's a much sharper signal than a model second-guessing its own output in the abstract, and it's available precisely because the agent is operating in the same environment where those tools already live — the terminal.
Beyond code
None of this is actually specific to programming. The pattern — an agent takes an action, observes a real and fairly immediate result, and adjusts before trying again — generalizes to any task where "try it and see what actually happens" is possible: querying a database and checking the result set, adjusting infrastructure and re-running a health check, editing a config and re-deploying to see if it holds. Coding just happens to be the domain where that feedback loop is fastest, cheapest, and most precisely measurable, which is why it's where this class of agent matured first.
Key takeaways
- The terminal gives an agent the same toolchain a human developer already uses — git, package managers, test runners, linters — composed the same way, with no bespoke integration per tool.
- Every terminal agent runs the same loop: the model proposes an action, a harness executes it against the real shell/filesystem, and the output becomes the next observation.
- Structured tool-calling (typed function calls the harness parses directly) is more reliable than free-form text the harness has to extract commands from.
- Because shell access is broad enough to do real damage, permission tiers — allowed, requires-confirmation, blocked — are a core design concern, not an afterthought.
- Fast, concrete feedback (compiles or doesn't, tests pass or fail) is what makes iterative self-correction work better than one-shot generation.
- The same agent-plus-feedback-loop pattern generalizes to any task where an action can be tried and its real result observed, not just coding.
