AI

My Claude Code Refactoring Review Loop

How I used Claude Code to scan a codebase, produce a numbered quality report, and apply approved refactors one finding at a time.

Rizky Romadon profile photoRizky Romadon··19 min read

Sources

Article details

Context for reading and verifying this note.

Updated Jul 13, 202619 min readAI6 sourcesAI-assisted, human-reviewed

Creation context: AI assisted with research, structure, or drafting. Rizky Romadon reviewed the sources, technical claims, examples, and final publishing decision.

Introduction

I recently wanted to refactor a codebase without turning the work into a large rewrite. My focus was narrow: make the code style more consistent, improve how the code represented data, and review security-sensitive areas that had accumulated over time.

Instead of asking Claude Code to "clean up everything," I asked it to inspect the repository as if it were performing a SonarQube-style review. The first output I wanted was not code. I wanted a report: numbered findings, affected locations, reasons, risks, and proposed directions.

Then I reviewed the findings one by one.

I did not tell Claude to fix the entire report. I chose which finding was valid, which one needed more explanation, which one should be deferred, and which numbered item it was allowed to change. After each approved fix, I reviewed the diff and verification result before moving to the next number.

That control loop was the most valuable part of the experience. Claude was fast at reading across the repository and organizing possible improvements. I remained responsible for deciding whether each suggestion matched the codebase, preserved behavior, and was worth the change.

This is the story of that workflow and the lessons I would carry into the next refactoring session.

TL;DR

  • I asked Claude Code to scan first and make no edits.
  • The review focused on code style, maintainability, data types, reliability, and security concerns.
  • I requested a numbered report so every finding had a stable identity.
  • I reviewed each finding against the real code and business context.
  • Claude fixed only the number I explicitly approved.
  • I checked the diff, tests, type checks, and relevant security assumptions after every fix.
  • The workflow helped me move faster without turning the agent into an automatic code-quality gate.

Why I Started the Refactor

The codebase was working, but "working" is not the same as being easy to change safely. Small inconsistencies had started to increase the cost of reading and reviewing the code.

Some of the concerns were about style: naming, repeated branches, methods doing more than one job, and patterns that differed between modules. Other concerns were about data representation. A plain string, nullable field, or loosely shaped object can be convenient initially, but it can also allow invalid states to travel much further than they should.

Security was the third lens. I wanted another pass over validation, authorization boundaries, sensitive logging, external input, query construction, and error handling. I was not assuming that the code contained a critical vulnerability. I wanted to identify security-sensitive places where the safety argument was unclear or depended on an undocumented assumption.

My goals looked like this:

AreaWhat I wanted to improveWhat I did not want
Code styleConsistency and easier reviewCosmetic churn across unrelated files
Data typesFewer invalid or ambiguous statesType changes that silently alter behavior
MaintainabilitySmaller responsibilities and clearer intentNew abstractions with no practical value
SecurityExplicit validation and authorization boundariesAutomated fixes based only on pattern matching

This distinction mattered. A broad request such as "refactor this repository" gives a coding agent too much freedom to decide both the problem and the solution. I wanted Claude to help discover candidates, but I did not want discovery to become authorization.

Why It Matters

Repository-wide refactoring creates a review problem before it creates a coding problem. The more findings an agent produces, the easier it becomes to approve them in bulk without understanding the behavior hidden behind each change.

A numbered, one-at-a-time workflow turns a large and uncertain refactor into a sequence of small decisions. Each decision can have a clear reason, bounded diff, verification step, and rollback point.

That is consistent with how I already think about AI-assisted engineering work: AI is most useful when it organizes uncertainty and provides a strong second reading, while the engineer still owns the final judgment.

I Asked for a Report Before Any Code Changes

The first instruction was the most important: inspect the repository, but do not edit it.

I wanted the exploration phase separated from the implementation phase. If Claude changed files while it was still discovering the codebase, I would have to review both its findings and an expanding diff at the same time. That would make it harder to decide whether the proposed problem was real.

The prompt was similar to this:

Prompt
Review this repository as a senior code-quality and application-security reviewer.
 
Use SonarQube-style quality lenses, but do not claim exact parity with a real
SonarQube rule profile. Focus on:
- maintainability and code style
- reliability and error handling
- data types, nullability, and invalid states
- validation, authorization, and sensitive data handling
- injection risks and security-sensitive configuration
- duplicated logic and unnecessary complexity
 
Do not edit any file.
Do not generate a patch.
 
Return a numbered Markdown report. For every finding include:
1. Stable finding ID
2. Severity
3. Confidence
4. Category
5. File and symbol
6. Evidence from the current code
7. Why it matters
8. Smallest reasonable fix
9. Possible behavior or compatibility risk
10. Verification required after the fix
 
