RB

$ cat blog/reviewing-ai-generated-code.md

What I Still Do Myself When AI Writes the Code

Raj Bhensdadiya
reviewing-ai-generated-code.md
Cover illustration for the article: What I Still Do Myself When AI Writes the Code

I use Claude Code every day. It is part of how I work now. I use it to write code, review code, write tests, understand parts of a codebase, and sometimes build most of a feature. I do not see a problem with that. If AI can save me an hour of repetitive work, I will use it.

What I do not want is to become dependent on it.

I do not want to reach a point where I can only work on a codebase if Claude is open beside me. I still want to understand the system, and if something breaks I want to be able to trace the problem without immediately asking AI to solve it for me.

For me, Claude is an assistant. It does a lot of the execution. I still want to own the engineering.

The part that got cheap is typing the code

I can describe a feature, give Claude enough context, and get a working version quickly. That means I can spend less time typing code and more time deciding what the code should actually do.

Before I ask Claude to build something, I think about the flow first, and I want to make these decisions myself:

  • What does the user need to do?
  • Which API endpoints do we need?
  • What data needs to be stored?
  • What can fail?
  • What happens if the same request is sent twice?
  • What happens if one part succeeds and another one fails?

The duplicate request question matters most when money is involved. A retried request that records the same charge twice bills a customer twice for one thing.

Once I understand the flow, I can give Claude clear instructions and let it implement a lot of the details. There is a big difference between saying, "Build this feature," and saying, "This is how the feature should work. These are the endpoints. This is the data flow. These are the failure cases. Implement it this way." In the second case, I am still designing the system. AI is helping me execute it faster.

I do not auto-accept code

When Claude changes code, I read it. If it changes three files, I read all three. If it changes more, I still go through the diff before I push anything. I want to understand what went into the codebase. If a change is big enough that I cannot read it, I asked for too much in one step.

Claude can write code that looks good but is not how I want the system to be built. Sometimes it adds an abstraction that is not needed. Other times it keeps something too simple when I know the feature will need stronger boundaries.

When that happens, I ask it to redo the implementation. I do not care that the first version works. If I do not like the design, I change it.

This is probably one of the biggest problems I see with AI coding. Generated code often looks finished. The naming looks fine. The structure looks clean. There are comments. There are tests. The explanation sounds confident. That makes it easy to accept code without understanding it.

I try not to do that.

Where I slow down on purpose

Architecture is one place. I do not want AI deciding the shape of the whole system without me thinking about it first. I might ask Claude for options, or use it to challenge my approach, but I make the final decision.

Database design is another. A bad schema decision stays in the system for years. So I read schema changes slowly. I look at what the natural key is and whether it is actually unique. I check what type and precision the money column has, and whether the change can run while the old code is still deployed. I think about migrations before I think about the ORM code that sits on top of them.

Anything involving money gets extra attention. If a piece of code can charge a customer, move money, calculate usage, apply credits, or affect billing, I review it manually. I do not want to trust it just because the tests passed and the implementation looks clean.

There are also things I simply do not give Claude access to. It reads the application code and the tests. It does not read my environment files, and it does not run anything against production. It does not need access to secrets just because it is working inside the repository.

The same is true for database migrations and backfill scripts. A bad API change can usually be fixed with another deployment. A bad data migration can leave you with damaged production data. That is a different problem.

I start every migration with a dry run

When Claude writes a migration or a backfill, I trace it manually. I check which rows it will read and which rows it will update. I check the conditions. I think about what happens if the script fails halfway through, and whether it can be run more than once safely.

I also like to start with a dry run. Before writing the version that changes the data, I ask Claude to write a version that only tells me what it would change. I run that first and inspect the output.

If the numbers or records do not look right, nothing has been changed yet.

Only after that do I move to the script that actually updates the data. I ask for two things on every backfill. It prints a count before it writes, and running it a second time changes nothing. If the second run still reports work to do, the script is not safe to rerun and I am not shipping it.

It takes a little longer, but I would rather spend ten extra minutes checking a migration than spend hours trying to repair the data later.

My definition of done has changed

Claude saying the task is complete does not mean the feature is done.

The code compiling does not mean it is done.

Tests passing does not mean it is done either.

I still run the application myself. I test the main flow, then I try the cases that should fail. I try bad input. I try edge cases. I send the same request twice. I check the API response, and then I check what actually landed in the database. If there is a UI involved, I use it like a user would.

I am not the only check. Generated code has to pass the same test suite as everything else. But the suite is a weaker check than it sounds. If Claude wrote the feature, it probably wrote some of the tests too, so I read those the way I read the rest of the diff.

A test suite catches a missing null check. It does not catch a design I should not have shipped, or a feature that solves the wrong problem. That part is still on me.

I read the diff once for correctness bugs, then again for anything touching secrets or auth. I also have AI agents review the code, and Claude skills for an extra pass. Anything touching money or production data gets a colleague on the pull request as well, and I tell them which part I am least sure about.

I push the code myself. I raise the pull request myself. Before I push, I read the code. Before I create the pull request, I make sure I am comfortable putting my name on it.

Ten files in a minute is not progress

The goal is not to generate as much code as possible. The goal is to ship software that works. Those are not the same thing. Claude can change ten files in a minute. That feels productive. But if I do not understand the changes, or if the code adds complexity we do not need, I have not really saved time. I have just moved the work somewhere else.

Reading code is much faster than writing it, so the saving is real. It is smaller than a demo makes it look, because the review and the testing do not get faster, and it goes to nothing if I skip them. AI gives me more speed during implementation. I try to use some of that saved time for review and testing.

The skills I practice on purpose

There is another skill I actively try to protect: debugging. Sometimes I intentionally build a feature without AI, mostly backend and database work. AI could probably help me finish the feature faster, and there is no business reason to do it by hand every time. I do it because I want to know I can still build something from start to finish by myself. I want to stay comfortable writing code without having to ask Claude what the next step is.

I do something similar with bugs. Sometimes, instead of giving the error to Claude immediately, I try to trace the problem myself. I follow the request through the system. I read the logs. I inspect the data. I check where the value changed. I try to understand the actual cause.

Then I fix it.

After that, I might ask Claude to review the fix or tell me if I missed anything. If every bug becomes a prompt the moment I see an error message, I stop practicing the part of engineering where I have to reason through the system. That is something I do not want to lose.

Using AI without thinking is the real risk

I do not think using AI makes someone a worse engineer. I think using AI without thinking can. There is a difference.

AI can help me move faster and still make me better at my job if I use it to remove repetitive work. It becomes a problem when I use it to remove understanding. If I ask Claude to build something, press accept, and move to the next ticket, eventually I am going to lose track of how my own system works.

I expect AI to write more of my code in the future, not less. The tools are getting better quickly.

I am fine with that.

So I will keep letting Claude write a lot of the code. But I still make the architecture decisions. I review the database. I read the changes. I read every migration before it runs. I test the feature. I push the code. I raise the pull request. I debug things myself from time to time.

None of that is new. It is how I have worked for five years.

That is the part I do not want to hand over. I just want to make sure I am still the one doing the engineering.