Pair Programming with Claude Code β What I Learned
βββββββ
Thanks to Diego Gomes Olvera for proof reading.
βββββββ
November 2025. New team, decade-old Android codebase. Multiple flavours, modularisation underway, Compose atop legacy XML. Each decision is deliberate β your first job is decoding those reasons.
I started experimenting with Claude Code after a month or more. This is the honest account of what happened.
It feels like pair programming
Not like using a tool. Like having someone next to you.
I describe what I need. Claude produces code. I review, redirect, and it adjusts. Very little code was written by hand β the thinking, decisions, and course corrections stayed with me. The mechanical production moved to Claude.
The IDE never went away. Not for writing, but for understanding generated code in context β types, navigation, warnings. Over time, checking IDE hints became a final polishing step, with one rule: only fix whatβs related to the current change. Donβt start pulling on threads. The IDE became the place where I verify work, not where I do it.
Lint rules β first real test
Custom Android Lint rules in January. PSI (Program Structure Interface) and UAST (Universal Abstract Syntax Tree) APIs are genuinely hard, and Claude navigated them well β scaffolding, stubs, test structure all came together quickly.
Then the hardest problem took a week. Kotlin suspend functions are compiler-transformed: suspend fun foo(): Response<T> doesnβt exist in bytecode β it becomes a regular function with Continuation<? super Response<T>> as its last parameter. The return type hides inside Continuation. Claude was parsing the source. That worked for simple cases but was fragile in ways that took time to understand.
I found the real fix. Claude hadnβt surfaced it.
Initially, I trusted Claudeβs judgment with PSI/UAST more than I should have. Better upfront research might have caught the problem sooner. Lesson noted.
Two recurring patterns: Claude tends to duplicate logic until guided toward reuse. Each new rule included tests β consistent coverage made the pace safe, not reckless.
CLAUDE.md β the persistent layer
A small but important commit: CLAUDE.md with project context, guardrails, and tool preferences. Things learned once, written down, not re-explained every session.
It has to stay short. An overly long context file floods the modelβs attention, and things start slipping. Every addition needs to earn its place.
The most useful prompt I developed: when Claude proposes a crash fix or change, interrupt it. Ask it to reflect on why the code ended up the way it did. That shift β from βfix thisβ to βunderstand thisβ β moves it from plumbing mode into actual reasoning. Itβs one of the most useful things I picked up in four months.
Large-scale refactorings were rougher β missed references, compilation errors, lots of back-and-forth. Still faster than by hand. And while Claude ground through the mechanical parts, I could actually do something else. Thatβs nothing.
Speed in unfamiliar territory
The highest-value sessions were in areas where I had partial knowledge β enough to know something needed doing, not enough to move fast alone.
Baseline profiles shipped in days, not weeks. Honest caveat: local benchmarks showed 10% startup improvement; Google Play was more modest. Moving fast meant I didnβt fully understand the gap. Thatβs worth going back to.
The lesson that actually stuck came from the Gradle profiler. I added an automatic regression report alongside it because setup was nearly free β why not? Turns out, someone needs to watch regression reports, understand them, and act on them. The cost of creation dropped. The cost of ownership didnβt. The reports became noise.
Speed is valuable, but it can substitute for understanding β gaps only appear later, and often at the worst moment.
Migrations and staying focused
Java-to-Kotlin looks easy until it reveals whatβs been baked in for years: nullability assumptions, singleton patterns, lifecycle behaviour that breaks in subtle ways in Kotlin.
One thing that always required explicit instruction: rename with git mv first, commit that alone, then convert. Claude never did this unprompted. It became a standing part of every migration instruction and eventually its own skill.
Scope awareness stood out. Claude flagged when changes risked becoming design decisions, making it easier to keep PRs focused. This discipline is easy to lose when fixing cascading issues, but easier to maintain when flagged in real time.
Dependency cleanup is the same story β unglamorous, mechanical, tedious at scale. Claude tends toward adding Gradle exclusion rules when the dependency-analysis plugin complains; the cleaner fix is usually a code change. The skill carries the procedure. The judgment still has to come from me.
Parallel workstreams were uncomfortable. This collaboration works best when I can watch whatβs happening and step in when something drifts. With parallel changes, that oversight becomes much harder β and git worktrees made it worse, not better.
Serialisation and R8 β reasoning about the invisible
Release obfuscation breaks things that work perfectly in debug, silently, with stack traces pointing to names that no longer exist. The kind of problem thatβs hard to reason about without support.
The lint rules built in January turned out to be foundational here β by the time the deeper obfuscation questions came up, those rules had already mapped the serialisation boundaries and flagged the risks. Thatβs what good groundwork looks like.
The risky change was mostly done by hand. Claudeβs value lay in the investigation: finding missing cases, reasoning about what else followed the same pattern, and evaluating what might break. Without that, the work would have been considerably harder and a lot more stressful. The app is now roughly two-thirds its previous size.
One responsibility that stayed firmly with me: reading output not just for correctness but for production meaning. Code that compiles and passes tests can still be semantically incorrect in ways that only surface in production monitoring. That gap doesnβt close automatically. Probably never will.
Where it stands
Four months in β this is just how I work now.
What changed most isnβt speed, though thatβs real. Itβs the texture of the work. Unfamiliar territory is less intimidating. Complex investigations feel less like diving into the dark. Mechanical debt gets cleared because the cost dropped enough to make it worth starting rather than deferring.
Collaboration has a pattern: I direct, review, and step in if needed. Claude executes, investigates, and reasons broadly. The IDE remains central to understanding.
The failure modes are known β fast decisions that skip understanding; reaching for text search when the IDE gives better answers; dirty edits leaving warnings behind; exclusion rules where a code change would be cleaner. Knowing they exist is most of the mitigation.
Things still unsolved: parallel workstreams donβt fit how this works best. Large-scale refactors remain rough. MCP tools are useful but fragile at the edges.
Things worth trying next: better upfront preparation before entering unfamiliar territory. More systematic use of context7 for documentation mid-session. An automated Crashlytics pipeline β new crash appears, Claude reasons about it, proposes a fix β feels close enough to explore seriously.
AI assistance didnβt remove the need to understand the system. The IDE still matters. Domain knowledge still matters. Judgment still matters. What changed is the cost of the work that sits between understanding and shipping. That cost came down significantly.
For an engineer who joined in November and was shipping meaningful technical improvements by January β thatβs not a small thing.
β β β β β β β β β
Stay tuned if you want to hear about code reviewing with Claude, maturing with skills, plugins, and MCP, and saving some tokens.
