CLAUDE.md is the file Claude Code reads at the start of every session in your repository. Most advice says to put your conventions in it. This part tested that on a small .NET solution, with the same task run eight times, with and without the file. The conventions the code already shows changed nothing. Four rules the code cannot show changed the result every time. That difference is what to write and what to leave out.
- Save CLAUDE.md.txt as
CLAUDE.mdin the repository root and replace its contents with your own commands, layout and rules - Keep the commands exact and runnable from the root:
dotnet build, thendotnet test - Add the rules nobody could learn from the code: files not to edit, what to update with a change, how to report
- Run
/contextin a session. You should see your file under Memory files with its token count - Give it a small task and check
git statusagainst the rules - Delete any line it already follows without being told
The file
The sample is AppointmentDesk, a booking API you can download and run: a minimal API on .NET 10, EF Core with SQLite, xUnit tests. Its CLAUDE.md is 32 lines and 244 words. It has five short sections: commands, layout, conventions, a do-not-touch list and working agreements. Two of them:
## Commands
- Build and test from the repository root, where `AppointmentDesk.slnx` is: `dotnet build`, then `dotnet test`. Do not cd into a project folder first.
- New migration: `dotnet ef migrations add <Name> --project src/AppointmentDesk.Api`
## Working agreements
- Do not edit `README.md`; it is regenerated at release.
- Every change to an endpoint adds one line under `## Unreleased` in `CHANGELOG.md`.
- Before you finish, run `dotnet build` and then `dotnet test` from the repository root as two separate commands. Never chain them in one command line.
- Your final message is exactly three lines, starting `Changed:`, `Tested:` and `Review:`.
The conventions section says that time comes from an injected TimeProvider
and never from DateTime.UtcNow, that tests are named
Method_State_Expected, and that errors are returned as Problem results. The
memory documentation asks for exactly
this kind of content, short and specific, and suggests staying under 200 lines.
Experiment 1: conventions the code already shows
The task was Add GET /appointments/today that returns today's appointments for the
clinic, with tests. It needs "today", so it tests the time rule, and it needs
tests, so it tests the naming rule. Four runs on fresh copies: two without any CLAUDE.md,
two with the first version of the file, which had the conventions but not the working
agreements.
All four wrote the same line in the service:
ListForDayAsync(DateOnly.FromDateTime(timeProvider.GetUtcNow().UtcDateTime), cancellationToken);
Nobody used DateTime.UtcNow. All thirteen new tests across the four runs
followed the naming convention. Nobody touched a migration. With the file or without it,
the code was the same, because the model read the existing service and tests and copied
what it found. A convention your code base demonstrates consistently does not need a line
in CLAUDE.md. Anthropic's best-practices page gives the same advice from the other side:
if Claude already does something correctly without the instruction, delete the line.
The file was not ignored. Asked to quote its build command and time rule, a session returned both word for word. It simply had nothing to correct.
Experiment 2: rules the code cannot show
One thing stood out in the first four runs: every one of them edited
README.md to document the new endpoint. Reasonable, and wrong for a team
whose README is generated. Nothing in the code says so. So the second experiment added
the four working agreements above, put an empty changelog in every copy, and ran the same
task four more times.
| What happened | Without CLAUDE.md | With the working agreements |
|---|---|---|
| README.md edited | 2 of 2 | 0 of 2 |
| Build, then test, as two separate commands | 0 of 2 (only dotnet test) | 2 of 2 |
| Final answer in the three labelled lines | 0 of 2 (14 and 17 lines) | 2 of 2 |
| Line added under Unreleased in CHANGELOG.md | 2 of 2 | 2 of 2 |
| Cost of the run (notional) | $0.33 and $0.34 | $0.33 and $0.32 |
Three of the four rules changed behaviour in both runs. The fourth made no difference:
an empty ## Unreleased heading in an existing changelog was enough of a hint,
and the runs without the rule filled it in anyway. A run without the file ended like
this: "I also added the endpoint to the table in README.md and to the
Unreleased section of CHANGELOG.md." A run with it ended like this:
Changed: Added `GET /appointments/today`, which lists appointments on the current UTC day (the day comes from `TimeProvider`) by reusing `ListForDayAsync` (`AppointmentService.cs`, `AppointmentEndpoints.cs`), and added a line to `CHANGELOG.md` under `## Unreleased`.
Tested: Added 2 service tests (today only versus tomorrow, and the clock moving past midnight) and 1 endpoint test; `dotnet build` had 0 warnings and `dotnet test` passed all 17 tests.
Review: "Today" means the UTC day because the clinic's hours are set in UTC, and like `GET /appointments?date=` it also returns cancelled appointments; tell me if you want those filtered out.
Two runs per column is an observation, not a rate. The direction was the same in every run.
What to leave out
The same evidence says what not to write. Leave out anything Claude can learn by reading the code: a description of every file, the name of the test framework, the fact that the project uses dependency injection. Leave out advice that is true of all software. Leave out long procedures, which belong in a skill and are the subject of Part 4. What remains is short: the commands, the handful of conventions a newcomer would get wrong, the files that are off limits, and the agreements your team made in conversations the repository never recorded. If you are unsure about a line, run the task without it once and see whether anything changes.
What the file costs, and how to see it loaded
The file is sent with every session. /context shows what was loaded and how
large it is; it also works from a script, as claude -p "/context", with no
model call at all:
| Memory files | 803 | 0.1% |
803 tokens for this file, 654 before the working agreements were added. Turns, time and cost showed no pattern between runs with and without it.
Where it lives, and what else gets loaded
The project file goes in the repository root as CLAUDE.md, or in
.claude/CLAUDE.md, and is committed. Personal notes for one project go in
CLAUDE.local.md, which you add to .gitignore. Your own
~/.claude/CLAUDE.md is loaded in every project on your machine. The
documentation says all the files found are joined together, not one overriding another,
so keep them consistent; it does not promise which side wins a contradiction.
Files in the folders above your working folder are loaded too, and that caught these
tests out. A master copy of CLAUDE.md saved one folder up from the test copies was read by
a session that was supposed to have none. A stray CLAUDE.md in your source folder or home
folder reaches every session started below it. If a rule seems to come from nowhere, run
/context and read the list.
What it did not do
CLAUDE.md is context, not enforcement. The documentation says so directly and points to
hooks for anything that must hold. These runs showed the gap twice. In the first
experiment one run with the file wrote dotnet build; if ($?) { dotnet test },
which was refused as a chained command although both commands were allowed, and the
session ended with "I haven't built or run anything yet." The rule about two separate
commands exists because of that run. In the second experiment a run followed every rule,
passed build and tests, and still left three missing spaces that
dotnet format --verify-no-changes rejects. No line in the file covered
formatting, and a line would not have guaranteed it.
Part 3 does.
This test also cannot say what the file does for a code base whose conventions are inconsistent or absent. There, the conventions section may earn its tokens. Test it the same way: one task, with and without, and look at the diff.
Frequently asked
- What should I put in CLAUDE.md for a .NET project?
- The exact build and test commands, a two-line layout, conventions that differ from defaults, files that must not be touched, and rules nobody could learn from the code, such as which files are generated and how to report a change. Keep it short: the file here is 32 lines and about 800 tokens.
- Does CLAUDE.md actually change what Claude Code does?
- For conventions the code already shows, no: in eight runs the code was the same with and without the file. For rules the code cannot show, yes: without the file every run edited README.md and none ran build and test separately; with it, no run edited README.md and both did.
- How do I check that Claude Code loaded my CLAUDE.md?
- Run /context in a session and look at the Memory files list, which names each loaded file with its token count. It also works non-interactively as claude -p "/context", which makes no model call.
Next: Part 2, permissions: which
dotnet commands run without a prompt, and what never runs at all.