Development reports

Dev Log: 2026-06-12

An agent reported success on a commit that hadn't actually landed. The pre-commit gate had rejected it — loudly, in theory — but by the time the output reached the agent through git commit 2>&1 | tail -1, the failure had been compressed down to a single blank line. A blank line reads as "fine." So the agent moved on, cheerfully wrong.

The obvious suspect was the gate. The gate was innocent.

The real culprit was fang, the CLI framework wrapping our error rendering. fang.Execute wraps stderr in a colorprofile.Writer before handing it to the error handler. That wrapper isn't a term.File, so DefaultErrorHandler's non-TTY guard — the one that's supposed to drop styling when stderr is piped — never fires. The padded ERROR box renders into the pipe anyway, blank trailing line and all. tail -1 dutifully returns the blank. The failure becomes invisible by being too well-decorated.

Two fixes were on the table. The narrower one: call os.Exit directly inside the gate. Fast, surgical, and it would have fixed exactly this one path while killing every test process that tried to exercise it. The broader one: register a custom fang.ErrorHandler that checks the real os.Stderr for TTY-ness, delegates to fang's pretty box when interactive, and prints a single plain line when piped. Testable. And it fixes every piped timbers error, not just the gate.

We took the broader fix. While we were there, we made the gate's error self-contained — the tool that rejected, the cause, the fix, the bypass flag — so that the one line surviving tail -1 is actually actionable instead of cryptic. The [timbers] advice block got routed to stderr where diagnostics belong, keeping stdout clean for anything that wants to parse it.

The lesson worth keeping: a CLI's error output has a contract with pipes, and pretty-printing frameworks can silently break it. Styled output is a feature for humans and a liability for everything else reading your stderr. If your framework wraps the stream before deciding whether to style, its TTY check is checking the wrong thing. The fix isn't to abandon the styling — interactive users still get the box — it's to make the TTY detection answer the question that actually matters: is the real terminal on the other end of this?

It also reframes a quieter problem. Agents are now a real consumer of CLI output, and they read it the way pipes deliver it. "Looks fine to a human in a terminal" is no longer the whole bar. If tail -1 of your failure is a blank line, you don't have an error message — you have a silent failure with extra steps.

The gate now fails loudly through every pipe we could think to point at it. That feels like solid ground.


Written with AI assistance from session logs.