Separate confirmed findings from questions that need domain context.
Avoid reporting formatting issues already enforced by the project tooling.

The phrase "SonarQube-style" needs care. SonarQube executes language-specific rules selected through quality profiles. Those rules produce issues across maintainability, reliability, security, and security hotspots. Asking a language model to act like SonarQube does not reproduce that deterministic analyzer, its exact rule versions, its data-flow engine, or the quality profile configured for a project.

I used the phrase to define review categories and the shape of the report. I did not use it as proof that every SonarQube rule had run.

The Numbered Report Became My Review Queue

The report changed the conversation from "please improve this codebase" to "let us evaluate finding F-001." That is a much better unit of work.

A useful finding was not just a statement that some code could be cleaner. It contained enough evidence for me to make a decision without asking Claude to rediscover the context.

The shape looked like this:

IDCategoryEvidence requiredDecision
F-001MaintainabilityLocation, repeated pattern, concrete costFix, defer, or reject
F-002Type safetyInvalid state currently representableReview compatibility first
F-003ReliabilityFailure path and observed consequenceCheck tests and operations
F-004Security hotspotSource, sink, protection, missing contextManual security review

Stable numbers solved several practical problems.

First, I could discuss one issue without pasting the entire report again. Second, Claude could keep the requested scope small. Third, I could record why a finding was accepted or rejected. Finally, if a later change caused a regression, the diff and its original rationale were easier to trace.

I also asked Claude to mark confidence. A high-severity, low-confidence suggestion is not ready for an automatic fix. It is a request for more investigation. A lower-severity finding with strong evidence may be a safer first change.

Field Notes: Reviewing Findings One by One

My review was not a process of accepting the report from top to bottom. I treated each item like a code-review comment from another engineer.

For each number, I opened the referenced code and asked a few questions:

  1. Is the evidence actually present in the current version?
  2. Does Claude understand the method's responsibility and callers?
  3. Is the reported behavior intentional?
  4. Would the proposed change alter an API, serialized value, database record, or integration contract?
  5. Is the finding already covered by a linter, compiler, test, or existing decision?
  6. Can the fix be isolated from unrelated cleanup?
  7. What concrete verification would show that behavior is preserved?

I used four decisions:

DecisionMeaningNext action
ApproveEvidence is correct and the fix is boundedImplement only this ID
ClarifyThe risk may be real, but context is missingAsk for call paths or alternatives
DeferValid issue, wrong time or too much migration riskRecord rationale and leave code unchanged
RejectFalse positive, intentional pattern, or weak valueExplain why and close the item

Rejecting a finding was not a failure of the workflow. It was evidence that review was doing its job. Static analyzers also distinguish between issues that demand a fix and security hotspots that require human review. An AI-generated report needs at least the same discipline.

My One-Finding-at-a-Time Fix Prompt

When I approved a finding, I referenced only that number and constrained the change. A typical instruction looked like this:

Prompt
Implement finding F-007 only.
 
Before editing:
- restate the problem in two or three sentences
- list the exact files you expect to change
- explain how behavior will remain compatible
 
During implementation:
- make the smallest coherent change
- follow existing repository conventions
- do not reformat or refactor unrelated code
- do not add a dependency unless I explicitly approve it
- preserve public APIs and serialized data unless the finding requires otherwise
 
After implementation:
- show a concise diff summary
- run the targeted tests, type checks, and lint checks
- report the commands and results
- stop; do not continue to F-008

The last line was important. Without it, an agent can interpret a successful fix as permission to continue through the queue. I wanted an explicit stop between findings so I could inspect the new repository state.

This is also why I prefer small AI-generated diffs. My separate checklist for reviewing AI-generated code before merge goes deeper into domain rules, data integrity, and production behavior. The same review standard applies even when the change is called a refactor.

Data-Type Improvements Needed Compatibility Checks

"Improve the data types" sounds safe, but type refactoring can change behavior far beyond the compiler. A string converted to an enum may affect JSON, database persistence, message consumers, test fixtures, and unknown values received from another system.

Consider a representative example—not a claim about the exact private codebase—from a service that accepts a status as a string:

JAVA
public void updateStatus(String status) {
    if (status == null || status.isBlank()) {
        throw new IllegalArgumentException("status is required");
    }
    this.status = status;
}

Changing it to an enum makes the allowed states clearer:

JAVA
public enum AccountStatus {
    PENDING,
    ACTIVE,
    SUSPENDED
}
 
public void updateStatus(AccountStatus status) {
    this.status = Objects.requireNonNull(status, "status is required");
}

