A short, honest engineering retro. I let an AI coding agent (and myself) build a bespoke tool for a job that Netmiko, Ansible, and Oxidized already do. The bug wasn't in the code. It was skipping one question before the first line.
The task looked small: give an agent a way to drive network-switch CLIs, send a command, wait for the expected output, react. So it built one. A tidy little wrapper around tmux
: open a session, send-keys
the command, poll capture-pane
until a pattern matched or it timed out, and stream everything to an audit log.
It got a design review. It got an architecture note. It got an adversarial pass that found a real bug (the capture only read the visible pane, so long output silently timed out). It got a fix. It got tests.
Then someone asked the only question that mattered, the one nobody had asked yet:
"So this leaves almost nothing, because Netmiko already covers switches?"
Yes. It did. Netmiko
(multi-vendor SSH for network gear), Ansible
's network_cli
connection plugin, and Oxidized
(config backup) already do send-command-and-expect against switches, at fleet scale, with per-vendor quirk handling nobody wants to reimplement. The bespoke tmux
driver was a worse version of tools that have existed for years.
A session's worth of building, reviewing, and fixing. All of it on something that should never have been written.
At Pulsed Media we run a lot of automation over our own hardware, and we lean on LLM tooling in day-to-day ops, so this failure mode isn't hypothetical for us. It's a standing tax. AI agents are very good at building. That's exactly the problem.
The fix is embarrassingly cheap. Before you (or your agent) build anything with a name (a "driver", a "client", a "manager", a "helper"), run the reuse gate:
rg -l "expect|send.?command|capture-pane|pexpect" .
That's it. Two commands, half a minute. If either returns a real answer, you are no longer deciding how to build. You are deciding whether to adopt. Those are different projects, and the second one is usually a config file instead of a codebase.
The reason this gate gets skipped is not laziness. It's that building feels like progress and searching feels like a detour. An AI agent amplifies the feeling: it will happily produce a clean, tested, well-structured implementation of the wrong thing, and every artifact it generates (the tests, the review, the docs) makes the wrong thing look more legitimate, not less.
The reuse gate is the counterweight. It runs before the first line, because after the first line, sunk cost takes the wheel.
Here's the part that stung. The tool got reviewed, thoroughly. Adversarial review, architecture doc, the works. And all of it was aimed at how well it was built.
Not one of those reviews asked whether it should be built.
This is a general trap, and it's worth naming: your review process can be rigorous and still be pointed at the wrong question. A review that only asks "is this well-designed?" will bless a beautifully-designed thing that shouldn't exist. The "should this exist at all?" check has to come first, at design entry. Otherwise every downstream review inherits the false premise and polishes it.
If you use an "authority/reuse gate" in your review templates, put it at the top, before the design critique, phrased as a hard question: does an existing tool, ours or third-party, already produce this output? If yes, the default answer is don't build: adopt, and justify any reimplementation explicitly. At Pulsed Media we now treat that as the first gate, not a footnote, precisely because we learned it the expensive way.
Once a build has momentum, your brain will manufacture reasons to keep going. Watch for these three, the ones that got me:
1. The misread policy. Our shop has a "no Python for new code" rule. I told myself that ruled out Netmiko (which is Python) and therefore a custom tool was necessary. Wrong: the rule blocks writing Python in our repo. It does not block consuming a mature Python tool as external infrastructure, and we already run plenty of third-party services in other languages. I had turned a real policy into a fake constraint that happened to justify the thing I was already building. When a policy conveniently makes your current path the only path, re-read the policy.
2. "The boss said we need it." An operator did say "we need this." But "we need the capability" authorizes solving the need, and adopting an existing tool solves it. "We need it" is not "we need you to build it." A stakeholder's yes to a goal is not a waiver of the reuse gate.
3. Sunk cost, wearing a lab coat. Once the tests passed and the review came back clean, stopping felt like wasting good work. But the tests passing on a tool that shouldn't exist is not a reason to keep it. It's just a well-tested mistake. The cost is already spent; keeping the tool spends more (maintenance, the next person's confusion) to avoid admitting the first spend.
The reuse gate is easy to wave at and hard to actually run, so here is what it looks like applied to three real automation needs, the kind that come up constantly when you run your own infrastructure.
Driving switch and router CLIs. This was the trap I fell into. The need is real: old network gear has no clean API, so you script the CLI, send a command, wait for the expected output, answer a pager. Thirty seconds of searching surfaces Netmiko (multi-vendor SSH with per-vendor prompt and paging quirks already solved) and Ansible's network_cli
connection plugin. Both are mature, both handle the vendor edge cases you have not thought of yet, and both are consumable as external infrastructure. The gate answer is adopt, not build.
Backing up device configs. The need is a versioned history of every config change. It is tempting to write a loop that SSHes in, runs show running-config
, and commits the output to git. Oxidized does exactly that, across dozens of vendors, with a web UI and a git backend, and it has done it reliably for years. Again: adopt.
Recovering a stuck text console. Here the gate answer is genuinely thinner. Driving an interactive rescue shell (an initramfs prompt, a busybox recovery, a manual fsck) is rare enough that no single mature tool owns it, but pexpect
and the classic expect(1)
cover the send-and-expect core. The honest verdict is "mostly adopt, occasionally glue," and even that admission is the gate working: it tells you the custom surface is small, so keep it small.
Notice the pattern. Two of three needs are fully covered by tools that already exist, and the third shrinks to a thin wrapper once you subtract what pexpect
already does. At Pulsed Media we now run the gate on every "we should build a tool for X" before a line is written, because the expensive lesson taught us the honest answer is usually "adopt, and stop." The reuse gate does not slow you down. It tells you, in thirty seconds, how much of the thing is actually yours to build, which is almost always less than it feels like at the start.
Before building anything with a name:
rg
the repo + one web search for the mature tool. 30 seconds.None of this makes you build slower. It stops you building the wrong thing at full speed, which is the failure AI agents make faster and more convincingly than any human ever could. The best code is the code you didn't write because someone already wrote it better.
If you're building agent systems that generate and ship code in production, or you just want to see what disciplined, own-hardware infrastructure looks like, I run infrastructure and support at Pulsed Media: seedboxes and storage on our own machines in our own datacenter in Finland, on an open-source platform (PMSS, GPL v3), EU jurisdiction, 14-day money-back. We publish our mistakes because the industry needs honest engineering write-ups more than it needs another launch post.