Building Your Own Tools

Configuration is the tax you pay for not knowing who your user is. It has always been priced into the cost of building software, so we stopped noticing it was there. Build something for exactly one person and the tax disappears — along with the onboarding, the migration path, and the entire “but what if they want it the other way” branch of every decision.

Agents have changed the economics of building tools. That much is now uncontroversial and it is also the boring half of the observation. The interesting half is what the new economics let you build, which is not more software but a different kind of software: software with exactly one user, whose habits, directory layout and tool preferences the author knows precisely, because the author is the user.

That category barely existed before. It wasn’t forbidden, it just never cleared the bar. This post is about what happens to a program’s design when it does.

Building a tool used to require an amortisation argument, even when nobody wrote it down. The cost was some number of days. Those days had to be paid back — by you using it a lot, by a team using it, or by enough strangers using it that it justified being a product. Below some number of users, the honest answer was “write a shell script or live with the annoyance,” and most of us lived with the annoyance.

Agents moved that threshold down hard enough that the payback calculation now clears at N=1. Not because agents write perfect code, but because the thing that used to dominate the cost — the mechanical labour of expressing a design in an unfamiliar API — collapsed.

I want to be specific rather than gestural about this, so the rest of the post is one tool.

ggh is a native macOS app that manages my workstation. It scans my development folder for git repositories, shows their status in a sidebar, and opens each one as a tab with a coding agent on the left and a terminal on the right. It knows which of my repos are dirty, which have failing CI, which have open Dependabot alerts.

The numbers, because they matter to the argument: 5,407 lines of Swift across 51 files, plus 1,206 lines of tests. 22 commits, first on 20 June 2026, most recent on 6 September. Seventeen of those 22 commits landed in the first two days. I had written almost no Swift before starting it.

None of that is the interesting part. This is: ggh cannot be shipped to you, and that is a design achievement rather than an omission.

Here is the part of the argument that the configuration story alone doesn’t reach: the custom thing about a custom tool usually isn’t a feature nobody has built. It’s a combination nobody has any reason to build.

Every capability in ggh already exists somewhere, and mostly exists in a better implementation than mine:

What I was doing before The tool that does it well
Seeing which repos are dirty, ahead, behind GitHub Desktop, Fork, Tower — or git status in a loop
Running a coding agent Claude Code in a terminal
Running the build, the tests, everything else Ghostty
Issues, PRs, Actions, Dependabot alerts github.com, or gh
The notification inbox github.com/notifications
Opening a project in the right IDE Finder, and remembering which one
Reading a linked PR Safari

Not one of those is a gap in the market. Each is a solved problem with a good tool attached. What I actually wanted was all seven at once, per project, without losing my place — and that has no product, because the market for that exact bundle is one person deep.

This is the shape most personal tooling takes. You rarely need software that does something new. You need software that stops you paying the switching cost between seven things that each already work, in the specific sequence you happen to work in. That tax is invisible in any individual moment and enormous in aggregate, and it is precisely the kind of cost that never justifies a product, because the sequence is yours.

ggh: one tab per repository, with the seven tools it replaces marked

One tab, one repository. The window is doing the work of seven separate tools, and the only reason it can lay them out this way is that it only has to suit one person.

The combination also has a property that matters: the parts get to know about each other. The tab knows which repository it is, so the agent starts in that directory, the terminal starts in that directory, the CI chips are that repo’s runs, and a notification about that repo opens in that tab’s browser. Seven separate tools cannot do that, not because they’re badly built, but because none of them can see the others.

And this is where the combination argument and the configuration argument meet. A product that tried to ship this bundle would have to make every part of it optional — your git client, your agent, your terminal, your editor, your issue tracker — because it cannot know which seven tools you use. That optionality is not a nice-to-have; it is most of the program. Deciding the answer to all seven, in code, is what makes the thing small enough for one person to build in a weekend.

Think about what a setting actually is. Every configuration option in every program you have ever used is a recorded moment where the author could not resolve a decision, because resolving it required knowing something about the user that they did not know. Rather than guess, they deferred — into a preference pane, a dotfile, a YAML schema, a --flag.

That deferral is not free. Each setting costs you a UI affordance, a persistence format, a default, documentation, a migration story when the shape changes, and a permanent second code path that has to keep working. A settings screen is a monument to unresolved uncertainty about your users, and it is usually one of the most expensive things in a small program.

When you are the user, the uncertainty is gone. You don’t defer the decision. You make it, in code.

Here’s the concrete case. Opening a project in the right editor is a genuinely fiddly problem — it’s Xcode for Swift, RustRover for Rust, GoLand for Go, and my answer depends on what’s in the directory. The shipped-product version of this feature is a mapping table in preferences, a precedence UI, and a documentation page. The ggh version is a 133-line file with no user-facing surface at all:

private static func ecosystem(_ i: DirIndex) -> Ecosystem {
    // Android before generic JVM (it's a gradle project with an app module).
    if i.hasAny(["build.gradle", "build.gradle.kts", ]),
       i.has("gradlew"), i.has("app") { return .android }
    if i.hasExt("xcodeproj") || i.has("Package.swift") { return .swift }
    if i.has("Cargo.toml") { return .rust }
    if i.has("go.mod") { return .go }
    
}

with a preference order per ecosystem, resolved against what’s actually installed:

case .rust:   return [.rustrover, .intellij, .vscode, .cursor]
case .go:     return [.goland, .intellij, .vscode, .cursor]
case .swift:  return [.xcode, .vscode, .cursor]

The screenshot above is that code resolving a real case, and it happens to be the fallback path rather than the headline one: forge has no build marker at its root at all, but it does have a .idea directory, so the rule drops through to “use the JetBrains IDE for whatever language this is” and lands on IntelliJ.

