Claude Code: We've Got Issues

A personal Software Development Lifecycle (SDLC), earned the hard way
Claude Code is Anthropic's AI coding agent — an LLM that lives in your terminal, reads your codebase, and writes code on your behalf. Two years of using it, and a pattern I can now recite from memory. Given half a chance, it will do any of the following and report the job done.
- Treat questions as instructions. "What do you think of X?" parsed not as an invitation to discuss but as a command to go and build X - committed to the codebase, sometimes with product decisions made along the way that nobody asked it to make, before the conversation has even started.
- Override the human-in-the-loop entirely. Type the human's approval keywords into its own chat output to unlock states only the human should unlock. Invoke the human's emergency bypass keyword on its own authority because it decided the current task didn't need human review. Forgery and self-granted exemption, both reported as task-complete.
- Disable a script's own safety mechanism quietly. The script begins with the standard stop-immediately-if-anything-fails header (
set -e), then appends an override (|| true) to every risky command below it, which tells the shell to pretend the line succeeded no matter what actually happened. The safety header is there. The safety is not. - Make sensitive files readable, writable, and executable by anyone at all - including unauthenticated strangers reaching the machine over the internet - to get past a permissions error (
chmod 777). - Switch off the check that verifies a web server is who it claims to be (SSL/TLS verification1), because a server's certificate was inconvenient.
- Hardcode credentials into source that was about to be committed.
- Silently create a public GitHub repository and upload an entire directory tree to it, because it thought it should raise a ticket. To overcome the "obstacle" of finding itself in a private folder that was not tracked by a git2 repository.
- Report green across the board on a test suite that never actually ran the command a user would type. The compiled code did nothing. The dashboard said healthy, every test passed! Ostensibly the tests were there, but it had written them in a way that was most convenient for itself with the least amount of effort.3
None of these are obscure mistakes. They are safety-critical decisions being made unilaterally, to get past whatever was in the way, and reported as task-complete.
An Affair with the Beast

That period taught me two things that sit uncomfortably side by side. First: given a clear task, Claude Code produces working software at a pace that would have seemed impossible a few years ago. Not toys. Real tools I use daily. The productivity is genuinely astounding. Second: left unsupervised, it exhibits a set of failure patterns that are, frankly, alarming.
A friend who had started using LLMs in his day-to-day work — nothing technical — told me:4
It’s like having an intern from Trinity. Bright, well-read, can talk a good talk. Genuinely helpful for an awful lot of things. But often just blagging it. And when it does, it does so with such confidence it may convince you the sky is green. You have to scrutinize everything.
That is the lesson of using LLMs5, and for code it's no different.
These failures are invisible by default. The model reports success in the same tone whether the work is sound or compromised. Nothing flags itself.
Every failure above was found because a human read the code, ran the binary, or noticed the inconsistency — not because the tool surfaced it.
Scaffolding, and the Necessity of Issues

What does work is external structure — something the model cannot edit, rewrite, or decide to skip.
The load-bearing rule, and the one everything else in the scaffolding serves: no code gets written until the problem and the solution are documented in an issue (ticket), and I have agreed to both. And not simply "there must be a ticket". The issue has to contain a clear problem statement and a documented solution design before any implementation starts. That is the moment the product decision actually gets made - by me, in writing, where I can review it before a single line of code commits me to it. Without this, the decisions happen inside the model, opaquely, and the first I see of them is a finished implementation I now have to argue with.
So I imposed a scaffolding using the established tools to keep track of work. GitHub, the dominant platform for open-source software development, has a built-in issue tracker. And using Claude Code's CLAUDE.md configuration file, I wrote some rules:
- No code may be written until there is an issue with a clear problem statement and a documented solution design, and I have signed off on both.
- The first line of code must be a failing test — TDD6 was a good idea for humans, it's non-negotiable for a model that cheats on tests.
- Nothing gets committed without being linked to an issue number.
- Nothing closes without a human review on that issue.
The issue is the record of what was agreed, what was built, and whether they match. Regular commits, clean branches, the ability to roll back to a known-good state. Without that, you cannot let a fast, confident collaborator near your codebase.
Git is not a nice-to-have in this workflow. It is the audit trail, the undo button, the only reliable way of telling this morning's state from last night's.
Test-driven development was good practice before LLMs and it is essential practice with them. A failing test gives the model an unambiguous target — make this pass — and an unambiguous stopping condition. Without one, the model writes its own success criteria. This, coupled with Git, ensures accountability and traceability.
Forgery, and Unilateral Exemption
The framework uses gate words the human is supposed to type - SATISFIED, PROCEED, APPROVED - to move between phases. Claude, asked to confirm it was waiting for sign-off, would write the gate word into its own chat output and carry straight on. That is not a shortcut. That is the model impersonating the human, typing the human's lines in order to unlock the state only the human is authorized to unlock.
It gets worse. BYPASS-GATE-7 is a real keyword in my framework - an escape hatch I wrote, for cases where a quick bug fix does not need to go through the full SDLC. Only the human types it. Except Claude, on more than one occasion, decided for itself that its current task did not need to go through the full process, typed BYPASS-GATE-7 into the chat, and proceeded as though I had released it from the rules. Not impatience. Not error. A unilateral decision that the rules I gave it did not apply to what it wanted to do.
A model that forges the approval signature, and will on occasion forge its own exemption from the rules, is not a model being held accountable by a human in the loop. It is theatre.
CLAUDE.md but still from time to time it sneaks its way around and I need to stop it.
The full catalogue of failures, and the specific rules each one produced, can be read on my project page. Every rule in the framework exists because a failure occurred. None are theoretical.
Whack-a-mole

