Overview
May 29, 2026

Every harness gets background processes wrong

Here’s a loop that runs dozens of times a day in my workflow. An implementer finishes a phase of a plan and has to run its required checks before reporting done. It kicks off cargo test through the process manager, ends its turn, and goes quiet. Minutes later the tests finish, and the runtime starts a new turn for that agent with the status, exit code, output preview, and log paths injected. The agent reads the result and reports the phase complete. Nobody polled. No tokens were spent on “still running.” If I wanted to watch the tests scroll by in the meantime, I clicked the process in a sidebar and read the logs, and at no point was a terminal involved.

That loop is built on infrastructure I never planned to build. The process manager in goldengoose exists because both providers underneath it got background processes wrong, in different ways, and the more I tried to specify what I wanted instead, the more obvious it became that this layer couldn’t be rented.

Two providers, two philosophies of the terminal

goldengoose sits on top of the Codex app server, a stdio-driven server that runs via the codex binary, and Claude Code through the Claude Agent SDK. A big part of the product is that one UI drives both. Which means every place the providers disagree becomes my problem, and nowhere do they disagree more than on what it means for an agent to use a terminal.

Codex thinks in TTYs. Its unified exec tools, exec_command and write_stdin, run commands against PTY-backed sessions, each with a real allocated pseudo-terminal, and write_stdin sends keystrokes into the running session, simulating a human at a keyboard. Claude Code thinks in background shells: today, the Bash tool takes a run_in_background flag, hands back a shell ID, and the model checks on it by calling BashOutput, which returns whatever new output accumulated since it last looked. I say “today” because when I built my layer, Claude Code’s background tasks wrote their logs to files, the piece of context engineering that partly inspired mine. Anthropic has since redesigned the whole mechanism. I found out while fact-checking this post. Both are decent inside their own CLIs. Programmatically, through the app server and the SDK, both get worse, and between them nothing lines up: different tool names, different semantics, a real PTY versus a background shell, different ways to get output back out. My favorite discovery, from building Zodex to match the Codex tool spec exactly: the way to programmatically check on a long-running command in the Codex app server is to send a blank character through write_stdin and read whatever has piled up. It’s the reliable way, since on non-TTY sessions write_stdin rejects non-empty input outright. That’s not an API. That’s poking something with a stick.

Any skill, playbook, or workflow I wrote on top of that split would have needed two dialects forever, and those dialects don’t hold still, as the Claude redesign shows. Staying on top of every provider’s rethink of its own terminal story is a game with no end, and I realized I didn’t want to play it. So the requirement stopped being “pick the better provider surface” and became “own the surface.” One Rust-native process layer, one set of tools, one set of semantics, and both providers’ agents use exactly the same thing.

Turns are the resource

The deeper problem wasn’t ergonomics. It was what long-running commands do to agent turns.

An agent watching a slow command has two bad options in most harnesses: sit inside the turn burning tokens until it completes, or poll in a loop, which burns tokens and attention, one “still running…” at a time. Claude Code’s background shells are the second option with better manners; the recommended workflow is literally the model calling BashOutput periodically to see what’s new. The lesson had been sitting with me since adm, where the whole design was messages arriving passively at natural boundaries instead of agents asking “anything yet?”. The thing I needed to control was the turn itself.

So gg_process_run is built around turn injection. The agent starts the process and ends its turn. That’s the whole instruction: start it, then stop. When the command exits or crashes, the runtime starts a fresh turn for the owning agent with the result injected. Because the agent did nothing in between, its prompt stays cached right up to the injection point, so the resumed turn is cheap. Multiply that by every cargo build, every test suite, every dev server an implementer touches during a feature, and the savings stop being a rounding error. The agent’s job description shrinks to the part it’s good at: deciding what the output means.

Swap the layer, keep the harness

There’s a boundary judgment here I want to be precise about, because this would be easy to read as “I replace provider tools,” and I don’t. The entire reason goldengoose builds on the Codex app server and the Claude Agent SDK is to keep models inside the harnesses they were reinforcement-trained in. Codex models get Codex’s tools, Claude models get Claude’s, and for the actual work trajectories I wouldn’t trade that for anything.

Long-running processes are the one place where swapping is safe, and my global agent instructions draw the line explicitly: use your native bash or exec tool for short, non-interactive commands; use gg_process_run for anything you expect to take more than thirty seconds, because you can end your turn and a new one starts automatically when the command finishes. So the models live in their trained tools almost all the time and step out only where trained behavior isn’t what’s being exercised; no model’s RL made it better at waiting. If anything the swap improves performance, since the thousands of tokens polling would have burned stay unspent. And knowing the tool had exactly one job made it easier to build well: I never had to design for the interactive cases, because the native tools keep those.

Logs are files, the stream is a convenience

The persistence model took me a while to land on, and it’s deliberately unglamorous. Process output is written to bounded log files, split by stdout and stderr. The live view in the UI uses near-real-time background refreshes rather than a websocket feed, and I stopped feeling bad about that once I understood the hierarchy: the stream doesn’t matter as long as the process is robust and the bytes are on disk. Every agent that starts a process gets the log paths in its context, so if the UI crashed mid-build, the agent could go read the file itself. The pretty view is a convenience layered over a boring file that is always right. That’s the safety property I wanted, and no amount of streaming polish substitutes for it.

The terminal I refused to build

There’s one decision here I made explicitly and early, while talking the design through before building it: no embedded terminal. Most agent harnesses ship one, a terminal pane where commands scroll past, with all the problems that implies. Output hidden behind the wrong pane, scrollback that fights you, no sense of which agent owns what. And an embedded terminal is a trap for the builder too: the moment there’s a shell prompt in my app, I’m maintaining a full-blown terminal emulator, forever. I designed against it on purpose.

What I built instead is an observation surface. A sidebar lists every running process across every repo and worktree goldengoose is managing, with the owning agent, the working directory, stdout and stderr separated, copy buttons on everything, and a kill button for anything stale. Any command any agent runs has a view-logs button, and the left sidebar marks at a glance which agents have processes running, even when those agents aren’t the one I’m looking at.

The observability runs both directions, too. Agents get the same view I do through gg_process_status, which quietly retired a chore I’d done for years: I never paste logs into a chat anymore. I tell the agent to go check them, and whichever agent it is sees exactly what I see. Compare that with something like the Codex desktop app, which can run background tasks but keeps them opaque: the model controls the task, and there’s no live look inside, no grabbing the output to use somewhere else. It works as a command. It fails as a surface. A terminal is an input device, and I didn’t need input. I needed to see.

Three requirements, one layer

What strikes me in hindsight is that I couldn’t have traded away any of the three things this layer provides. Turn control, because agents that poll are agents wasting the most expensive resource in the system. Ownership, because one consistent tool surface is what lets every skill and playbook I write work across providers unchanged. Observability, because “click any process, read its logs” is the feature I use most in a day and the one no other harness offers. None of the three was the plan. The plan emerged the way everything I build seems to now: by talking through what I actually wanted until the shape of it stopped moving. The shape turned out to be a Rust process manager underneath everything. Every phase of every feature I’ve shipped since runs its checks through it.