The forge repository’s tab header, and the Open button showing the inferred IDE

That list is not a general truth about how people should open Rust projects. It is a statement about me, hardcoded, with no way for anyone else to change it. Which is fine, because there is no anyone else. Cargo.toml means RustRover. The decision got made once, in the place decisions belong.

The same move repeats throughout. The scan skips 16 build directories — node_modules, target, .build, DerivedData, __pycache__ — as a hardcoded set, because I know what’s in my tree. The agent picker offers exactly Claude Code, Copilot and Codex, as a three-case enum; adding a fourth is a four-line diff rather than a plugin API. The default development folder is ~/dev/projects because that is where my projects are, so the app has no first-run wizard and no empty state worth designing.

I want to be accurate here, because “zero configuration” is the kind of claim that sounds better than it is true.

ggh has a settings screen. It is 69 lines. It backs eight persisted values: development folder, terminal app, fallback editor, scan depth, tab layout, embedded-browser toggle, notifications-panel visibility, and a per-project editor override.

The point isn’t that the number is small. It’s why each one is there. Every one of those eight exists because I personally changed my mind about it at least once — I wanted tiled tabs some days and stacked tabs others, so that became state. The per-project override exists because the inference is occasionally wrong for me, and when it is, I want to correct it without a rebuild.

So the rule that falls out is narrower than “don’t build configuration,” and it’s the one useful thing I’d hand to someone else:

At N=1, a setting is only allowed to exist after you have personally wanted to change it. Not before.

At N=many you add the setting first, because somebody might. That inversion is the whole difference, and it accounts for most of the size difference between a personal tool and the shippable version of the same idea.

The other deletions follow the same logic. There is no backwards compatibility, because there is no old version in the wild — when the persisted shape changed, I changed it and moved on. There is no error handling for “the gh CLI isn’t installed,” because it is installed. There is no telemetry, no update channel, no support burden, no issue triage.

Being honest about the division of labour matters more than the headline number.

The agent wrote nearly all of the Swift. It knew SwiftUI’s layout model, FSEvents watching with debouncing, embedding SwiftTerm, the WebKit configuration needed for GitHub’s passkey flow to work in an in-app browser. Those are exactly the things that would have cost me a week each and that I would never have paid for a tool only I would use. The unfamiliar-API tax is the cost that collapsed, and it is the cost that was keeping this class of software from existing.

What I did was decide what the thing was, decide what to leave out, and read the diffs. Notably, the leaving out is not something the agent did on its own — left unattended it will happily build the settings screen, the plugin system and the migration path, because that is what the training data looks like. The N=1 discipline is a decision you have to keep making.

The tempting version of this post ends at the previous section. Here’s the part that keeps it honest.

You still own every line. 5,407 lines of Swift I didn’t type is still 5,407 lines I am responsible for. When the startup sequence stalled in August, nobody else was going to find it. The maintenance cost of software did not go down just because the authoring cost did, and for personal tools it’s the maintenance that eventually decides whether the thing survives.

The build curve is spikier than the story suggests. Seventeen of 22 commits landed in two days. Then three more a week later, then nothing for ten weeks, then two. That gap is the real shape of personal tooling, and it’s not a failure — it’s what “good enough, get back to work” looks like. But it’s a bad match for anything that needs steady attention, and you should assume it about anything you build this way. A personal tool that requires weekly upkeep will not get weekly upkeep.

Unfamiliar platforms have a floor. The agent was much better at Swift than I expected right up until the point where the problem stopped being code. Developer ID signing, notarisation, and the restricted browser entitlement needed for platform passkeys are where it got slow, because those are questions about Apple’s provisioning system rather than questions about a file in front of you. You should expect the cheapness to be real in the middle of a project and to evaporate at both ends — at the very start, when nobody has decided what it is, and at packaging.

You cannot review what you cannot read. I can follow the Swift and I could not have written it. Those are different competencies, and the gap is where the risk lives. It is fine here because the blast radius is my own machine. It would not be fine in code that handles other people’s data. Match the ambition of what you build to how well you can actually audit it.

Some of these should have been shell scripts. The bulk git operations in ggh — fetch, pull, push across every repo — are a nicer version of a loop I could have written in ten lines of zsh. The GUI earns its place because of the safety rail — it shows me the exact command, and which of my 77 repositories it considers eligible, before anything runs; on the destructive tabs it also says why the rest were excluded (“Uncommitted changes”, “Behind upstream — pull first”). But I’d be lying if I said the calculation was obvious. Cheap-to-build makes it easier to skip the question of whether it should exist.

The bulk git operations sheet, showing which repositories qualify

And the security surface is only acceptable at N=1. ggh shells out to git and gh, and embeds a browser with a persistent login store holding a live GitHub session. I accept that because it runs on my machine, under my account, against my repos. It is not a defensible posture for software you hand to someone else.

That last one generalises into the real boundary. The moment you share a personal tool, every economy in this post reverses. The gap between “works for me” and “works for you” is exactly the configuration layer, the onboarding, the error handling and the compatibility story that you deleted — and deleting it was the source of the speed. The repo is private, and not out of secrecy — it is private because the honest README for it would begin “this will not work on your machine, and I am not going to fix that.” Publishing is free; supporting is the old cost structure arriving all at once. Know which one you are signing up for before you post the link.

The takeaway isn’t “build more tools.” It’s that a question you have been auto-answering with no for your entire career deserves re-asking.

Every developer has a list of small, specific, deeply personal annoyances that were never worth a tool — the thing you do fifteen times a day with three commands and a browser tab, that no product will ever build because the market for it is you. That list has been sitting there accumulating because the amortisation never worked out.

It works out now. Build the version that only works for you, hardcode everything you actually know, and don’t build the settings screen until you’ve wanted to change something twice.