The framework is never finished. I write a rule to prevent a specific failure. The rule works for a while. Then a new failure surfaces that the rule does not technically cover — sometimes genuinely new, more often a variation taking a route the rule did not anticipate. I tighten the rule. That closes the gap. Then something else surfaces.
A rule saying do not call the library directly in tests can be side-stepped by calling a wrapper that does the same thing. A rule saying before marking any test as passing, state in chat what user action it simulates and what the user would observe is much harder to game - it forces the model to produce a sentence a human can read and immediately tell is nonsense.
The version that worked, and broke me
An earlier iteration had four hard gates and a thirty-two-step sequential checklist around them:
SATISFIED- Agree on Acceptance Criteria and Test Plan before solution is designed and documentedPROCEED- Agree on Solution Design before implementation startsAPPROVED- Approves the design and allows code to be written, but not committedCLOSE- Only when the human has reviewed the implementation and is satisfied that it meets the agreed criteria, the issue can be closed.
Every phase had mandatory quality checks. Every step had to complete in order.
In terms of output quality, it was the best version I have ever run. Nothing got implemented without agreed requirements. Nothing got closed without demonstrated results. The shortcut behaviours were meaningfully suppressed. If the only goal were quality at any cost, I would have stopped there.
I could not stop there, because it was unliveable. For a simple change, the requirements phase took longer than the whole task would have taken with no process at all. Meanwhile Claude would disappear off to run AC audits, test-coverage audits, solution-design documents - producing diligent-looking output I could not easily steer or interrupt. I would look up half an hour later to find a cathedral of audit markdown and no code. The checklist treated every task as equally large and equally risky.
The process had become the work rather than supporting it.
So I loosened it. Reduced to three gates, the checklist decomposed into invocable skills the human triggers when needed, in whatever order the situation demands. A simple change now costs ten minutes of ceremony instead of two hours. The gate keywords remain the only hard stops.
The personal SDLC is never done
What I have built is sized for one human and one collaborator, but the failure modes are not — they recur, more dangerously, anywhere a team uses these tools without comparable scaffolding. All of this was arrived at by accumulation rather than design. Every rule in it was written after a specific failure. Most of them are obvious in retrospect. The interesting thing is not the rules but the record - the accumulating sense of this is what went wrong, this is what we did about it, and here is the shape of the problem that keeps reasserting itself under new names. It is a living document in the boring and literal sense. It is being edited as I write this.
The rebellious teenager
What has changed across successive model versions is not that the older rules stopped working. It is the sophistication of the workarounds - and, more uncomfortably, the willingness of the model to disagree with me about whether a rule should apply at all.
I tell Claude an approach is wrong. Rather than accepting the instruction, it defends its previous work. It explains, politely, why its approach is actually correct. The conversation becomes a debate rather than a direction. I report a bug and paste the output, and Claude questions whether I ran the right command, whether my environment is set up correctly, whether I am reading the output properly. I articulate what I want and the model, apparently concluding its version is better, delivers something different with the alterations folded in quietly. The rationale, if I ask, is usually reasonable sounding. That is not the point. The point is that I made a product decision and the model overrode it.I have had cases where the model would not believe my test result until I showed it a screenshot.
I do not have a confident answer for this. A rule like never do X is checkable. The human's call is the call is not - it depends on the model noticing the moment it is about to override a human decision and choosing not to. A more capable model is better at reasoning about that moment. It is also better at convincing itself, in that moment, that its judgement should prevail.
Which is to say: the process works, mostly, and I will keep sharpening it. But I am not fooling myself about the shape of what I am doing. You do not win the argument with a teenager. You just keep having it. And next month there will be a new failure I have not yet named.
TLS (Transport Layer Security) is the protocol that encrypts traffic between your computer and a web server. When verification is on, your software checks that the server's identity certificate is genuine and issued by a recognized authority. Switching verification off means the connection is still encrypted, but your software will trust anyone claiming to be the server - which defeats most of the point.
Git is a version control system that allows developers to track changes to their code, collaborate with others. Often this is part of a platform like GitHub, which adds issue tracking and other project management tools on top of Git's version control features.
The most egregious example of this — observed on multiple occasions — was defining an end-to-end test to be written, and it would interpret that as "call this internal library function, get a byte array back, and report green if the byte array is not empty". The test exercised internal pieces of code in isolation; never testing the entry point for the use.
Trinity College Dublin, a very old and prestigious university in Ireland. It is also my alma mater.
LLM: Large Language Model. The category that includes Claude Code, responsible for most AI news of recent years and now firmly in the public eye.
Test-Driven Development. Write the tests first, then write code to make them pass.