Inspiration

I vibe-code iOS apps, and my loop was always the same: ask the AI for something, check it in the simulator, spot something I don't like, take a screenshot, paste it into chat, and then explain which part I meant. "In screenshot 1, that button. No, the other one." Every single time.

It had a few problems that kept stacking up. A screenshot alone doesn't tell the AI which UI I mean, because the same design might show up on different pages. Capturing and pasting takes forever when you do it twenty times a day. There's no good way to say "change these three things together" across screens. And if you solve this by building a debug tool into the app, now you've modified your own code and you'll have to rip it out later.

What bothered me most is that all the information was already on the screen. I could see exactly what was wrong. It just had no way to reach the code the AI was editing. So I tried to shrink that gap down to a single gesture, with one rule for myself: don't touch a single line of the app's own code.

What it does

Shake the phone and a floating button appears. Tap the thing you want changed, crop the screenshot down to just that part, write a short note like "make this button red." That's a pin.

Each pin quietly records what you tapped, which screen you were on, a hint about where that thing lives in the code, and the cropped screenshot. Pins get numbered per screen (1-1, 2-1, and so on), and you can group several pins, even from different screens, under one shared instruction like "use the same accent color for both of these."

Then you run the pinpatch-apply skill in Codex with GPT-5.6. It reads only the pins that haven't been handled yet, actually looks at each screenshot, finds the code, changes just what's needed, runs the relevant tests, and writes back a one-line result for each pin. Rebuild and you're done.

It works two ways. As the developer I run the skill straight against live simulator storage. But testers don't need Xcode or any tools at all. They shake, pin whatever bothers them, and send one zip. I drop that zip into Codex and every request gets applied in one go.

How we built it

PinPatch is a Swift package you add to your app target, and that's the entire setup. The host app never imports it or calls into it. No AppDelegate or SceneDelegate changes. Add the package and it turns itself on; remove it and there's nothing left.

Shake detection goes through exactly one path: swizzling UIWindow.motionEnded, always calling the original implementation first. No Core Motion, no sensor polling, no sendEvent hooks. Each window scene gets its own overlay, and in view mode every touch, scroll, gesture, keystroke, and VoiceOver step outside PinPatch's own controls goes straight through to the host app.

I spent a lot of time on storage because I wanted it to survive a crash in the middle of a write. Everything is stored as UUID records, fully written into a staging folder first and then renamed into place on the same volume. Editing a note writes a new immutable revision and atomically swaps the current pointer. On launch it cleans up after itself: abandoned staging and trash get removed, stale revisions get pruned, and the index is rebuilt from the actual folders.

Screens are identified by a fingerprint of the normalized navigation title, so "Room 301" and "Room 302" stay separate instead of merging into one. Export hands a folder to NSFileCoordinator and lets the OS build the zip itself. The Codex skill side is a handful of Python scripts that unpack untrusted zips in isolation, dedupe revisions, and double-check that a pin is still current right before recording a result.

One more thing: PinPatch itself was built with Codex and GPT-5.6, from planning through implementation, testing, and bug fixes.

Challenges we ran into

The hardest constraint was the one I set myself: instrument the app without editing its code. Everything had to come out of one swizzle plus a hit-test that hands touches back to the host everywhere except my own button and menu. A pin must never eat a tap that was meant for the app.

Getting the AI to process the right pin exactly once took a few attempts. The labels you see on screen, like 1-1, are display-only. Real identity had to be UUID-based the whole way through, with a check right before recording any result: is this revision still the current one?

Staying fully local and fully removable also took discipline. No networking, no CloudKit, no telemetry. Storage is excluded from backups and protected until first unlock. And there's no fatalError or force-unwrap anywhere in the production paths, so when something fails it fails inside PinPatch instead of taking the app down with it.

Accomplishments that we're proud of

The thing I'm happiest about is that the core rule held all the way through: never touch the existing app. You add a package and it attaches itself. You remove the package and everything is exactly as it was.

I'm also proud that someone with zero dev tools can file a precise, actionable report just by shaking their phone, and that one skill handles both my live simulator and a pile of zips from other people without me watching over it.

What we learned

Making AI coding work well turned out to have little to do with writing smarter prompts. The real win was deleting the translation step between what a person sees and what the model edits. Once that context existed as files with stable UUIDs, I could hand the agent a whole batch of changes and trust it to apply them.

You can just tell an AI "figure it out," and sometimes that works. But I still think clear instructions from the developer matter, and this tool exists to make a clear instruction cost one gesture instead of a paragraph.

What's next for PinPatch

A diff preview on the device before you rebuild, group instructions that can carry more nuance than a single line, and eventually the same shake-pin-patch loop on Android and the web.

Built With

Share this project:

Updates