I certainly agree with Andrej Karpathy, who coined the term “vibe coding,” on one point: “It’s not too bad for throwaway weekend projects.”
If we treat the prompt and its supporting resources as a compressed representation of our knowledge about a project, vibe coding is a form of lossy compression: the resulting application can embody far more project-specific know-how than the initial specification captured. That means we need to provide additional context, inspect the results, and challenge them throughout the process.
In this part I’m sharing the architectural proposals I asked Claude to change, the questions that helped, and the lessons I wanted it to remember afterwards. We’ll work through duplicated calculations, state leaking between records, and a few cases where a locally reasonable fix created a wider problem.
In one project, I was using Claude Code with Opus 5.5 to build a mechanism for calculating checksums for profile pages. The goal was to update the updated_at date so Google would detect changes more efficiently during indexing.
One proposal seemed quite sensible: calculate a fingerprint for each profile so we could detect changes to its content.
There was just one catch: our processing pipeline already calculated checksums. Claude implemented the same functionality again without flagging the duplication or asking for an architectural decision.
Makes no mistakes one who does nothing… :)
I apply that to myself too. Accepting and merging a change is still an engineering decision, even when an agent wrote most of the code. Describing what went wrong is useful when it changes how we investigate the next task.
Part 2 of this article series covered the review process. Here I want to go further into what I actually challenge during a review, and what I ask Claude to carry into future sessions.
Keep one recent proposal from your own repository in mind as you read. Would you ask for the same checks? Is there an existing mechanism the agent should have found? The cases include prompts and small exercises you can adapt, so you can turn that suspicion into something more useful than “this design feels wrong.”
I also include two small Python exercises. The real project examples involve PHP and PostgreSQL, but the exercises isolate the failure patterns without requiring access to our application or database. They are teaching examples, not exports of production code. You can ask LLM to translate the checks into the language and test framework your project uses after you understand what they establish.
When I say that Claude is learning my project, I mean that we are improving the instructions and notes available to later sessions. Asking it to remember a correction does not retrain the model’s weights.
Claude Code supports project instructions and persistent memory files. Those files can supply useful context, but they do not enforce behavior or guarantee that every future agent will use a particular lesson. The memory documentation explains the distinction and how to inspect the stored material with /memory.
There is a little of Memento movie in this arrangement. The film’s main character relies on notes to recover context. Here, a later session can read a Markdown file containing what an earlier session learned. The note needs to say enough to be useful, and we still need to question it when the situation changes.
That is why I want to see what was recorded. “Remember to be careful” sounds reassuring, but it gives the next session very little to do. Name the component, the failure, and the condition that exposed it. Then the reminder can lead to a check.
I also separate my preferences from team knowledge. A preference for shorter explanations can stay personal. A rule about the lifecycle of a shared parser belongs where other people working on that parser can find it too.
Open /memory and find where the relevant project guidance is stored. Choose a correction you recently made and ask Claude to show the saved wording. If you have only said “remember this,” do not assume that the useful details landed in the right place.
I would use a request like this:
Show the guidance relevant to this component and explain when it is used.Compare it with the mistake we just investigated.Propose one concise update containing the failure condition, the existingmechanism to reuse, and the check that would catch a recurrence.Avoid adding the entire session history or repeating existing rules.
Read the proposed note as a future maintainer. Could someone understand what to inspect without reading today’s conversation? If it says “always use the existing checksum,” ask which checksum, which inputs it covers, and when it is valid. If it says “never keep state,” ask whether the component legitimately needs a cache with a defined lifetime.
Read that note with the same attention you would give a code change. Otherwise, we can take one mistaken suggestion, save it, and help the next session repeat it more confidently.
These are examples from 1 month of work with Claude Code on project:
Prompt: "Check recent sessions and list short examples of mistakes Claude made, and how each was fixed: caught by an automated skill, by re-verification in the same session, or by me."Context: a large PHP web app with crawlers, background daemons and a production database. Claude writes the fixes, and daily "skills" (automated agent runs) then test tickets on production, triage error logs and review PRs.Caught by an automated skill or CI- Retry cap used up too early: Claude limited a failed-download retry to 3 attempts but missed that another code path already retried. The cap ran out within a minute and the scheduled retry never ran. The prod-test skill marked it FAILED, and the fix replaced the cap with a cool-down.- Ping-pong after a fix: a fix moved an external ID to the right record, but two records both matched it, so the ID bounced between them (1.3k warnings in 2 days). Prod test FAILED, and a follow-up fix resolved it.- Incomplete fix for leaked locks: the fix covered one cause of leaked processing locks but missed others (deploy restarts, timeouts, killed workers). New leaks appeared after deploy, so a cleanup sweep and an index were added.- One-off cleanup without prevention: a one-off script cleared bad flags, but they came back within a day. A nightly sweep was added.- Fix guarded only the crashing line: auto-fixes generated from stack traces guarded only the line that crashed. Later checks found the same null bug elsewhere on the same path, one of them hidden because the first crash stopped it from running.- One character broke the build: an apostrophe in a code annotation broke a config file generated at build time. Only CI caught it.- Static analysis claimed clean: Claude reported static analysis as clean but dismissed some errors as harmless, and CI failed. Fix: run the exact CI check before every PR.Caught by re-verification- Ticket premise wrong: a ticket asked to "fix" 200k database rows. Checking against prod data showed the rows were correct, so nothing was changed.- Wrong root cause in a ticket: the real cause was different and the ticket was rescoped.- Unsafe branch rejected: a branch would have overwritten real production data with mock data on prod, so it was dropped for a variant that doesn't save anything.- Automations repeating their work: automations posted comments but sometimes failed to save their own state. Checks for existing comments caught this before duplicates went out.- Time-zone bug: a date helper shifted timestamps by an hour across daylight-saving changes, which could make a paging loop run forever. A safe variant was added.Found by the developer- Hidden database load: new queries were correct but overloaded the prod database. New rule: estimate query cost and propose an index up front.- Data leaking between items: a cached value in a long-running worker leaked one item's data into the next. New rules: no per-item state in shared services, plus a test that runs two items through one worker.- Error stopped the whole pipeline: throwing an error on an empty page skipped every later step. New rule: read the whole pipeline before aborting.- Real personal data in artifacts: a real person's protected name ended up in a commit, a test and a docs page. It needed a history rewrite, so the rule is now fictional names only.- Transaction hiding failures: Claude proposed wrapping a failing multi-step operation in one transaction, which would hide the failure. Rejected in favour of fixing the root cause and letting the queue retry.- Silent deferrals: an automation quietly postponed tickets, and they looked skipped. Two of them, once tested, had failed.- Smaller slips: wrong markup format in tickets, code comments that read like investigation notes, integer overflow in a default parameter, and wrong SQL clause order in migrations. All are now project rules.Takeaway: most of these passed local tests and showed up only with production data or load. The daily prod-test skill and checking real data before coding catch the most.
Some of these are small corrections. In one of cases listed above under “Found by the developer” PHP_INT_MAX looks convenient as a default upper bound, but a value valid in PHP can exceed a database parameter’s intended range. The failure depends on how the query binds or casts it; comparing a large value does not always fail. An absent bound should usually be represented explicitly, for example with null and no corresponding filter.
The migration issue is less ambiguous: the PostgreSQL syntax reference puts TABLESPACE before WHERE. Recording that convention helps, and executing the migration in a suitable test environment is stronger evidence than a memory note. Interesting part is that developers often made exactly the same mistake in the past, which was detected during technical review.
The architectural entries need more room. A short rule becomes much easier to judge once we see the problem that produced it.
One task concerned the “last updated” date on profiles. Background processing can revisit a profile without changing the information a visitor sees. We wanted the date to reflect a meaningful content change, rather than move every time the pipeline ran.
Claude proposed a profile fingerprint. After processing, it would gather selected profile fields and related rows, calculate a hash, and compare it with the previous value. A different fingerprint would update the date.
As a standalone design, this sounded reasonable. It also introduced new calculator classes, extra queries, and another maintained list of fields defining what “changed” meant.
The project already had checksums in its processing-information tables. The merge pipeline used them to recognize unchanged inputs and avoid repeating work. I pointed Claude towards those fields and asked why the new fingerprint could not be derived from them.
That changed the implementation substantially. Instead of reading and hashing much of the profile again, we could combine the relevant existing checksum values. The duplicate field lists and associated queries could go.
It is a little like receiving a warehouse inventory and then proposing to reopen every box to make another inventory. Recounting may be necessary if the first list misses something. I would still read that list before ordering the extra work.
This is the kind of correction I find myself making more often now. I can understand significant parts of proposed code and question why we need to add it.
Another proposal concerned merging duplicate data records. The operation moved relationships from one record to another, created redirects, and removed the duplicate. Failures midway through that process left incomplete work.
Claude identified the missing encompassing transaction as a main defect and proposed making the whole operation atomic.
I asked it to look more closely at the failure and at the existing reprocessing mechanism. Two concrete problems appeared. Moving queued work could conflict with a partial unique index. Moving relationship rows could also hit a uniqueness conflict, and a catch-and-ignore path discarded more information than intended.
Wrapping the same operations in a transaction would not remove those conflicts. We needed to decide how overlapping rows should be handled and stop swallowing errors.
The revised approach corrected the conflict handling, preserved nonconflicting relationships, and re-enqueued the affected records when processing failed. The exception remained visible. The operation was intended to converge when its idempotent steps were retried.
I would be careful about turning this case into a general rule against transactions. Atomicity can be essential, and retryable steps do not automatically make a partially completed operation acceptable. A transaction and a recovery mechanism can also be useful together.
There is a concurrency detail here as well. Checking that a row does not exist before moving another row does not, by itself, prevent a competing process from changing the situation. Database constraints and the concurrency strategy still need to support the intended behavior.
I wanted Claude to explain which intermediate states were acceptable, which conflicts we expected, and how interrupted work recovered. We had reached for “add a transaction” before answering those important questions.
Part 2 of this article series discussed batch workers as a way to reduce repeated process startup. A document parser showed why that change also needs a careful review of object state.
Under the earlier execution model, one process handled one report. A helper cached values from the current and previous reporting years. Starting another process for the next report also discarded the helper and its caches.
After moving to batch processing, the same process handled several reports. The current-year state was refreshed, but previous-year values could survive from an earlier report. Processing the next company could therefore produce valid-looking values belonging to the wrong company.
This was more difficult to detect than an exception. The parser returned values of the expected shape, and the application could store them successfully. The relationship between the value and its source report was wrong. Everything could look ordinary unless we checked which report each value belonged to.
The old process boundary had quietly done part of the cleanup for us. Keeping the worker alive was like keeping the same desk for the next report: useful, provided the previous report’s notes were cleared away. We had changed how long the desk stayed in use without checking everything left on it.
My instruction to remember was to prefer stateless processing services where possible and to inspect the entire lifetime of anything reused inside a batch (it’s a good practice to avoid using stateful services). Passing the report explicitly makes dependencies easier to see. If a cache is necessary, its key and invalidation must cover all the data it represents.
The most useful regression check is very specific. Process report A and report B through the same instance, then compare B’s result with the result from processing B in a fresh instance. Repeat in the other order, with deliberately different values in the fixtures.
A test that creates a new helper for every report can pass while the batch worker is wrong. The test needs to reproduce the reuse that exposed the problem.
I now treat a change from short-lived processes to long-running workers as a lifecycle change. It deserves questions about instance fields, static state, caches, and recovery in addition to questions about speed.
In a crawler tool change, Claude added an exception when one crawled page section did not provide the expected result. Looking only at that section, the reaction was understandable: the page could not be processed, so signal an error.
The crawler had more work to do afterwards. It also downloaded a separate document from another tab, and that step did not depend on successfully reading the first section.
The new exception stopped the whole chain. A local improvement in error reporting prevented an independent result from being collected.
The existing “continue anyway” behavior had a reason. This is easy to overlook when an agent cleans up a function in isolation: an awkward-looking branch may be preserving a useful result elsewhere. We had to follow the downstream steps before deciding where the failure should be handled.
I asked Claude to remember this as a pipeline question: what follows this line, which outputs depend on it, and which can still be produced? The fix should preserve useful independent work while reporting the failed part accurately.
This does not mean catching every exception and continuing regardless. An invalid shared session or corrupted common input might make the rest of the run meaningless. The important step is identifying the scope of the failure instead of assuming that every local error should stop everything.
Another change checked whether a redirect would send the visitor to a profile that should not be available. The logic addressed a real problem. The database cost of running it on a frequent request path had not been adequately considered.
Once the check ran repeatedly under normal traffic, it put too much load on the database.
The lesson I asked Claude to record was to estimate the cost when proposing the database query, and to include the required optimization or index in the same PR. A query that looks small in a code diff can run extremely often.
I want the review to connect the query plan with its frequency. How often will this execute? How many rows might it inspect? Is there an appropriate index? Can existing data answer the question without another lookup?
If caching is proposed, its freshness also needs to fit the decision. A cache for a restriction check needs an invalidation policy that preserves the intended behavior.
For measurements, use an appropriate environment with representative data. As the PostgreSQL EXPLAIN documentation explains, EXPLAIN ANALYZE executes the statement, so the measurement itself needs to be suitable for that environment.
That reasoning applies to writes as well. The fingerprint case started with redundant computation, then exposed the cost of repeatedly updating the wrong table.
After these cases, “memorize this mistake” has become a more specific request for me.
A useful note about the parser case would say that helpers survive across reports in the batch worker, that current-year and previous-year values must not leak between inputs, and that verification should process two different reports through the same instance.
A note about fingerprints would point to the existing processing checksums and ask for a coverage check before introducing another mechanism. It should also mention the temporary reset state, because that detail is difficult to guess from a high-level description.
Related to heavy database query Claude should always consider to verify and optimize introduced new queries. It should also consider creating dedicated database INDEX.
I want these notes close to the relevant work, with a short shared rule where appropriate and more detail available when needed. If I keep appending whole conversations, I will eventually create a history book that each session has to search for one useful sentence.
The notes also need maintenance. A correction can become outdated when the architecture changes. And an incorrect lesson, such as “never use transactions,” could make the next proposal worse.
Memory helps bring the question back. A regression test, a database constraint, a clear service interface, or a measured query plan provides stronger evidence that we answered it correctly.
In many of these sessions, my intervention was short. I named an existing checksum, asked about a later crawler step, or pointed to the history of a feature. Claude then did substantial work checking the evidence and revising the implementation.
That is why I still find experience with the codebase and the business so valuable. I do less of the mechanical implementation myself, but I need to recognize when a plausible proposal starts from the wrong understanding of the system.
I also learn from the exchange. Sometimes the agent finds a real complication after following my suggestion, as it did with the checksum coverage gap. Reusing existing code was a good direction, but it was not the end of the investigation.
In Part 2 I mentioned rubber-duck debugging. The duck now answers back and can propose a patch. That makes the conversation more useful, provided I keep asking it to show how the answer fits our project.
For me, this is what the shift towards solution engineering looks like in daily work. I define what should change, check what already exists, and review the consequences. When we find a mistake, I want both a corrected implementation and a lesson specific enough to help next time.
Thank you for reading this article. Please share your experience with debugging AI-generated code in the comments.
See also previous Part 5 Resuming Sessions, Working in Parallel, and Keeping Costs Sensible.
Preventing Claude Architectural Mistakes - Automating Project Implementation and Maintenance with… was originally published in Towards AI on Medium, where people are continuing the conversation by highlighting and responding to this story.