Four apps, one AI, and every time it lied to my face
I built four apps with Claude Code. It writes code that compiles, passes its own tests, matches the spec - and is still quietly, confidently wrong. Here are the best examples.
- #ai
- #building-apps
- #claude-code
I’ve spent the last stretch of evenings building four apps more or less side by side with Claude Code: two macOS virtual cameras (FrostCam and WarmCam), a baby-name app for couples (Névtárs), and a menu-bar meter for your AI usage (MeterTab, still cooking). Different languages, different platforms, different problems.
Everyone wants the same summary from me, usually at the pub: “So is it magic or is it garbage?”
Neither. It’s a brilliant, tireless junior engineer who will confidently hand you code that compiles on the first try, passes the tests it wrote for itself, matches the spec you agreed on twenty minutes ago - and is still wrong. Not wrong in a “throws an error” way. Wrong in a “everything looks green and your app is quietly broken in production” way. The worst kind.
The tests are green. The build is clean. And the thing is lying to you.
And before anyone reaches for the pitchfork: I adore this thing. All four apps exist because of it. I would not have shipped four products solo in a year of evenings without it, full stop, not close. Half the bugs below are ones I’d cheerfully have written myself and shipped with no paper trail at all - the difference is that with Claude there’s a trail, and a second Claude to catch them.
So this isn’t a takedown. It’s more that “works on my machine” has a strange new cousin now - “works in the model’s head” - and I’ve learned to love the first one and fear the second. Here are my four favourite lies, one per app.
FrostCam: “Installed!” (the camera was gone)
FrostCam is a virtual camera that freezes your webcam so you can scratch your nose during a meeting without broadcasting it. To be a camera that Zoom can see, it ships a system extension - a little process macOS runs on your behalf, deep in the plumbing.
Here’s the fun part about system extensions: when you ship an update, the old version doesn’t go away
immediately. macOS keeps it resident until you reboot, and hands you back a status that politely means
“yeah, it’ll be done later.” That status is .willCompleteAfterReboot.
Our install code did something very reasonable-looking. It treated “will complete after reboot” the same as “success.” Why wouldn’t you? It’s not a failure. Nobody threw an error. The function returned, the spinner stopped, the UI lit up green and said Installed!
Meanwhile the actual camera had vanished from Zoom, Meet and Teams. Users were staring at a confirmation screen telling them everything worked, next to a Zoom window where the camera flatly did not exist.
I spent three consecutive releases chasing that one. Every layer agreed with every other layer:
macOS said the install went fine, the API returned success, our UI said success. All three were
technically correct and collectively a lie. The fix needed two independent checks - honour the reboot
result and actively go poll the list of cameras to see if ours was really there - because you cannot
trust a system extension to tell you it’s alive. There’s now a comment in ExtensionManager.swift
that is basically a confession: reporting plain success here is what made the camera silently
disappear.
The lesson I keep relearning: an AI optimises for the happy path because the happy path is what the docs describe. Reality is all sad paths.
WarmCam: the thread-safety comments that were fiction
WarmCam is FrostCam’s better-looking sibling - it runs your webcam through a GPU pipeline in real time to smooth skin, warm the lighting, blur the background. Metal shaders, per-frame processing, the works.
At some point Claude wrote the code that sets up the GPU pipeline, and it declared those pipeline
objects as lazy var - meaning “create this the first time someone asks for it.” Perfectly idiomatic
Swift. It even wrote a tidy little comment nearby about the threading model, reassuring me that this
was all handled correctly.
The comment was fiction. lazy var is not thread-safe. When two frames come flying in on the capture
queue at the same time - which, in a real-time video app, is the entire job - two threads can race to
initialise the same thing and step on each other. The code looked safe. It was annotated as safe. It
was not safe.
I only caught it because of a habit I’ve now made permanent: after Claude finishes a feature, I send in
a fresh batch of Claude agents whose only job is to tear the code apart. Six of them, each with a
different remit - security, concurrency, architecture, the boring stuff. One of them flagged the
lazy var. Another one, in the same sweep, found something much scarier: the skin-smoothing shader had
a brightness threshold that quietly switched itself off on darker skin tones. A one-line number that
would have shipped a beauty filter that didn’t work on a big chunk of humanity.
Neither of those was going to show up in a test. They showed up because I stopped trusting the author - even when the author was the same model reviewing itself an hour later. Turns out Claude is a much harsher critic of Claude’s code than Claude is of its own. Use that.
MeterTab: the server that passed its own exam and still couldn’t say hello
MeterTab watches how much of your AI budget you’ve burned, and - the whole point - exposes that over MCP so your agents can check their own fuel gauge and back off instead of face-planting into a rate limit. So it ships a little MCP server.
We built the server. We wrote contract tests for the server. The tests passed. I plugged it into Claude Code, the actual client it exists to talk to, and… nothing. No error. No log. No handshake. Just a server and a client sitting in awkward silence like two people who’ve forgotten each other’s names.
The cause was one integer.
The MCP handshake lets a server advertise experimental capabilities. Ours announced itself with a
version marker as a bare number - "x-tokenbar-mcp": 1. Sensible. Readable. Wrong. The spec says the
value there has to be an object, not a number, and the real client validates that strictly. A bare
integer fails validation, and the whole handshake dies - silently, because why would you shout about a
capability check. Wrapping it (["x-tokenbar-mcp": ["schemaVersion": 1]]) fixed it instantly.
Here’s what got me. Claude had reasoned about MCP version negotiation correctly and at length. It understood the protocol better than I did. It just tripped on the gap between what a spec says and what a real implementation actually enforces - and that gap is a place no amount of clever reasoning saves you. Only plugging the two real things together and watching them fail does.
The contract tests were green, by the way. They tested the server against our own idea of the server.
Névtárs: the spec that was confidently wrong about everything that mattered
Névtárs helps couples pick a baby name. Both partners swipe, and the app only reveals the names you both liked - so nobody has to admit out loud that they hate the name their mother-in-law suggested.
Before writing any code, I had Claude produce a proper technical spec. And I want to be fair here, because it was genuinely excellent - the kind of document that would’ve cost me a weekend, delivered over one coffee, and it got a hundred decisions right that quietly saved me weeks. It also called itself the “Deep Research Edition,” with a straight face, which I’ve decided to find endearing.
Then it made two foundational calls: use Isar for the local database, and use a consumable in-app purchase so people can pay again for each new baby.
Both were wrong.
The database that actually shipped is Drift (SQLite). And the IAP is non-consumable - because it turns out Apple permanently limits non-consumables to one purchase per Apple ID, forever, which quietly demolishes the “pay per baby” business model the spec was built around. Nobody caught that until the live payment config met Apple’s actual rules and they disagreed.
The wrongness isn’t even the interesting bit. The interesting bit is the fossils. The spec was
authoritative right up until the code quietly overruled it - and then never updated itself. To this day
there’s a constant in the live app called isarDbName, pointing at a database that isn’t Isar. There
are comments referring to “fetching from Isar.” A whole separate planning doc, proudly stamped
“Submission Ready,” still lists “Local Database (Isar)” and its three collections. Multiple AI-written
documents, all disagreeing with the code and with each other, each perfectly confident.
That’s the trap with a tireless writer. It generates documentation faster than reality can keep up, and
none of it knows when it’s been superseded. Your git log used to be the archaeology; now it’s the
comments, and half the strata are lying.
So what do I actually do about it?
I still use it every day. To be clear about that. These four apps exist because of Claude Code, not in spite of it - I would not have shipped four things solo otherwise, not a chance. But I’ve stopped reading “the tests pass” as “it works,” and I’ve picked up a few reflexes:
- Assume the happy path is the only path it tested. The bug lives in sleep/wake, in a refund, in a reboot, in a second thread. Go there on purpose.
- Make it review itself, adversarially, with fresh context. Claude is a merciless critic of code it didn’t just write. A new agent with “find the security holes” as its only instruction finds real ones.
- Treat green tests as “consistent with my assumptions,” not “correct.” The MCP server passed every test I had and still couldn’t say hello.
- Keep a running list of everything it must never do again. My
CLAUDE.mdfiles have quietly turned into scar tissue - “neverfatalErrorin the extension,” “never hardcode the price,” “never watermark a stored frame.” Every line is a bug that bit me once.
The honest one-liner for the pub: it’s not magic and it’s not garbage. It’s the most productive colleague I’ve ever had - one who will also, occasionally, look me dead in the eye and swear the build is green while the camera’s been gone for twenty minutes. Both of those are true at once, and once you hold both, you go genuinely, absurdly fast.
So no, I haven’t stopped trusting it. I’ve just learned where to trust it - with the writing, the grunt work, the stuff I’d never have the patience for at 11pm - and where to double-check it, which is mostly the exact places I should’ve been double-checking myself all along. Turns out the best thing about working with a confident junior engineer who never sleeps is that it finally forced me to build the habits I’d been skipping for years.
Four apps in. Still my favourite colleague. I just read its PRs a lot more carefully now.