I thought my English wasn't good enough. It was Claude's defaults.
2026-05-18
2026-05-18
Most AI coding agents read a rules file before they touch your code. Claude Code reads CLAUDE.md and the files under .claude/rules. Cursor and Copilot have their own versions. The format differs, but the idea is the same: a place where you write down how code in this project should look, and the agent reads it on every session.
That place fills quickly. Once you have it, almost any coding guideline looks like it belongs there. Naming, commit format, how to structure a NestJS service, security advice, testing habits. It all sounds reasonable as a rule.
From what I have seen, this is where rule files go wrong. They become a second copy of every guideline the team has ever written, and most of those lines do nothing. This post is about a simple test for which lines belong in a rule and which have a better home.
I treat an agent rule as the last place I put guidance, not the first. Before a line becomes a rule, it has to fail every other home.
The test I use has two parts. Keep a line as a rule only if both are true:
If a tool can enforce it, the tool is a better home. If removing it changes nothing, it should not be there at all. What survives both questions is small, and that small set is what a rule is for.
This is the biggest group. A lot of what ends up in rule files is something a linter or a check can verify.
Commit message format is the clearest example. Writing "use the imperative, start with a capital letter" as a rule asks the agent to remember a convention. commitlint just enforces it:
{
"extends": ["@commitlint/config-conventional"]
}
Now the format is not advice, it is a check that fails when the message is wrong. The same goes for naming. A rule that says "boolean variables start with is, has, or should" can be an eslint rule instead:
{
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "variable",
"types": ["boolean"],
"format": ["PascalCase"],
"prefix": ["is", "has", "should"]
}
]
}
When a tool enforces a guideline, something useful happens. The tool becomes the only definition. There is no prose copy that can disagree with it later. A failing check is also the most reliable documentation I know, because you cannot ignore it. It stops the merge.
This fits how I already like to work. I rely on tests and CI to catch mistakes rather than on documents people are supposed to remember. An agent rule that repeats a lint rule is just a less reliable copy of something the linter already does.
The second group is the one that feels the most useful and is usually the most useless. These are general engineering principles. "Simpler designs are more secure." "Prefer composition." "Write tests." "Use clear names."
They are all true. That is exactly why they do not belong in a rule. A current model has read this advice many thousands of times. It already writes simpler code and clearer names by default. Repeating the principle does not change the output, because the model was going to do it anyway.
These lines are also not free. Every line in a rule file is read on every session. It costs tokens, and it competes for the model's attention with the few lines that actually matter. A rule file that is mostly general advice makes its own useful parts harder to notice.
There is also no place to stop once you start. If "simpler is more secure" earns a place, so does every other principle anyone has ever agreed with. You can keep adding them forever, and each one looks justified on its own. When a category has no natural boundary, that is usually a sign the category is wrong.
Some guidelines are real and specific, but they only apply to one area. How to structure a NestJS service. How to write a React component in the admin app. These are worth writing down. They just do not need to live in a global rule.
A README next to that code is a better home. The agent reads it when it works in that folder, and so does the next developer who opens it. One file, both readers, sitting where the work happens. A global rule puts that same guidance far away from the code it describes, which is how it starts to disagree with reality over time. I wrote more about keeping a single document for both humans and agents in an earlier post.
After tooling, deletion, and module docs, what remains is small. It is the guidance that is specific to this project, that a tool cannot catch cheaply, and that the model would get wrong without being told.
An example from the kind of work I do: in one project the convention is to pass a whole entity into a service method instead of passing just its id. A linter will not catch that, the model does not do it by default, and getting it wrong causes real confusion later. That is a genuine rule. It changes what the agent does, and nothing else enforces it.
Notice that this rule is not verifiable. No check confirms it, and that is fine. The test was never "can a tool verify it." The test was "does removing it change what the agent does here." Some judgment conventions pass that test even though no tool can enforce them. Those are the rules worth keeping, and there are not many of them.
Before you write an agent rule, ask two questions. Can a tool enforce this? Then it belongs in the tool. Would removing it change nothing? Then delete it. What is left, the project-specific guidance that a tool cannot catch and the model would otherwise get wrong, is the whole job of a rule file.
In the projects I work on, that final list is short. A rule file that stays short is doing its job. One that keeps growing is usually collecting things that belong somewhere else.
I got beta access to FastAPI Cloud, the new hosting provider from the FastAPI team. This is a short report of what I deployed and how it went. It is not a full review.
The project I used is a hobby test project. Nothing critical. If it is unavailable I do not care. That made it a good fit for trying a beta hosting provider.
For context: my usual way to deploy a Python app is Docker on a VPS or a dedicated server. I have used Digital Ocean App Platform in the past. The developer experience was great, but I stopped using it because the price was not worth it for my use case. I have not tried Render or other managed Python hosting providers. So this is not a full comparison against managed hosting in general. It is just my experience trying FastAPI Cloud.
I part of the django app behind abelcastro.dev is a small API fetching and serving sports (actually just football) data. To try FastAPI Cloud, I extracted that part into a standalone FastAPI app. The code is here: github.com/abel-castro/sports-api and the live version of it can be found here sports-dashboard.abelcastro.dev/.
The migration itself was easy with AI help. The original code was simple, and the move to FastAPI plus SQLModel/SQLAlchemy was straightforward.
In his blog post about starting FastAPI Cloud, Sebastian Ramirez names Vercel, Netlify, and Cloudflare in the frontend world as the kind of developer experience he wanted to bring to Python deployment. Vercel was also the comparison I had in mind when I tried it.
Login and creating a first app were straightforward. It is not as seamless as Vercel today, but for a beta the starting experience is impressive.
One thing I liked is the direct integration with database providers. I connected the app to a Neon Postgres database from inside the FastAPI Cloud setup, and that worked well.
After the app was live, I noticed intermittent database connection errors. This was not a FastAPI Cloud problem. It is how Neon works. Neon suspends idle compute, so connections in the SQLAlchemy pool can become stale and fail on the next request.
The fix was to pass two extra options to create_engine:
engine = create_engine(
settings.database_url,
pool_pre_ping=True,
pool_recycle=1800,
)
pool_pre_ping=True makes SQLAlchemy check that a connection is alive before using it. pool_recycle=1800 tells the pool to discard connections older than 30 minutes. After this change the errors stopped.
The commit is here if you want to see it: Fix db connection errors.
The sports API needs a periodic job to refresh data. As far as I can tell, FastAPI Cloud does not support scheduled tasks at the moment.
For this hobby project I used a workaround. A scheduled GitHub Actions workflow calls a protected endpoint on the API. The endpoint checks a token passed in a header, and the token is stored as a GitHub secret. The workflow file and the endpoint are in the same repo linked above.
I want to be clear: this is a hobby pattern, not how I would do it for a real project. GitHub schedules can be delayed under load, there are no built-in retries, long jobs do not fit inside an HTTP request, and observability is split between two platforms. For a real project I would use a task queue with a worker (Celery, RQ, ARQ, Dramatiq) or a managed scheduler.
So my open question for the FastAPI Cloud team is: are proper scheduled tasks on the roadmap?
Right now you get an auto-assigned subdomain on FastAPI Cloud and there is no custom domain option yet. For a beta this is fine, and I would not complain about it. It is just something to know.
For a beta, FastAPI Cloud is impressive. The login and first deploy are easy, and the Neon integration works well (with the small SQLAlchemy tweak above). The one real gap I found was scheduled tasks.
I am looking forward to seeing how the product evolves, and what pricing they introduce. Hosting costs money, and at some point the team has to make money from it. In any case, in my opinion they are making a great start.
Last week I asked Claude to summarize a deploy log. It told me a job had "stalled". I spent a few minutes checking the system for actual problems before realizing the word was figurative. The job had paused, not broken. Stalled is what cars do.
This is the pattern. Every few prompts, Claude reaches for a phrase that sounds natural to a fluent reader and lands wrong on me. Drain, in-flight, beat us to it, head down, spin up. Each one is a small interruption. I read it twice, I check if it means what I think, sometimes I look it up. Multiply by 50 prompts a day.
For a long time I treated this as my problem. The author of the message is a tool I am paying for, and yet I was the one stumbling. It felt like a gap in my English, something I should fix by reading more.
It is not that. Claude's default writing is polished English: fluent, idiomatic, the way "good writing" is supposed to sound. For a non-native reader, that polish is the problem.
Once you frame it that way, the fix is a configuration issue, not a vocabulary problem.
Correcting Claude inside the conversation. It works for two or three turns and then the next idiom shows up. The corrections do not carry across sessions, and they do not carry between projects.
Maintaining a list of forbidden words. I tried this. The list grew. Drain. In-flight. Stalled. Beat us to it. Spin up. Tear down. English has thousands of these phrases. I cannot enumerate them, and I should not have to.
Both approaches fail for the same reason. They treat each idiom as a separate item to memorize, when the underlying issue is a writing principle the model is not applying.
The principle is one question, applied to every sentence the model writes:
Would this phrase still mean what I intended if translated word for word into another language?
If yes, great. If no, it is an idiom. You can usually replace it with clearer words.
A few examples:
The right column reads slightly less fluent and is much easier to understand. For a non-native reader this is not a downgrade, it is the entire point.
Once you have the principle, you do not need the list. The principle generates the list on demand.
Claude has a settings field called "Instructions for Claude", under Settings, General, Profile. Text in that box applies to every chat across the account. It is the right place for a writing rule that should always apply.
Paste this:
Write all prose, code comments, log messages, and chat responses
with literal vocabulary. The reader is a non-native English speaker.
Test before sending: would each phrase still mean what I intended
if translated word for word into another language? If not, rewrite it.
Avoid:
- Phrasal verbs whose meaning differs from the literal words
(spin up, take off, wind down, tear down, look up).
- Sports, war, cooking, or sailing metaphors (in the trenches,
simmer, on the hunt, beat us to it).
- Body-part metaphors (head down to, keep an eye on, lend a hand).
- Verbs that physically describe something else (stalled, drained,
burned down). Use neutral verbs that describe what is actually
happening (did not finish, stopped before completing).
- Niche jargon when a plain phrase is the same length (terminal
status, in flight). Prefer the plain phrase.
Keep technical terms with literal meanings (SIGTERM, graceful
shutdown, cache miss). The rule is about figurative language,
not standard terminology.
Keep sentences short and direct. Prefer the phrasing a 10-year-old
or a non-native English speaker would understand. Boring prose is
correct prose.
Save. From the next chat onward, Claude applies the rule by default.
The settings field above applies to claude.ai on the web, the desktop app, and the mobile app. These are different clients to the same backend, so one setting covers all three.
Claude Code (the CLI, and the VS Code extension that uses it) is a separate product with its own configuration. The web setting does not apply there. To get the same rule, paste the same text into a user-level file:
~/.claude/CLAUDE.md
Claude Code reads that file at the start of every session, in every project. Project-level CLAUDE.md files at the repo root also work, but for a writing rule that should always apply, the user-level file is the right place.
You now have two copies of the same text, one in the web settings and one in ~/.claude/CLAUDE.md. If you change one, update the other. There is no automatic sync between them.
The same principle works for anything I write that other developers might read. Documentation, commit messages, code review comments, Slack threads. An idiom in a PR description costs everyone two seconds and costs a non-native reader a search.
The literal-translation test is a habit I can apply with or without an LLM. The Claude instruction just makes the habit show up in every chat I have, without me having to remember.