Writing about AI in development comes in two flavours: "it will write everything for you" and "it generates garbage". Both are useless, because they talk about the model, while the outcome is decided by what you build around it.
Below is my setup: project memory, repeatable procedures and access to the production server. Plus the part people usually skip - how to verify that the generated thing actually works.
Project memory beats prompting
The core problem is not that the model writes bad code. It is that it does not know your project: why this duplication is deliberate, why that library is vendored instead of installed, why a field is computed on write rather than on render.
The fix is a project description file in the repository root that loads into every session. The key point: it holds the non-obvious parts, not the structure. Structure is visible in the code. This is not:
Deploy logic is deliberately duplicated in two files:
one for the admin panel, one for the webhook.
The steps must match. Change one, change the other.
A note like that saves hours rather than minutes: without it an edit lands in one of the two files and half your deploy quietly keeps the old behaviour. The model would never guess, but it will read and comply.
What belongs there: deliberate duplication, workarounds and their reasons, style agreements, places where the obvious solution is wrong. What does not: folder structure, dependency lists, a retelling of the code.
An honest failure: documentation rots too
The other day I was writing about how language versions work and opened that very file for the wording. It described auto-detecting the language from the browser header and storing the locale in the session.
None of that has existed in the code for a long time: the controller simply reads a route parameter. The note survived a refactor and kept describing behaviour that is not there. Had I trusted the file, the article would have documented a mechanism that does not exist.
The lesson is simple and unwelcome: a project description file is as much a source of errors as a stale code comment. Before leaning on it for anything that matters, verify the claim against the code. In my case that was one grep across the application.
Skills: a procedure instead of a prompt
The second thing that actually moves the needle is extracting repeated operations into named procedures. Not "let me explain how I do commits every time", but one file of rules triggered by a phrase.
The shape is simple: a line of triggers, then the rules.
Run when: commit, push, send to git.
Rules:
- Message is one short phrase, 2-6 words, lowercase.
- No Conventional Commits: no feat:, fix:, chore:.
- Do not include documentation files in the commit.
After that "push it" is enough. The difference from an ordinary prompt is that the rules do not blur by the end of a long session and stay identical across projects.
I have around twenty of these: planning before a task, reviewing changes, debugging on the production server, reports, working from design files. One of them proposes new ones - when the same operation comes up a third time, it is time to write it down.
The rule that makes skills shareable
There is a non-obvious constraint I did not adopt right away: a procedure must contain nothing that identifies a project, a company or a client. No names, no domains, no paths like /var/www/client, no keys, no tracker issue IDs.
The reason is practical. A good procedure will eventually be useful to a colleague, and handing it over should not be scary. If a client domain and a path to their server are baked in, you cannot share it without scrubbing it first.
Everything machine-specific moves into a separate local file outside the repository: keys, addresses, exceptions. The procedure reads it itself, and if the file is missing it falls back to defaults or asks. Examples use example.com, /var/www/project, user@host.
Access to the production server
The most debatable and the most useful part: SSH access to production. Not for edits - so that nobody has to guess.
The usual dialogue without access: "something is broken on the site" - "show me the logs" - "which logs, where do I get them". With access it is one request: read the tail of the log, check migration status, confirm that a scheduled task actually fires.
There is a permissions trap here that everyone hits. The user you SSH in as is usually not the user the application runs as. Reading works from either:
ssh myserver "cd /var/www/project && php artisan migrate:status"
Anything that writes files or logs must run as the owner, or the command fails on permissions or, worse, creates a file the web server can no longer overwrite:
ssh myserver "sudo -u app_user php artisan cache:clear"
A real example: a one-off script run as the wrong user left a cache file owned by someone else. The application started failing on writes, and the cause was not in the code at all.
Being able to inspect the scheduler is worth its own mention. My crontab turned out to hold two schedule-run entries for the same application - one from the hosting panel, one added outside it. Both fired in the same second, so every task ran twice. That was not found in the code but in the system journal: two processes with different IDs in the same second.
Verification instead of trust
This is what separates work from a gamble. Generated code is not done until it is verified - and the verification should not be a human squinting at a screen, it should be something reproducible.
What works for me:
- Tests for behaviour, not for coverage. In the past week they caught three real bugs: a validation call did not return an optional key and broke saving; a newly attached handler made the suite call real external services, pushing a run from one second to eighteen; nobody would have caught either before production.
- Screenshots through a headless browser. Layout cannot be verified by reading code. Opening the page at three widths and looking can.
- Measurements instead of impressions. "The blocks look misaligned" turns into measuring coordinates: the left edge of the logo, the heading and the footer must match. They match, question closed.
One full example. The complaint was "horizontal scroll on mobile". A script walked the page at three widths, compared document width against window width and named the culprit: a decorative element pushed past the section edge. It also turned out the familiar overflow-x: hidden on body does not help there - per spec the value propagates to the viewport while the element itself stays visible. Without the measurement that would have been half an hour of guessing.
Where the line is
What I do not delegate: architectural decisions, the choice between fast and correct, anything touching money or personal data. Not because the model cannot write the code, but because the cost of a mistake is asymmetric and I am the one answering for it.
And the main point. The setup does not make the result good by itself - it makes it verifiable. The difference between "generated and hoping" and "generated, ran, looked, measured" is about the same as between code without tests and code with them. The model here is just one more source of changes that has to be checked.