Setting Up Context: Rules Files and Project Config

You open a coding agent in the Kitebase ticket service, a small Python codebase for the made-up project-tracking app used across this site, and ask it to “add a --status filter to search_tickets”. Say it writes the filter, then runs pytest to check its work. That fails with No module named 'kitebase', so it starts “fixing” imports that aren’t broken. On this project the tests only run as python -m pytest. Nobody told it.

Nothing is wrong with the model. That fact isn’t anywhere in the code. The fix is to write such facts down once, in a file the tool reads at the start of every session, plus a few settings that decide what the agent may run without asking you.

What you’ll build: a 20-line CLAUDE.md and a settings file for the Kitebase ticket service, a 223-line bad CLAUDE.md to compare it with, and a small linter that catches the common mistakes in rules files. None of it needs an API key.

What a rules file is

A rules file is a Markdown file of standing instructions for a coding agent: how to run the tests, which conventions to follow, what not to touch. The tool reads it at the start of a session and puts its text into the model’s context window, the fixed amount of text the model can see in one request.

That’s the whole mechanism. The tool doesn’t parse it or check it. In Claude Code, the file’s contents arrive as a user message right after the system prompt (the tool’s own built-in instructions). So it’s context, and like any context, the model can still get it wrong. As article 01 put it, the context decides the quality of what comes out. The rules file is the part of it you write in advance.

Each tool reads its own file name. At the time of writing:

