Ramblings about LLMs from a guy on the frontlines banging his head on the wall

Over the past 6 months, I’ve had a dramatic change of heart regarding the use of generative AI for software development. Partially this has been caused by newer and better models such as Opus 4.8, but mostly because I made the leap from using a chat window within VSCode to using Claude Code on the terminal. I started by occasionally asking questions on the web interface of ChatGPT and being dissapointed when it hallucinated about the relatively niche library I was using, and from the introduction of GPT3 to the beginning of 2026, this was my experience of LLMs and it was sorely unimpressive.

There were a few ‘Aha’ moments which began to change my mind. Programmers tend to be very set in their ways regarding their tools (see the decades-long vim vs emacs debate) and I’m no different. Adding a chat window to VSCode felt sacreligious, and installing Claude Code felt sacreligious, and also felt premature given the pace at which AI tooling was moving.

Agentic harnesses

But pasting code into a web chat and then slowly watching the code diverge from your local copy as the LLM suggested changes was never practical long-term. Copy-pasting code has always been a source of pain, and a practise i forbade myself from a long time ago. Using an agentic harness such as Claude Code, codex, or the open-source ones such as OpenCode or Pi allow the LLM to stay up-to-date with the code, make changes directly, run tests, and frees you from the drudgery of the bash prompt.

Context windows

At the point I began using an agentic harness, I noticed that when taking on a task, things would move incredibly quickly and frictionlessly, and then over time the quality of the work being done would drop, the machine made more mistakes, and it began to feel like wading through treacle. As it turns out, I was filling up my context window with these long-running conversations where I’d make a bunch of unrelated fixes and features.

In hindsight it should have been obvious, when you’re talking about feature E and the LLM keeps bringing up bugfix B which was already pushed and merged a week ago, that should have tipped me off. What actually taught me this was when I decided to personally pay for a Claude subscription, and seeing my usage disappear almost instantly caused me to do some research and realised that I was filling up my context windows. The fix is almost trivial, as soon as you’re done with a task or the LLM starts spinning, /clearand start with a fresh session.

Testing

There are two camps that have formed around LLMs, the people who believe LLMs are improving at such a rate that they soon will never make mistakes, and the camp that believes LLMs produce nothing but bugs. My opinion on this is simple - why are you waiting to find out which is true? Don’t allow the LLM the chance to produce buggy code. Test-driven development can be summarised as follows:

  1. define the specification of a feature
  2. create tests that prove a feature will work according to the spec.
  3. write code until all the tests pass.

If you have existing tests, you can instruct the LLM to not consider the work done until all the tests pass alongside the feature being complete. You can even register a ‘stop hook’, which is a small code shim which must pass before the LLM can stop thinking. I don’t recommend these because it has to run tests after you ask it what the weather is doing this weekend, but you can get a similiar result by just including “ensure all tests pass” in your initial prompt request.

If you don’t have existing tests, you can request the LLM to write those tests from the specification. If you don’t have a specification, what are you doing?r

Tombstones

This is a more specific niggle I’ve noticed and a very effective way to stop it. LLMs are primarily text generation programs, the concept of ‘removing text’ is rather foreign to them, they have to be specifically told to do so. This runs in conflict with maybe the most succinct axiom of programming - “code which doesn’t exist can’t have bugs”, or put another way “the best code is code you didn’t write”, or “YAGNI (You ain’t gonna need it), or any one of a myriad other ways to state this. The goal of a programmer should be to wake up every day and think about which jenga piece they can pull from their codebase without it falling over.

If an LLM discovers a bug in a piece of code such that a = b + c*d; should be a = (b + c) * d;, it’s liable to leave a comment above it such as // This now correctly applies the operator precedence rule such that the multiplication applies to B and C. Such a comment is immediately out of date, as soon as it’s committed it references non-existent code. It’s a grave marking old behaviour which is invisible to anyone reading the codebase, and it conveys no information beyond what the git history has.

It’s possible to edit the Agents.md/Claude.md file on your machine to instruct the LLM against this kind of behaviour. My agents.md contains the following:

- never write tombstone comments - comments that only make sense as a record that a problem *used to* exist eg: \`// use neon here, not scalar C (which is too slow)\`. Once merged, readers only see the current code, so the comment just marks a grave. Phrase any comment about the present code, never "this used to be done differently / was broken".

- never write comments that restate the code (\`|| defined(__aarch64__)\` does not need \`//enable on aarch64\`).

- assume the code is self-documenting; comment only to explain a genuinely non-obvious *why* that the code and surrounding context can't convey.
- default to NO comment on a small self-evident change.

I would advise against copy-pasting this prompt in directly, instead, I would advise you to have a conversation with your LLM where you state your expectations for when comments exist (even just pasting the above text in), and ask it to write the rules into Agents.md in its own words. This has a few benefits:

  1. there may be edge cases in your particular workflow that aren’t caught by my prompt. For example, I had a set of yaml files at work that did need fairly self-explanatory comments as they related to prior history not tracked by git. think “this machine occasionally needs 5 seconds more to boot because of a failing backup job on tuesdays”. next to the boot time being 125 instead of 120 seconds.

  2. The human instinct is to reach for negative cases (“don’t do X”, “don’t say Y”, avoid “Z phrase”) which constrains the output and may cause the LLM to reach for strange phrasing to ‘bend the rules’. By having the LLM rephrase, it will likely use a positive reformulation of the prompt which constrains the output less.

  3. It allows the LLM to find a canonical phrasing which is densely covered by training data, which it may follow more consistently.

Finding your voice

For the longest time, the one thing I was not willing to do was to let an LLM speak on my behalf, by which i mean commit messages, PR descriptions, and Jira tickets.

I think we’ve all experienced seeing a 100 line output directly from chatGPT littered with emojis, bullet point lists, changing tone of voice (2nd person prose), and the occasional hallucination, and I think we’ve all experienced a sort of disgust when confronted with that.

Ultimately, human attention is becoming a scarce commodity, and I find it unbelievable that people are willing to subject their coworkers to reading such long-form LLM rambling.

So I would use an LLM to make changes, then manually do the work of making commits and PRs. This was fine, except for the odd occasion where an LLM-generated commit appeared in my work output. I realised that I essentially have my own training data available, and gave a prompt like this:

gather all commits from <repo> that I authored prior to <month i started using LLMs> and read the commit message title and body, and use this to distil my usual style of commit message. Write instructions for how to make an authentic commit message and add them to Claude.md.

Then do the same with all my PRs on bitbucket.

It did a very good job at discerning that I am a gen-Z typist - I never capitalise i in commits, but I always capitalise other peoples name, and if a sentence ends a paragraph, it doesn’t get a full stop. It also realised I rarely write more than a title, or occasionally a single line in the body. I do the bare minimum of prose, and rely on the reviewer reading the diff. Since then, I actually feel confident to let the LLM write commit messages and open PRs without risking huge sloppy infodumps.