The Unix philosophy is the oldest working answer to the question the AI industry is now asking: how do you build something powerful out of parts you can still understand? It was written down in 1978, and it fits an ecosystem of language models, tools and harnesses better than most things written since.
The philosophy, in three sentences
Doug McIlroy, who invented the pipe, put it this way in the Bell System Technical Journal:
- Make each program do one thing well. To do a new job, build afresh rather than complicate old programs by adding new features.
- Expect the output of every program to become the input to another, as yet unknown, program. Don’t clutter output with extraneous information.
- Design and build software to be tried early, ideally within weeks. Don’t hesitate to throw away the clumsy parts and rebuild them.
Everything else is commentary. Rob Pike’s rules on measurement before optimisation, Eric Raymond’s seventeen rules in The Art of Unix Programming, the habit of text streams as the universal interface, the small sharp tool, silence on success and a clear exit code on failure: all of it unpacks those three sentences.
The philosophy is not about operating systems. It is a theory of how to compose work from parts that were never designed together, by agreeing on very little: a stream in, a stream out, a number at the end that says whether it worked.
Why the agent ecosystem forgot it
The first generation of AI agents is a monolith. One model, one context window, one enormous prompt, forty tools bolted to the side, and a loop that runs until the model says it is finished. It does everything, so it does nothing well, and when it fails nobody can say which part failed.
This happened for an understandable reason. A language model is the first program that can read a man page, so the temptation is to hand it the whole machine. Why write a pipeline when the model can shell out? Why define a tool’s interface when the model can improvise one? Why keep output clean when the model will read it anyway?
The cost arrives later, and it is the cost McIlroy warned about in his first sentence. A monolith agent cannot be tested in parts, replayed in parts or trusted in parts. Its output is prose mixed with data, so nothing downstream can consume it without another model to parse it. Its failures are silent, because a model that hits an error explains the error in fluent English and carries on. And every new job is added to the old prompt, which grows until no one knows what it does.
None of this is new. It is 1975 again, with every program a thousand-line shell script that reads everything, writes everything and exits zero. The answer is also not new.
The translation
Almost every Unix idea has a direct counterpart in a well-built agent system. The table is the argument of this post: the names changed, the shapes did not.
| Unix | Agentic ecosystem | In Cazi |
|---|---|---|
| A program | A tool: one container image, one job | A task from a template |
| A process | One run of that tool, with a start, an end and a record | A task, event-sourced from submitted to completed |
| The shell | The harness that launches programs, wires them and enforces limits | The control plane |
| The user at the prompt | The model, choosing what to run next | The agent in a run |
stdin and stdout |
Data in, data out, nothing else on that channel | /pipe/stdin mounted from the previous step’s stdout |
stderr |
Logs, progress, complaints: never mixed with data | The container’s stderr stream, kept apart |
| The pipe | Output of one tool becomes input of the next, unknown in advance | A pipeline step with stdin_from_prev: true |
| Text streams | Structured lines any tool can read: JSON Lines | output_format: ndjson, one object per line |
| The exit code | A machine-readable verdict, separate from the output | Task state plus the agent’s reported outcome |
| Users, groups, permissions | What an actor may touch, decided outside the program | Agent scope: access, images, nodes, connections, subjects |
ulimit and set -e |
Hard ceilings that stop a runaway process | Eleven circuit breakers: tokens, turns, wall clock, spend, drift |
syslog |
An append-only record of what ran, as whom, with what result | The hash-chained activity log |
cron |
The same small job, on a timer | Schedules |
| The man page | How to call the tool, what it reads, what it writes | Templates and skills |
| A shell script | A saved composition of small tools | A pipeline template |
| A core dump | Enough of the failure kept to study it afterwards | Learnings, proposed from tripped breakers and failed tasks |
The column that matters is the middle one. Cazi is one way to build it. Any harness that keeps these distinctions will inherit the properties that made Unix last.
The same picture, fifty years apart
The pipe survived. Above, a person types subfinder | dnsx | httpx at a shell. Below, the model declares the same three tools as tasks and the harness pipes them through /pipe/stdin. Drawn as text, rendered by Vizo.
The three programs did not change. What changed is who types the line and what the shell enforces around it.
The rules, one by one, inside a harness
Raymond’s rules read as if they were written for agent systems. Six of them do most of the work.
Modularity: simple parts, clean interfaces. A tool is one image with one command. It takes a target and, when chained, reads the previous step’s output. It does not know what runs before or after it. The harness knows that, which is why a template verified once stays correct.
Composition: design programs to be connected. The pipe is the whole idea. A model that can chain three small templates needs no permission to do anything the three templates cannot already do, and the harness can check each link. A model that writes a && b && c inside one container has built a monolith the harness cannot see into.
Separation: policy from mechanism. The tool is mechanism. Who may run it, against what, how often, at what cost, is policy, and policy lives in the harness, not in the prompt and not in the tool. Unix put this in the kernel and the file mode bits. A harness puts it in scope and breakers. A prompt rule is policy written where the mechanism can argue with it.
Transparency: design for visibility. Every process has a pid, a parent and an exit status. Every task has an id, an actor, the person it was done on behalf of, a chain back to the run that asked for it, and a record that cannot be edited later. Transparency is not a dashboard. It is the property that a stranger can reconstruct what happened from the record alone.
Silence: when a program has nothing surprising to say, say nothing. On stdout, that is. A tool that prints a greeting, a progress bar and a summary onto its data channel has broken every pipe downstream. Data on stdout, as JSON Lines. Logs on stderr. Models are the worst offenders here, because fluent explanation is what they do; the harness has to keep the two channels apart for them.
Repair: when you must fail, fail noisily and as soon as possible. A tripped breaker pauses the run and tells a person. A failed task is a failed task, not a paragraph beginning “It seems the command did not succeed, so I will try another approach.” The non-zero exit code was the most important invention on this list, and the first thing a chatty agent loses.
Where the analogy breaks
Four places, and each one is a design decision rather than a footnote.
The user is not deterministic. Unix assumed a person at the prompt who, given the same situation, would mostly type the same thing. A model will not. The same goal on two runs can produce two different pipelines. The tools stay deterministic; the composition does not. So the harness has to record the composition, not just the parts: which template, which version, which arguments, in which order, decided by whom. Replay then means replaying the record, not asking the model again.
Text is no longer the universal interface. McIlroy’s streams were plain text because that was what every program could read. A model can read anything, which is a trap: it will happily consume a page of mixed prose and numbers and extract what it thinks is there. The discipline is stricter now, not looser. JSON Lines on stdout, one object per line, typed where it matters, because the next reader may be a model that will trust whatever it is given.
Processes were free; turns are not. fork cost microseconds. A model turn costs money, time and a slice of a finite context. The Unix instinct to spawn freely has to be paired with budgets the shell never needed: a spend ceiling per run, a turn ceiling per task, a tree budget for delegation. Cazi prices every turn in integer micros and treats an unpriced model as not free, which is the cost equivalent of refusing to run as root.
The model wants to be both shell and program. The hardest break. A model can write the tool, choose the tool and run the tool, and every agent framework that lets it do all three has rebuilt the monolith. The Unix answer is the separation of roles: the user proposes, the shell enforces, the program executes. A model proposing a template is fine. A model editing its own allowlist is a program rewriting the shell, and no harness should permit it.
What survives these breaks is the core. Small tools, clean streams, composition under a shell that enforces what the programs cannot. The breaks tell you where the shell must be stricter than Bourne ever was.
A worked pipeline
Here is the recon pipeline from Cazi’s own documentation. Three tools, none of which knows the others exist.
name: recon
steps:
- name: subdomains
image: projectdiscovery/subfinder:latest
command: ["-d", "example.com", "-silent"]
- name: resolve
image: projectdiscovery/dnsx:latest
stdin_from_prev: true
command: ["-l", "/pipe/stdin", "-silent", "-a", "-resp-only"]
- name: probe
image: projectdiscovery/httpx:latest
stdin_from_prev: true
command: ["-l", "/pipe/stdin", "-silent", "-json", "-title", "-status-code"]
Read it as a shell line: subfinder | dnsx | httpx. Every Unix habit is present.
- Each step does one thing. Enumerate names, resolve them, probe them. Swapping the resolver changes one line.
- Each step is silent on success. The
-silentflag strips banners so stdout carries only names, then only addresses, then only JSON. Anything a tool wants to say about itself goes to stderr, where the harness keeps it as a log. - The pipe is explicit. The harness captures one step’s stdout and mounts it read-only at
/pipe/stdinin the next container. The tool reads a file. It never has to care that the file came from another process on another node. - The last step emits JSON Lines, so
output_format: ndjsonturns it into structured results without a model parsing prose. - The composition is a template. Saved with a
TARGETplaceholder, it runs again next month from a schedule with one argument changed, and a model that needs it calls one template instead of inventing three commands.
When an agent runs this, it never shells out. It submits the pipeline as an intent, the harness checks the three images against its scope, runs each step as its own process with its own record, and hands the agent the JSON. If the probe step loops or the run exceeds its ceiling, a breaker stops it and a person is asked. If the subdomain tool changes its flags, the template is fixed once, and a learning records why.
That is McIlroy’s second sentence, made load-bearing: the output of each tool became the input of a tool its author never heard of, including, now, a language model.
The shell, the user and the program
Unix lasted because it separated three roles and let each one be replaced. The user decides what to do. The shell launches it, connects it and enforces the limits. The program does one thing and reports a number.
The agentic ecosystem has the same three roles with new occupants. The model is the user at the prompt: fluent, fast, fallible, and not to be trusted with the shell’s job. The harness is the shell, and it must be stricter than any shell before it, because its user is not deterministic and its processes are not free. The tools are the programs, and the best of them are the same small sharp tools Unix always had, in containers now, reading /pipe/stdin instead of file descriptor zero.
Get the roles right and the rest of the philosophy follows: small parts, clean streams, silence on success, a loud and early failure, and a record that outlives the run. Get them wrong and you have a thousand-line script that exits zero.
Fifty years on, the question has not changed. Build the small thing well, expect its output to feed something you have not imagined yet, and throw away the clumsy parts. Do that, and the model is just the newest user at an old and well-made prompt.
This is the fourth note in a series on Cazi, after Cazi: A Deterministic Harness for AI Agents, What an AI Agent Is and What an AI Harness Is.
Discussion
0 commentsNo comments yet. The first perspective opens the thread.