ToolMain rules fileScoped rules
Claude CodeCLAUDE.md (reads AGENTS.md when there’s no CLAUDE.md)nested CLAUDE.md, .claude/rules/*.md
CursorAGENTS.md, or .mdc files in .cursor/rules/nested AGENTS.md, rules scoped to file patterns
WindsurfAGENTS.md, or .md files in .windsurf/rules/nested AGENTS.md, rules scoped to file patterns
Other agentsAGENTS.md, a shared convention across toolsnested AGENTS.md; the nearest one wins

Windsurf’s docs now call the product Devin Desktop and prefer .devin/rules/, with .windsurf/rules/ still read. The rest of this article uses Claude Code for the details; the ideas carry over to every row.

Where Claude Code looks for it

Four places matter day to day. Each has a scope: who it applies to.

  • User: ~/.claude/CLAUDE.md. Your personal preferences for every project on your machine.
  • Project: CLAUDE.md (or .claude/CLAUDE.md) in the repo. Committed, so the whole team gets it.
  • Local: CLAUDE.local.md next to it. Personal notes for this one project. Add it to .gitignore yourself.
  • Nested: a CLAUDE.md in a subfolder, like tests/CLAUDE.md. Loaded only when the agent reads a file in that folder.

At launch, Claude Code loads the CLAUDE.md and CLAUDE.local.md in the folder you started it in and in every folder above it. They’re joined, not overridden: every file goes into the context, broadest first, so the file closest to your working folder is read last. Nested files in subfolders wait until the agent opens something there.

At launch, in this order 1. USER ~/.claude/CLAUDE.md just you, every project 2. PROJECT kitebase-tickets/CLAUDE.md committed, whole team, 20 lines 3. LOCAL (OPTIONAL) kitebase-tickets/CLAUDE.local.md gitignored, just you, this project Later, only when needed 4. NESTED kitebase-tickets/tests/CLAUDE.md loads when the agent reads a file in tests/ WHAT THE MODEL READS Claude Code's own system prompt Your rules files, joined in load order 1 + 2 + 3, sent as a user message You: "add a --status filter to search_tickets" Agent reads tickets.py, cli.py, status.py tests/CLAUDE.md, added when the agent opens tests/test_search.py Joined, not overridden. The file closest to your folder is read last. Context the model reads, not config the tool enforces.
Where each rules file comes from and when it enters the context, for the Kitebase repo.

To see what loaded, run /context in a session and look under Memory files. Check that first when the agent seems to ignore your file. If you started Claude Code in the parent folder of your repo, the repo’s CLAUDE.md counts as nested and waits until the agent reads a file inside it.

/init writes a first draft from the codebase, with the build and test commands it finds. Keep it as a start: it can only write down what’s in the code, and the valuable lines are the ones that aren’t.

CLAUDE.md or AGENTS.md: which one should I write?

If your team uses more than one tool, write AGENTS.md. Cursor, Windsurf and Claude Code all read it.

Claude Code reads AGENTS.md only when there’s no CLAUDE.md in the folder or above it (from version 2.1.277). If you also want Claude-specific lines, keep both: a CLAUDE.md whose first line is @AGENTS.md pulls the shared file in, and the Claude-only rules go below it. An @path in a CLAUDE.md is an import: the tool pastes that file in at launch.

What goes in: facts the code can’t tell the agent

The test for every line: would a capable engineer, new to this repo, get it wrong without being told? If they’d work it out from the code, leave it out. If they’d only learn it by breaking something, write it down.

Here’s the whole rules file for the Kitebase ticket service:

# Kitebase ticket service

CLI and library for searching Kitebase tickets. Python 3.10+.

## Commands

- Run the tests: `python -m pytest -q` from this folder. Plain `pytest` fails with `No module named 'kitebase'`.
- Try the CLI: `python -m kitebase search "login" --assignee maya`

## Conventions

- Search filters are keyword-only arguments on `search_tickets`, defaulting to `None` for "don't filter". Copy how `assignee` works.
- A new filter needs three things: the argument, a flag in `kitebase/cli.py`, and a test in `tests/test_search.py`.
- Statuses are stored snake_case (`in_progress`). Pass anything a person typed through `normalize_status()` in `kitebase/status.py` before comparing.

## Gotchas

- Standard library only. No new dependencies: this runs on customers' locked-down servers where we can't pip install.
- Archived tickets never appear in search results, whatever the filters. Support's exports rely on it.
- `data/tickets.json` is demo data for the CLI. Tests build their own tickets and must not read it.

Twenty lines, about 258 tokens (a token is the unit models read and bill in, roughly four characters of English). Each line blocks a specific way the --status task can go wrong:

The test command comes first. An agent that can run your tests can check its own work. Here the obvious guess is wrong. This is what a plain pytest prints:

$ pytest -q
ImportError while loading conftest '.../kitebase-tickets/tests/conftest.py'.
tests/conftest.py:3: in <module>
    from kitebase.tickets import Ticket
E   ModuleNotFoundError: No module named 'kitebase'

python -m pytest -q prints 9 passed, because python -m puts the current folder on Python’s import path and bare pytest doesn’t. The rule names the error too, so the agent recognises it instead of “fixing” it.

Conventions say which existing code to copy. “Copy how assignee works” points at the right pattern in one line. The status rule stops a subtle bug: people type “In progress”, the data says in_progress, and a raw comparison finds nothing:

>>> [t.id for t in load_tickets() if t.status == "In progress"]
[]
>>> [t.id for t in load_tickets() if t.status == normalize_status("In progress")]
['KB-102', 'KB-105']

An agent that reads status.py may find normalize_status() itself. The rule makes sure it looks before writing its own.

Gotchas carry the why. “No new dependencies” alone is a rule an agent might weigh against a good reason to add click. “Customers’ servers can’t pip install” is a fact it can’t argue with, and no file in the repo says it.

No rules file: what can go wrong With the 20-line CLAUDE.md 1. RUN THE TESTS $ pytest -q No module named 'kitebase' 1. RUN THE TESTS $ python -m pytest -q 9 passed 2. MATCH "IN PROGRESS" t.status == "In progress" 0 tickets: it's stored as in_progress 2. MATCH "IN PROGRESS" normalize_status("In progress") in_progress: KB-102 and KB-105 3. ADD THE CLI FLAG $ pip install click fails on customers' locked-down servers 3. ADD THE CLI FLAG --status, next to --assignee argparse, standard library only Each difference is one line of the rules file. Same model, same prompt, less context.
Same model, same prompt. The right column had 258 more tokens of context.

The left column isn’t a transcript of one run; a good agent might avoid some of it. It’s the set of mistakes this repo makes easy, each costing a wasted turn or a review comment. The rules file makes them unlikely.

What to leave out

The companion’s examples/bad-CLAUDE.md is the kind of file that grows when nobody prunes it: 223 lines, about 1,601 tokens. Parts of it:

You are an expert senior Python engineer with 20 years of experience. You write
clean, maintainable, production-quality code. You always follow best practices.

    KITEBASE_DB_URL=postgres://admin:hunter2@staging-db.kitebase.example:5432/tickets

## Project structure
kitebase-tickets/
├── CLAUDE.md
├── data/
...
## Code reference
Here is the current code so you don't have to look it up.
  • Pasted source code. The “code reference” copies four files, about 100 lines. The agent can read them itself, and the copy goes stale the first time someone edits the real one. Then the model sees two versions of search_tickets and can’t tell which is current.
  • A file tree and a dependency list. The agent can list files. Claude Code’s own /doctor check suggests cutting exactly these, along with architecture overviews.
  • Vague rules and a persona. “Write clean code”, “never make mistakes”, “you are an expert senior engineer”. Nobody can check them, so they change nothing. Claude Code’s docs give the fix: rules concrete enough to verify, like “run npm test before committing” instead of “test your changes”.
  • Secrets. A database URL with a password, and an API token. The file is committed, and its whole content goes to the model provider every session. Keep secrets in a gitignored .env and name the variable instead: “the staging database URL is in KITEBASE_DB_URL”.
  • No test command. “Make sure the code is well tested” isn’t a command.

Keep it short

Everything in a rules file loads in every session, whether the task needs it or not. Claude Code’s docs target under 200 lines per CLAUDE.md: longer files use more context and reduce adherence, meaning the agent follows the rules less reliably. It warns at startup when a file is too long. Cursor’s docs, at the time of writing, say to keep each rule under 500 lines. Windsurf’s docs cap each workspace rule file at 12,000 characters.

The cost isn’t mostly money; 1,601 tokens is small. It’s that three rules which matter sit among forty that don’t, and nothing tells the model which are which. When a file grows, move rules closer to where they apply:

  • Nested files for rules about one folder, like tests/CLAUDE.md.
  • Path-scoped rules for rules about one kind of file. A Markdown file in .claude/rules/ with paths: frontmatter (a YAML block between --- lines at the top) loads only when the agent reads a matching file:
---
paths:
  - "tests/**/*.py"
