Back to projects

Flight Deck

September 6, 2026

  • electron
  • spec-driven-development

Four places to look

The details I care about most as a developer live in four different apps. What I’m spending on Claude is in the session files on disk. What I’ve actually shipped is spread across repositories. What my dependencies released while I wasn’t paying attention is in a registry I have to remember to check. None of it is hard to find. It’s just that finding it is a small tax I paid several times a week, and paying it always meant a detour through three tabs I didn’t otherwise want open.

So the honest origin story here is convenience. I wanted one screen. That’s the whole objective — not a profound one, and I’d rather say so than dress it up.

What made it worth writing about is what I did with the excuse. I had been wanting to try spec-driven development properly, and I had never built an Electron app. This was a project small enough that if either experiment went badly, the only person inconvenienced was me.

What it is

Flight Deck is a local macOS desktop instrument that answers three questions on one screen: what am I spending on Claude and where is it going, what have I shipped across my repositories, and what did my dependencies release recently. It is a personal instrument, not a product. One surface I open instead of stitching the same answer together from three places.

Everything runs on the machine that produced the data. There is no backend, no telemetry, no analytics, no crash reporting. The only outbound requests go to GitHub and to registries named in a config file I maintain, and the Claude usage panel works with the network switched off entirely.

The more interesting list is what it refuses to do, because each item is a decision rather than unfinished work. It never stores a credential — GitHub access is borrowed from the already authenticated gh CLI at the moment of use, and cleared when the refresh that needed it ends. It never writes to ~/.claude/, which is the operating record of my real work and which Claude Code is often writing to while this reads. It never discovers anything; every repository it looks at is there because my config names it. And it does not tell me what I spent — it tells me what it estimates I spent, from a pricing table I maintain, labelled with that table’s effective date. A model with no price reports unknown, never $0.00.

Those four rules are easy to write down and easy to erode. Six weeks later, someone reaching for one convenient path lookup breaks the third one without noticing. A good chunk of the project is about making that impossible.

The spec-driven experiment

I built this with Spec Kit, which means the rules came before the code and stayed enforceable after it.

Everything starts from a constitution: seven principles, four of them non-negotiable. Local-first with zero telemetry. Read-only against source data. The main process owns all I/O. Credentials never cross the trust boundary. It’s versioned like code, and it has already survived one major revision — the runtime model moved from a localhost web server to an Electron application with a hard process boundary, which was a 1.0 to 2.0 bump carrying a written record of which principles were redefined and which existing plans no longer conformed.

That record turned out to be the most useful artifact in the project, and it isn’t code. It meant I knew exactly which work had been invalidated, instead of discovering it one file at a time over the following week.

From there each feature runs the same cycle. A spec describes user stories with priorities, functional requirements, and measurable success criteria, and names the GitHub issues it answers alongside the ones it deliberately leaves out of scope. A plan turns that into an approach, supported by research notes, a data model, and explicit IPC contracts. A task list breaks the plan into numbered work grouped by user story, each task citing the requirement it discharges.

Five features have been through that cycle, a little under four hundred tracked tasks. The habit that pays for itself is the smallest one: tasks cite criteria. When a test exists because a specific success criterion demands it, that test can’t quietly be deleted as redundant later, and the reason it was written is still attached to it a month on.

Here’s the thing the process caught that I would have missed. While researching the assigned-issues panel, I measured the account and found it had zero open assigned items at that moment — which means a correct ingest and an ingest that silently fetched nothing produce byte-identical output against live data. Every manual check would have passed. The spec recorded the finding, and fixtures became the primary evidence for that feature rather than a supplement to it. I wouldn’t have reached that conclusion while writing the ingest code, because while writing ingest code the empty screen looks like success.

I’ll be fair about the cost too: for a single-user tool this is more ceremony than the features strictly need, and there were afternoons where writing the spec took longer than the implementation would have. The return isn’t speed. It’s that I can come back after three weeks away and read why something is the way it is.

The Electron half

Four directories, and the split between them is a trust boundary rather than an organizational preference. The main process is privileged and is the only code that does I/O. The preload builds the single object the window is allowed to see. The renderer is React, presentation only, trusted with nothing. A shared directory holds the types and schemas both sides may see.

The boundary itself is a frozen map of twenty-one IPC channels plus three one-directional events, declared in one file. That file is not documentation of the boundary — it is the boundary. The main process iterates the map to register its handlers; the preload iterates the same map to build the renderer’s API. There is no generic invoke(channel, payload), and no channel accepts a path, URL, host, or command. The renderer asks for things by identity, never for addresses to fetch. Opening a link sends a kind and an id, and the main process resolves the actual URL from its own cache row.

None of that is enforced by review, because review is exactly the thing that fails at week six. A boundary test suite asserts that the registered handler set equals the contract set exactly, that no input schema names a location, that no exception or stack trace escapes to the renderer, and that no outbound request originates outside the two directories allowed to open a socket. Lint carries the rest: the ingest, cache, assigned, and GitHub directories are an Electron-free zone, so that convenient import { app } from 'electron' fails the build instead of eroding the constraint quietly. The practical proof is that the entire ingest path runs under plain Node with no window at all — if that ever stops working, something has crossed a line it shouldn’t have.

The same logic shows up in smaller places. The renderer carries no charting library and no styling framework; the charts are CSS, a row of rounded divs and an SVG grid of circles, because a dependency inside a window the constitution treats as untrusted has to earn its place. The typeface is vendored rather than linked, because the main process cancels any request that isn’t file:-shaped before it reaches the network stack — a hosted font wouldn’t have failed politely, it wouldn’t have loaded at all.

And the cache is disposable on purpose. It holds nothing that can’t be rebuilt from session files and GitHub, and a test asserts that deleting it and re-ingesting reproduces byte-identical aggregates.

One thing I’d have got wrong

The first version of the usage panel was barely a project — read the files, sum the tokens, multiply by a rate. It gave me a figure that looked entirely plausible and was roughly twice the real number.

About half the usage records in a Claude session corpus are duplicates. Deduplicating on message id and request id is the difference between a real figure and an inflated one, and nothing about the inflated version announces itself as wrong. It’s in the right order of magnitude. It moves in the right direction when you work more. You’d act on it for months.

That deduplication is now the primary key of the usage table, and its test is the one I’d keep if I had to delete every other test in the project. The rates themselves were derived by solving against the cost records Claude Code writes, which carry both a token breakdown and the cost computed from it, so the table reproduces those recorded figures rather than approximating them.

Where it ended up

It’s one window. I open it in the morning, I get the four things I used to go looking for, and I close it. That’s the entire user-facing story, and it would have been a worse project if I’d pretended otherwise.

The parts I’ll actually carry forward are the two experiments. Spec-first is now how I’d start anything I expect to leave and return to, and I know what it costs. And I have a working mental model of Electron’s process boundary that I got by treating it as a security boundary from the first commit rather than retrofitting one later.