I'm Masaki Hirokawa, an artist and indie developer running a small collection of wallpaper and wellbeing iOS apps. Once cumulative downloads crossed 50 million, the warnings piling up in my Xcode build logs started to bother me in a quiet but persistent way. None of them were serious enough to crash the apps, but watching 300+ warnings stream past during every build felt like the kind of misalignment a craftsman would notice before anyone else.
Both of my grandfathers were temple carpenters in Japan, and one of the things I absorbed from them is that something built well shouldn't drift over decades. Compiler warnings carry that same signal for me: small misalignments that hint at where the joints have shifted. So this spring I gave myself a four-week sprint with Claude Code as a partner, aimed at getting Xcode warnings to zero. This article is the field report.
Setting the Scope Before Touching Any Code
The framing I committed to up front was not "achieve zero warnings" but "switch to a workflow that can keep warnings at zero." No new features during the sprint, no major library upgrades, no chasing of stretch refactors. The only objective was reducing the addressable warnings that showed up in the build log.
A tight scope made the context I handed to Claude Code naturally narrow. I limited the work to three production apps, captured the xcodebuild output without -quiet, and saved it as a plain text log inside the Claude Code working tree. In the first session I asked Claude to read the log and group the warnings by intent, then sort each group by the cost of fixing it.
The result was a four-bucket map: Deprecation warnings, Unused symbol warnings, places where type inference had gone fuzzy, and @available bounds that had aged out. That single step gave me more clarity in thirty minutes than I'd had by squinting at the log alone for months.
Week-by-Week: Start With the Mechanical Wins
Week 1 was nothing but Unused warnings and stale import statements. I asked Claude Code to "propose deletions for Unused Variable warnings only, without changing observable behavior," and I accepted the diffs one feature branch at a time. The rule I learned to follow: never try to clear multiple warning categories in a single commit. One category, one region, one PR. If a test failed later, rollbacks stayed cheap.
Week 2 was the Deprecation backlog. I had more leftover UIApplication.shared.statusBarOrientation calls and pre-UIScene lifecycle paths than I'd realized — APIs deprecated as far back as iOS 13 that quietly survived. Claude Code drafted the replacement strategy per API, and I personally verified iPad orientation behavior by hand because that's something a static analyzer can't feel.
Week 3 targeted fuzzy type inference and missing @MainActor annotations. This is the area where Claude Code can over-correct if you let it. I switched modes: have it propose the smallest possible change that compiles cleanly, accept only that. Treat it as scaffolding for the eventual Swift 6 strict concurrency turn-on, not the turn-on itself.
Week 4 closed out remaining @available ranges and the warnings I couldn't fix because they came from third-party SDKs. For that last category I asked Claude Code to "document every unfixable warning with the reason it must remain." The output became _docs/known-warnings.md, which now acts as the basis for any build-setting suppressions I decide to add.
What I Delegated, What I Kept
By the end of the sprint, a clean line had emerged between work I happily handed off and work I refused to. Claude Code handled the local, diff-reviewable changes well: deleting unused variables, swapping API signatures, normalizing @available ranges. These are the chores I personally don't enjoy doing, and the speed and accuracy I got back were genuinely a win.
What I kept in human hands was anything that touches felt experience — widget background rendering, scroll inertia, haptic intensity, image-loading timing. For apps that long-time users have a muscle memory for, a refactor that's logically correct can still erode the "usual feel." This is something I've learned the hard way across twelve years of indie iOS development. Zero warnings is never worth a regression that loyal users perceive but can't articulate.
The other piece I kept was the decision to intentionally retain certain warnings: ones from third-party SDKs, ones from compatibility shims I want to keep, ones from A/B-test code paths running in parallel on purpose. Claude Code wants to remove these, and I instead annotate them with a // MARK: comment that records the reason. I avoid // swiftlint:disable precisely because future-me needs the rationale, not the silence.
Friction and How I Dialed It Down
The first source of friction was Claude Code's tendency to fix too much per turn. The cure was instruction granularity. Every prompt I sent included three constraints: target a single file, target a single warning category, and keep the commit under roughly 200 lines of diff. With those guardrails, the proposals stayed reviewable.
The second was making Claude Code self-verify the build. Running xcodebuild through Claude Code produced logs too long to digest in one round. I switched to xcodebuild ... | tee build.log and told Claude Code to "read only the last 500 lines of build.log" when checking the result. Latency dropped, and review pacing improved noticeably.
The third was my own review fatigue. Claude Code was fast enough that I became the bottleneck. Mid-sprint I set a daily ceiling on how many diffs I would merge, on the theory that approving fixes faster than I could read them was the same as creating new debt with a different name.
Numbers From the Sprint
Starting warning count across the three apps: 312. Final count after four weeks: 6. The remaining six are all third-party SDK warnings logged with reasons in known-warnings.md. Total commits: about 90, of which roughly 75 were Claude-drafted diffs that I reviewed and accepted.
The biggest unexpected payoff was psychological: the resistance I used to feel when opening the build log was gone. With 300 warnings on the badge, I had unconsciously closed the log before reading it. That habit was quietly degrading the quality of every other debugging decision I made — AdMob latency, Firebase init ordering, anything that lived in build output became easier to triage once the noise was gone.
Zero new warnings were introduced during the sprint. Writing "do not introduce new warnings" into Claude Code's standing constraints turned out to be surprisingly effective. For an indie developer with a long-running codebase, the lesson I keep coming back to is that "achieve zero" matters less than "switch to a workflow that holds zero."
What's Next
The next four-week block will gradually flip on Swift 6 strict concurrency and work through Sendable violations with Claude Code. Turning the flag on across the whole codebase at once would resurrect hundreds of warnings, so I'm planning to enable it target-by-target with @preconcurrency boundaries drawn deliberately.
In parallel I want to wire up an automation where release-time xcodebuild logs flow into Claude Code, get summarized, and update _docs/known-warnings.md without my input. If I can codify "holds zero" into the build pipeline itself, I free up time for the parts of indie development I actually want to spend hours on. The self-taught programming I started in 1997 was once the goal; today it works best when it gives me back time for everything else. I hope these notes are useful to others maintaining long-lived codebases.