---
Every new filter gets one test in test_search.py and one CLI test in test_cli.py.

An @path import doesn’t help: imported files load at launch too. One trick is free: block-level HTML comments (<!-- like this -->) in a CLAUDE.md are stripped before the text reaches the model, so notes for human maintainers cost no tokens.

The rules file for this website is a real example. It’s 310 lines, and the companion linter flags it for length and a 42-line file tree. What earns its place is what no file in the repo could say: features that were tried and removed, each with its reason, so no session adds them back.

Settings: what the agent may run

A rules file is advice. “Don’t read .env” is a sentence the model will usually follow. Claude Code’s docs say it plainly: CLAUDE.md shapes what the agent tries to do, but isn’t an enforcement layer.

Settings are. They’re JSON files the tool enforces, whatever the model decides, with the same scopes as the rules files:

  • ~/.claude/settings.json: you, every project.
  • .claude/settings.json: the project, committed.
  • .claude/settings.local.json: you, this project. When you answer a permission prompt with “Yes, and don’t ask again”, Claude Code saves the rule here and keeps the file out of git for you.

Lists in several files are combined, not replaced. Here’s the Kitebase project’s .claude/settings.json:

{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "permissions": {
    "allow": ["Bash(python -m pytest *)", "Bash(python -m kitebase *)"],
    "ask": ["Bash(pip install *)"],
    "deny": ["Read(./.env)", "Read(./.env.*)"]
  }
}

Each entry is a permission rule: a tool name and a pattern for what it may do. Bash(python -m pytest *) matches any command starting with python -m pytest, and because the * has a space before it, it matches the bare python -m pytest too. The $schema line gives your editor autocomplete for the file.