The second version has a stronger local model, but it raises compatibility questions:

  • What happens when an older database row contains another value?
  • Does the API accept lowercase values?
  • Will a message consumer reject a future status added by another service?
  • Does the persistence framework store names or ordinals?
  • Is null currently used to mean "not evaluated"?
  • Do callers need a migration period?

For type-related findings, I asked Claude to map the value across boundaries before changing it. A good report included constructors, mappers, serializers, database columns, API schemas, and tests. A suggestion that looked elegant inside one class could still be unsafe across the system.

The lesson was simple: stronger types are valuable when they make invalid states harder to express, but the migration must respect every place where the old representation is part of a contract.

Security Findings Required a Different Review Mode

I did not treat a security finding like a naming improvement. Security depends on data flow, trust boundaries, deployment configuration, and protections that may not be visible in one function.

For example, a URL passed to an HTTP client may look like an SSRF risk. The real review needs to identify where the URL originated, how it was parsed, whether redirects are followed, which destinations are allowed, and whether network-level egress controls exist. That deeper reasoning is why I keep a separate Spring Boot SSRF mitigation workflow for this class of problem.

I used this security triage table:

QuestionWhy it matters
What is the untrusted source?A value is not dangerous only because it is a string
What sensitive operation receives it?The sink defines the possible impact
What validation or authorization already exists?Local code may not show upstream controls
Can the protection be bypassed through encoding or alternate paths?Happy-path validation is not enough
Is this a vulnerability or a hotspot needing review?The response and urgency are different
What test proves the protection?Security fixes need evidence, not only cleaner code

If Claude could not demonstrate the source-to-sink path, I treated the item as a question, not a confirmed vulnerability. If the risk involved authentication, authorization, secrets, cryptography, deserialization, file access, command execution, or outbound network access, I slowed the workflow further and considered an independent review.

The team operating model for AI coding agents describes the broader approval boundaries I use for risky backend changes. The core rule is the same: an agent can help identify and implement a security improvement, but it cannot be the only party that declares its own change safe.

Verification Happened After Every Approved Fix

The report was qualitative. Verification needed deterministic signals.

After each change, I wanted Claude to run the narrowest useful checks and show the result. Depending on the repository, that meant a targeted test, compiler or type checker, linter, formatter check, dependency scan, static analyzer, or a full build.

My loop was:

Prompt
Baseline -> inspect -> report -> review one finding -> approve one fix
         -> inspect diff -> run targeted checks -> run broader checks
         -> record decision -> continue or stop

The verification order mattered:

  1. Inspect the diff for scope creep.
  2. Run tests closest to the changed behavior.
  3. Run type checking or compilation.
  4. Run repository lint and formatting checks.
  5. Run the real static and security analyzers available to the project.
  6. Run the full build when the change crossed module boundaries.
  7. Review public contracts, migrations, logs, and configuration separately.

I did not accept "the change should work" as a result. I wanted the exact command, exit status, and any warning that remained. If a required tool could not run, the finding stayed unverified.

For a Java service, I would also compare the result with a broader Spring Boot production checklist, because passing unit tests does not prove that configuration, database behavior, observability, and deployment assumptions are safe.

What Worked Better Than a Bulk Refactor

The one-finding loop was slower than accepting every proposed fix in a single command, but it made the useful work faster to trust.

It reduced several common costs:

  • Review remained connected to one stated problem.
  • Unrelated formatting did not hide behavior changes.
  • A bad suggestion could be rejected before it influenced later changes.
  • Tests could be selected for one risk instead of a vague repository rewrite.
  • Commit messages and pull-request notes could explain why each change existed.
  • Rolling back one decision did not discard the rest of the refactor.

It also improved the conversation with Claude. When I rejected a finding and explained the repository-specific reason, later analysis became more grounded. The workflow was not only Claude reviewing the code; it was me teaching the session which constraints mattered.

Common Failure Modes I Would Avoid

This workflow still has failure modes. Most come from giving the report more authority than its evidence deserves.

Asking for the latest rules without supplying the profile

"Use the latest SonarQube rules" sounds precise, but a real analysis depends on the SonarQube version, installed analyzers, language, active quality profile, and project configuration. If exact parity matters, run the actual analyzer and give Claude its output. Do not rely on a role prompt.

Mixing discovery and editing

If Claude scans and fixes simultaneously, the repository changes underneath the report. Later findings may refer to code that no longer exists, and review becomes harder to reproduce.

Accepting severity without inspecting impact

Severity is a triage suggestion, not proof. A seemingly minor type change can break a public contract. A scary security label may be a hotspot protected by another layer. Inspect the whole path.

Fixing a number plus nearby cleanup

Agents often notice adjacent improvements. That is useful during discovery but harmful during a scoped fix. New ideas should become new findings, not hidden additions to an approved diff.

