Why AI agents put 2>&1 at the end of everything
- What 2>&1 actually does
- The Bell Labs origin story
- Why AI agents can’t live without it
- How agents capture output in practice
- The gotchas nobody warns you about
- Fifty years of duct tape
If you’ve ever watched an AI coding agent work, you’ve seen it. Claude Code, Copilot, Cursor, Aider – they all do the same thing. Every shell command ends with 2>&1.
npm run build 2>&1
python -m pytest 2>&1
git status 2>&1
It looks like line noise. But it’s doing something important, and the reason it exists at all traces back to a frustrated engineer, a washing-machine-sized printer, and some very expensive paper.
What 2>&1 actually does
Every running process on a Unix system gets three communication channels, called file descriptors:
- fd 0 (stdin): where input comes from
- fd 1 (stdout): where normal output goes
- fd 2 (stderr): where error messages go
When you run a command in your terminal, both stdout and stderr show up on the same screen. You can’t tell them apart. But they are separate streams under the hood, and that separation matters when something other than a human is reading the output.
2>&1 tells the shell: “take file descriptor 2 (stderr) and point it wherever file descriptor 1 (stdout) is currently going.” The & before the 1 means “this is a file descriptor, not a filename” – without it, 2>1 would create a file literally called 1.
At the kernel level, this is a dup2() system call. One line of C. It copies a file descriptor pointer. That’s it.
The Bell Labs origin story
Here’s the part I keep coming back to. stderr didn’t always exist. Through Version 6 of Unix (around 1975), there was no separate error stream. Everything, normal output and error messages alike, went to stdout.
This was fine until Bell Labs got a C/A/T phototypesetter. It was a big, expensive machine that exposed characters onto photographic paper using a strobe and a spinning drum. After typesetting, you fed the paper through a chemical developer, waited several minutes, and pulled out your beautifully printed page.
Stephen Johnson, the creator of yacc and lint, told the story on the Unix Heritage Society mailing list. One afternoon, several people at the lab had the same experience: they sent a document to the typesetter, waited for the developer to process the paper, and unrolled it to find a single, perfectly typeset line:
“cannot open file foobar”
The error message went to stdout. The typesetter dutifully rendered it in whatever font and size had been specified. Minutes of machine time and expensive photographic paper, wasted on a beautiful error message.
The grumbling happened in front of the right people. Within days, Dennis Ritchie added a third file descriptor, stderr, so error messages would go to the terminal instead of getting mixed into program output. A pragmatic fix to a physical-world annoyance, not some grand design decision.
Why AI agents can’t live without it
When you type npm run build in your terminal and it fails, you see the error. Both stdout and stderr render on the same screen. You read the message, understand the problem, fix it.
AI agents don’t have terminals. They spawn shell processes and read the output through pipes. The problem is that a pipe only carries stdout by default. stderr goes to a separate pipe, or nowhere at all.
So when an agent runs a build command and it fails, the agent might see… nothing. The error message is on stderr. The agent only read stdout. From the agent’s perspective, the command produced no output and failed for unknown reasons.
What happens next is predictable and painful. The agent retries the same command. Or it guesses what went wrong and “fixes” code that was already correct. Or it spins in a loop, confused about why silence keeps coming back. This isn’t theoretical – GitHub is full of issues documenting exactly this failure mode across every major AI coding tool. Users of Copilot, Codex, and Claude Code have all reported the same thing: the agent can’t see what went wrong because the error was on stderr and only stdout was captured.
2>&1 fixes this by merging both streams before the pipe. The agent gets everything in one combined stream: normal output and errors together.
How agents capture output in practice
Most AI agent harnesses use one of two approaches in #python:
Merge at the shell level (what you see in the command):
npm run build 2>&1
The shell handles the redirect before the agent reads anything. Simple, universal, works everywhere.
Merge at the code level (inside the agent’s subprocess call):
result = subprocess.run(
command, shell=True,
capture_output=True, text=True
)
output = result.stdout + result.stderr
This captures both streams on separate pipes, then concatenates them. Anthropic’s reference implementation for the bash tool uses this pattern. Aider takes a slightly different approach with stderr=subprocess.STDOUT, which merges the streams at the file descriptor level before the parent process even sees them.
Either way, the goal is the same: make error messages visible to the model.
The gotchas nobody warns you about
The redirect order matters, and it’s counterintuitive.
# Wrong: stderr still goes to the terminal
command 2>&1 > file.log
# Right: both go to the file
command > file.log 2>&1
Bash processes redirections left to right. In the wrong version, 2>&1 copies stdout’s current destination (the terminal) to stderr. Then > file.log moves stdout to the file. But stderr still points at the terminal because 2>&1 was a snapshot, not a permanent link.
There’s also a buffering asymmetry that trips up agent developers. The C standard says stderr is unbuffered, so writes appear immediately. But stdout, when connected to a pipe instead of a terminal, is block-buffered. Output gets batched. This means the original chronological order of stdout and stderr messages gets scrambled when you capture them separately and concatenate after the fact. A program that prints out1, err1, out2, err2 might appear as out1 out2 err1 err2 in the agent’s view.
Some tools complicate things further by putting useful information on the “wrong” stream. curl writes progress to stderr. git writes some status messages to stderr. docker writes build progress to stderr. There’s no universal rule about what goes where.
Fifty years of duct tape
What I find interesting about 2>&1 is that it’s a seam. On one side, you’ve got a design decision from the 1970s, born from phototypesetter frustration at Bell Labs. On the other, you’ve got AI models from the 2020s trying to understand what went wrong with your npm build.
The AI agent has no idea what 2>&1 means. It doesn’t understand file descriptors. It just knows that appending those characters to shell commands makes the output more useful. Fifty years of #unix philosophy, compressed into six characters of duct tape, keeping the whole thing running.
Next time you see an agent tack 2>&1 onto a command, you’re watching a language model unknowingly pay homage to Dennis Ritchie’s quick fix for wasted phototypesetter paper. I think there’s something kind of great about that.
#ai #programming #linux #nostr
Write a comment