The agent wants to run Claude Code checks the rules Result .CLAUDE/SETTINGS.JSON Read .env 1. deny Read(./.env) Blocked pip install click 2. ask Bash(pip install *) Asks you first python -m pytest -q 3. allow Bash(python -m pytest *) Runs, no prompt git push no rule matches Default: asks you Checked in order: deny, then ask, then allow. Enforced by the tool, whatever the model decides.
Deny is checked first, then ask, then allow. A command no rule matches gets the default: a prompt.

Deny is checked first, then ask, then allow, and the first match decides, so a deny in any file beats an allow in any other. In the default permission mode, a shell command no rule matches gets a prompt, apart from a built-in set of read-only ones like ls.

The allow list is where the day-to-day value is. With the test command allowed, the agent runs python -m pytest -q after every change without waiting for you. Allow what you’d approve every time (tests, linters, your CLI), and nothing that deploys, pushes or deletes.

Three gotchas from Claude Code’s permissions docs:

  • Committed allow rules wait for trust. Claude Code asks you to trust a folder the first time you open it, and the project’s allow rules apply only after that, so a cloned repo can’t grant itself permissions. Deny and ask rules apply straight away.
  • Rules split compound commands. Bash(python -m pytest *) doesn’t approve python -m pytest && git push; each part has to match on its own.
  • A Read deny isn’t a sandbox. It stops Claude’s file tools and shell commands it recognises, like cat .env. It doesn’t stop a Python script that opens the file itself. For a hard guarantee, Claude Code’s sandbox blocks the path at the operating-system level.

Keeping lockfiles and generated code out of the agent’s searches is a separate job, for ignore files. That’s in article 05: Context Management.

Lint your rules file

Rules files rot like any docs, and nobody reviews them closely. The companion’s linter reads a CLAUDE.md or AGENTS.md and flags the problems above. Here’s its check for a missing test command:

TEST_RUNNERS = re.compile(
    r"\b(pytest|unittest|npm (run )?test|yarn test|pnpm (run )?test|go test|cargo test"
    r"|make test|rspec|mvn test|gradle test|gradlew test|tox|nox|jest|vitest|phpunit|dotnet test)\b"
)

def check_test_command(lines: list[str]) -> list[Problem]:
    code = [span for line in lines for span in re.findall(r"`([^`]+)`", line)]
    for first, last in fenced_blocks(lines):
        code += lines[first - 1 : last]
    if any(TEST_RUNNERS.search(snippet) for snippet in code):
        return []
    return [Problem("warn", "no-tests", "", "no test command in backticks.")]

It only looks inside backticks and code blocks, where commands live, so “make sure the code is well tested” still fails. The pasted-code check reads the repo next to the rules file and flags any code block where 60% of the lines appear in one source file. On both examples:

CLAUDE.md: 20 lines, about 258 tokens
  looks good

bad-CLAUDE.md: 223 lines, about 1,601 tokens
  ERROR  secret    line 37: looks like a credential. Move it to .env.
  ERROR  secret    line 38: looks like a credential. Move it to .env.
  WARN   too-long  223 lines. Aim for under 200.
  WARN   restates  lines 43-56: a 14-line file tree. The agent can list files.
  WARN   restates  lines 82-124: copies kitebase/tickets.py. Point at the file.
  ...
  WARN   no-tests  no test command in backticks.
  WARN   vague     lines 11, 15, 16, 22, 24, 219, 220, 221, 223: rules nobody can check.
  10 problem(s)

Secrets are errors and make the script exit with 1, so you can run it in CI (the automated checks on every pull request). The checks are rough on purpose: a regex can’t judge whether a rule is useful, but it catches the mistakes that slip through review.

One companion test is worth copying: it runs the Kitebase test suite with the exact command the CLAUDE.md gives. If someone changes how the tests run and forgets the rules file, that test fails. A wrong command is worse than none.

Try it yourself

The companion example has the Kitebase repo with its rules files and settings, the bad example (kept outside the repo so no tool loads it), and the linter. No API key needed.

Download the runnable example (zip)

cd 03-setting-up-context
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python main.py