Letting the agent verify itself only with prose

An explanation is not a test result. Require executable checks, then review whether those checks actually cover the risk.

Losing the report as the session grows

Long sessions accumulate noise. Keep the accepted report in a repository note or issue, and refer to stable IDs. If context becomes unreliable, start a focused session with the selected finding and its evidence.

A Decision Framework for Each Finding

Before authorizing a fix, I now use a small decision framework:

DimensionLow-risk signalHigh-risk signal
EvidenceExact path and reproducible behaviorGeneral pattern with no trace
ScopeOne responsibility and a small diffCross-module or repository-wide change
Contract impactInternal implementation onlyAPI, schema, database, or event change
Security impactDefense-in-depth improvementAuth, secrets, injection, or trust boundary
VerificationExisting targeted testNo test or environment-dependent behavior
RollbackRevert one isolated commitData migration or irreversible transformation

I approve immediately only when the evidence is strong, the scope is bounded, compatibility is understood, and verification is available. Otherwise, I ask for clarification, split the finding, or defer it.

This is not bureaucracy for its own sake. It keeps code-quality work from becoming a source of accidental product changes.

What Claude Code Cannot Replace Here

Claude Code can read across files, explain patterns, propose tests, and produce a structured review. Current Claude Code documentation also describes a dedicated Code Review capability that looks for logic errors, security problems, edge cases, and regressions. Even that review is described as best-effort and keeps existing review workflows intact.

The limitations remain important:

  • Claude does not become a deterministic static analyzer because of a prompt.
  • It may miss a data flow that a specialized analyzer detects.
  • It may report a problem without knowing a runtime or infrastructure control.
  • It cannot infer every business invariant from source code.
  • It can propose a locally clean change that breaks an external contract.
  • It should not approve its own security-sensitive patch.

The strongest setup is complementary: compilers, tests, linters, SonarQube or another static analyzer, dependency and secret scanners, Claude-assisted reasoning, and human review. Each catches a different class of failure.

How I Would Improve the Workflow Next Time

The experiment gave me a repeatable starting point, but I would make a few improvements before the next repository scan.

First, I would capture repository-specific rules in a concise CLAUDE.md: build commands, test commands, architecture boundaries, generated folders, code-style exceptions, and files that require special approval. This reduces repeated prompt context and makes findings more consistent.

Second, I would provide the actual static-analysis configuration when available. That includes the SonarQube quality profile, linter configuration, compiler options, and suppressed rules. Claude can then explain or prioritize real tool output instead of approximating it from general principles.

Third, I would separate report passes:

  1. Maintainability and style.
  2. Data types and contracts.
  3. Reliability and failure handling.
  4. Security and trust boundaries.

Separate passes reduce shallow lists and make the required evidence clearer.

Fourth, I would store every decision beside the report:

MARKDOWN
### F-007 — Replace loosely typed status values
 
- Decision: Approved with compatibility constraint
- Reason: Invalid values were representable inside the domain layer
- Constraint: Preserve existing JSON values and database representation
- Verification: Mapper tests, persistence tests, API contract tests
- Result: Fixed in commit <hash>

Finally, I would keep fixes commit-sized. One finding does not always equal one line, but it should usually equal one coherent reason to change the code.

Production Readiness Checklist

Before merging a batch of these refactors, I would check:

AreaRequired evidence
ScopeEvery changed file maps to an approved finding
BehaviorTests cover preserved behavior and intended improvement
TypesAPI, storage, serialization, and message compatibility reviewed
SecurityTrust boundaries and source-to-sink paths reviewed manually
ToolingCompiler, tests, linter, and configured analyzers pass
OperationsLogs, metrics, errors, and configuration remain usable
RollbackChanges are isolated enough to revert safely
DocumentationDeferred and rejected findings include a reason

I would also reread the final combined diff. Small changes can interact even when each one passed independently. The repository after F-012 is not the same repository that existed when F-001 was reviewed.

Conclusion

My biggest lesson was not that Claude Code could imitate a static-analysis report. The valuable lesson was that a report can create a disciplined interface between AI analysis and human approval.

Claude helped me scan broadly, organize findings, explain possible risks, and implement focused changes quickly. I kept control by requiring a report before edits, reviewing every number, authorizing only one fix at a time, and demanding verification after each change.

That made the refactor calmer. I did not need to trust a repository-wide rewrite or reject AI assistance entirely. I could use Claude's speed for exploration and implementation while keeping engineering judgment at every boundary.

The practical next step is small: ask for a read-only report, choose one well-supported finding, and take it through the full review loop. Do not measure success by how many findings disappear. Measure it by whether the code becomes easier to understand and safer to change without losing behavior you still depend on.

Related posts