Skip to content

A Pair of Missing Hands

Edit source

No eyes, no hands

Part 2 of 3ofDriving Eternal
  1. 1The Room Across the Tunnel
  2. 2A Pair of Missing Hands
  3. 3ET Phone Home

An agent I was pairing with needed to restart a service on a remote box. It knew the command. It knew the host. And it just sat there, because the only way onto that machine was a terminal built for my eyes and my hands, and an agent has neither. It cannot watch a prompt redraw. It cannot tell the echo of what it typed from the answer that came back. It has no fingers to hit Enter, and no way to know whether the thing it ran worked or failed.

I had a durable, eternal terminal onto the machine I live on, Eternal Terminal, and it felt like home precisely because every inch of it, the prompt, the colors, the living terminal et hands you, was made for a person. The thing that makes it feel like home is the same thing that locks everyone else out.

The agent was only the newest arrival at a wall people had been bruising themselves against for decades. Every team has someone who automates the toil so nobody else has to think about it. Picture her clearly enough and she gets a name. Maya.

It’s two in the morning and an alert is going off. The fix is three commands on a box in another datacenter, the kind of thing Maya has typed by hand a hundred times. Tonight she wants it to run itself, so she does what any of us would: she writes a small script to log in and type the commands for her.

The box wants a password, and then a prompt, and then a [y/N] it expects a human to answer. A plain script can’t answer a question it can’t see, so Maya reaches for expect, the thirty-year-old tool built for exactly this, and teaches her script the words to watch for. It works. She goes to bed.

Wednesday it breaks. The remote tool printed Proceed? where it used to print Continue?, and her script, waiting on a word that never comes, hangs until it times out. She fixes the pattern. Thursday a different prompt has moved. The trouble with teaching a script to read a screen built for a person is that the screen keeps changing underneath it, and nothing anywhere promised it wouldn’t.

And even on a good run she can’t fully trust it. What comes back is the prompt, and the color codes, and the echo of her own keystrokes, braided together with the one line she actually wanted. Worse, when a remote command fails, her script often can’t tell, because the interactive shell hands back a screenful of text and no clean exit code that says this worked or this didn’t. She once watched a deploy script report a tidy success over a deployment that had quietly failed. That one cost her a Saturday.

I went looking to find out whether this was just me and Maya and one bad Saturday, and found a whole genre of suffering, decades deep and very well attended.

The classic tool for answering prompts from a script is expect, and its Python cousin pexpect. Between them they carry more than 3,600 questions on Stack Overflow (2,809 for expect, 817 for pexpect), which is a lot of confusion for tools whose whole job is to make automation easy. And the reason they’re hard isn’t a bug, it’s physics, which pexpect’s own docs admit out loud: because it reads a pseudo-terminal one character at a time, “the $ pattern for end of line match is useless,” and when the child prints a newline “you actually see \r\n.” You aren’t reading output. You’re parsing a screen.

How hard is parsing a screen? The single most-viewed pexpect question on Stack Overflow is just someone asking how to read the output of a command they ran. It has more than 147,000 views, and the catch is that the first thing you read back is the echo of your own command, not its result, which is precisely the braided mess Maya kept fishing in. One developer who tried to skip the libraries and do it with Python’s raw pty module left a verdict I think about often: “the pty library is not fit for human consumption. The docs, essentially, are the source code.”

The exit code is its own quiet heartbreak. Run a command over ssh the way a script wants to, with no terminal attached, and ssh greets you with Pseudo-terminal will not be allocated because stdin is not a terminal. That one sentence has its own Stack Overflow question with over 780,000 views. Attach a fake terminal to make the warning go away and you’re back to parsing a screen. Get the quoting subtly wrong and the failure goes silent: there’s a well-worn question titled “bash script executed over ssh returns incorrect exit code 0,” where the remote command fails and ssh reports success anyway. Maya’s lying deploy, filed as a bug report.

My favorite of the lot is the smallest. Someone’s automated file copy kept breaking, and the culprit turned out to be a friendly echo in the remote .bashrc, a line of greeting meant for a person. The machine on the other end couldn’t tell the greeting from the data. The accepted answer draws the only lesson there is, and it happens to be the whole thesis of this story: “make separate accounts for humans and for machines (scripts), or just stop tattling via .bashrc.”

That’s the wall, and it’s been there thirty years. We even keep a shelf of tools that exist only because one corner of it, the dropped connection, hurt us enough to fix: Mosh, autossh, and Eternal Terminal itself, all built so a session can survive “network outages and IP roaming.” We fixed the part that hurt the humans. The part that locks out everything without hands, we mostly just learned to live around.

I kept turning it over until it came clear. The problem in front of me wasn’t a smarter agent. It was a pair of hands.

Everything I reach for on that box, the shell, the prompt, the pager, the colored output scrolling past, was made for a person: for eyes that read and fingers that type. An agent shows up with neither and finds a world shaped end to end around a body it doesn’t have. And almost every tool any of us has ever made is exactly like this. We built all of it for us. The terminal, the dashboard, the wizard with the Next button, the confirmation dialog, the form that wants a click. A whole civilization of interfaces, every one assuming eyes and hands on the other side.

So the work turns out to be quieter than making the models bigger, and a good deal more fun. It’s walking the human-shaped world one tool at a time and giving each thing a handle that something without a face can hold. A terminal becomes a socket. A prompt becomes an exit code. A twelve-second wall becomes a fifth-of-a-second room. None of it makes the agent think any harder. It just lets the agent in.

I didn’t know yet how many sharp edges were waiting in a terminal that never once expected to be driven by something without hands. There turned out to be more than I thought. But the shape of the fix was already clear: take the eternal terminal I love, the one built end to end for a person, and give it a handle a machine can hold.

Building that handle meant teaching a terminal that had only ever answered to a human to answer to something that wasn’t one, and the machine on the far end didn’t give that up without a fight.

The first thing I built wasn’t inside et at all. It was a Python wrapper called etch.py that ran Eternal Terminal inside a pseudo-terminal, kept the session warm, and scraped the rendered screen to hand a script back clean output and a real exit code where before there was a wall of color codes and tea leaves. It worked. It’s still on my machine.

But look at what it took. It neutralized the shell prompt so the Powerlevel10k banner would stop bleeding into the output, stripped ANSI escape codes by hand, wrapped every command in printf sentinels to carve the real output out of the stream, chunked long lines so the kernel’s input cap wouldn’t silently truncate them, resynced the session with a fresh nonce when a command wedged, and paid for a Python interpreter to start on every single call. None of that is et. All of it is a program outside the glass, reconstructing from a picture of a screen the structured thing et already had on the inside and never offered to anyone.

The wrapper earned its keep twice over. It settled the vocabulary an agent actually wants, and it kept failing in one shape. A piped stdin that hung. A kilobyte-long line that the PTY’s canonical mode chopped, leaving truncated junk that poisoned the warm channel until a full stop-and-sweep. Both were a scraper guessing at a stream built for eyes, and guessing wrong at an edge I’d never have hit by hand. I fixed them, and the fixes were better guesses, not a different idea.

The different idea was to stop guessing. et has that clean stream on the inside; I just couldn’t reach it from out here. So the version worth talking about is the one I built into the terminal itself. That’s the next part.