Then try these:

  1. In kitebase-tickets/, run pytest -q, then python -m pytest -q. The first fails, the second prints 9 passed.
  2. Open Claude Code in kitebase-tickets/ and trust the folder. Run /context and check CLAUDE.md is listed under Memory files. Ask it to “add a --status filter to search_tickets”. Check the diff uses normalize_status(), adds a CLI flag and a test, and that the tests ran without a permission prompt.
  3. Run python main.py path/to/your/CLAUDE.md on a rules file from your own work, and fix what it flags.

pytest -q runs the offline tests.

Common beginner mistakes

  • No test command, or the wrong one. The agent can’t check its work, or burns turns on a failure that isn’t a bug.
  • Treating it as documentation. Overviews, file trees and pasted code belong in the code and the README.
  • Relying on it for safety. “Never read .env” is advice. A deny rule in .claude/settings.json is enforced.
  • Committing secrets to it. It’s in git and in every session. Name the environment variable instead.
  • Never pruning it. Add a line when the agent makes the same mistake twice, as Claude Code’s docs suggest. Delete lines that no longer match the code.

Questions you will face in production

“What do we commit, and what stays personal?” Commit CLAUDE.md and .claude/settings.json, and review changes like code. Personal preferences go in ~/.claude/CLAUDE.md, CLAUDE.local.md and .claude/settings.local.json. Article 09: Team Workflows and Guardrails covers the team side.

“The agent keeps ignoring a rule. What now?” Run /context to check the file loaded. Look for a conflict: two files, or a rule and the code, saying different things; the docs warn the model may pick either. Make the rule concrete: “use normalize_status()” beats “handle statuses carefully”. If it must be guaranteed, make it a permission rule or a hook (a script Claude Code runs at fixed points, like before every tool call).

Check your understanding

A teammate's agent keeps running plain pytest in the Kitebase repo, though CLAUDE.md says python -m pytest -q. What do you check first?

Whether the file loaded: /context, under Memory files. A common cause is starting Claude Code in the folder above the repo, which makes kitebase-tickets/CLAUDE.md a nested file that loads late. If it did load, look for another file that contradicts it.

Someone adds "Never run git push" to CLAUDE.md. Is that enough?

No. It’s advice the model will usually follow. If a push must not happen, add a deny rule like Bash(git push *) to .claude/settings.json, which the tool enforces. And keep branch protection on the server, since the docs note a push written another way, like git -C . push, isn’t matched.

You added Bash(python -m pytest *) to .claude/settings.json, but Claude Code still asks before every test run. Why?

Two likely causes. The folder isn’t trusted yet, and a committed file’s allow rules wait for that. Or the agent runs a command the rule doesn’t match, like python3 -m pytest or pytest. The prompt shows the exact command, so compare it with the rule.

Your frontend rules are 150 lines and only matter in web/. Where do they go?

Out of the root file. Put them in web/CLAUDE.md, which loads only when the agent reads a file in web/. Or put them in .claude/rules/frontend.md with paths: ["web/**"]. Either way, backend sessions don’t pay for them.

What to remember

  • A rules file is text the tool puts into the context at the start of every session. It’s advice, not config.
  • Claude Code joins user, project and local CLAUDE.md files at launch, and loads nested ones when the agent works in that folder. /context shows what loaded.
  • Write down what the code can’t say: the test command, the conventions to copy, the gotchas with their reasons.
  • Leave out pasted code, file trees, vague rules and secrets. Aim for under 200 lines; move folder-specific rules into nested files.
  • What must be enforced goes in .claude/settings.json: allow the commands you’d always approve, deny what must never happen.

What to study next

A rules file carries the context that’s true for every task. Each request still needs the context for this task: what done looks like, which files matter, how to check it. That’s article 04: Prompting Coding Agents, which works through the same Kitebase --status task.

Further reading

Where this article comes from. This is a synthesis of common practice in AI engineering as of 2026, not a citation of any single paper. The sources above are where the mechanics come from; the Claude Code details were checked against version 2.1.281 and its docs. If you find an error or have a better source for a claim, the article gets fixed within a day, send me a note.


Auto-marks when you reach the end. Click to toggle.