Skip to content
santiago cajal
Blog
All posts

8 min read

Claude Code on a real codebase: context beats tooling

How I set up Claude Code on a production Laravel app: a short CLAUDE.md, path-scoped rules, skills, strict types and tests as the contract for every change.

The Claude Code workflow that works on a real codebase has less to do with prompts than with the repository. The model reads what the project gives it: the rules file, the conventions in sibling files, the types, the tests. When those are clear, the agent writes code that fits. When they are missing, no amount of clever prompting makes up for it.

That is the short version of how I built Contamos, a multi-currency shared ledger, alone and from first commit to production in about four weeks. Claude Code was part of the loop from the first commit, which already shipped the agent instructions. What made that safe was not the model. It was the context around it. This post is the setup, piece by piece, with real examples from that repository.

What goes in CLAUDE.md, and what stays out

CLAUDE.md is the file Claude Code loads at the start of every session. Most examples online treat it as a place to dump everything the agent might need. That makes it long, and a long rules file is a rules file the model skims.

In Contamos, CLAUDE.md and AGENTS.md are the same file, so every agent reads the same instructions. Most of it is generated by Laravel Boost, and it holds only what applies everywhere:

  • Foundational context. The PHP version, and an instruction to check installed package versions before relying on an API rather than assuming one.
  • Conventions. Follow sibling files for structure and naming. Reuse components before writing new ones.
  • Boundaries. "Do not change the application's dependencies without approval." "Stick to existing directory structure; don't create new base folders without approval."
  • Test enforcement. Add or update tests for behaviour changes, run the affected ones, and "do not add tests beyond them".

Everything specific to one part of the codebase lives somewhere else.

A useful test for any line in CLAUDE.md: would it be true for every file in the repository? If not, it belongs closer to the code it governs.

Put decisions next to the code they govern

The second layer is a directory of path-scoped rules, .ai/rules, with an index that maps file globs to rule files. Before editing a file, the agent reads the rows whose globs match its path. There are around fifty of these files in Contamos, and each is short.

A rule looks like this, from the one that applies to the MCP tools:

---
paths:
  - 'app/Modules/Mcp/Tools/**'
---

# Tools

## Money is bcmath on strings
Carry amounts as decimal strings and compute or compare them with
bcadd/bcsub/bcmul/bcdiv/bccomp at an explicit SCALE of 18. Never use
floats, round() or number_format for money.

And this one, for domain Actions:

## Name write Actions Create or Update
Name transactional write Actions `Create{Resource}` or `Update{Resource}`.
Do not use `Store`, `Save`, or `Handle` prefixes for those writes.

Three things make this layer work.

Rules are settled decisions, not wishes. Each one records a choice that was already made and is non-obvious from the code alone: a trap, a naming convention, a constraint. "Write clean code" is not a rule.

Rules are scoped. A rule about Filament panels never loads when the agent is editing a migration. Context is finite, and what is irrelevant to the current task is noise.

Rules are recorded deliberately. The project instructions say it plainly: "Instructions for the work at hand are not rules, no matter how emphatic." If I tell the agent "use X here", that is work. A rule gets written only when I ask for one. Without that line, the agent turns every correction into a permanent rule, and the directory fills with contradictions.

Skills: procedures, not facts

Rules say what is true. Skills say how to do something. Claude Code loads a skill when its description matches the task, so it costs nothing until it is needed.

Contamos has about forty of them in .agents/skills, where every agent can read them, symlinked into .claude/skills for Claude Code. About a dozen ship with Laravel Boost and cover the packages the app uses: Cashier Paddle, Pennant, Inertia, MCP, testing. The rest are about process, like code-review and to-spec, and are installed with the skills CLI and pinned in skills-lock.json with a hash of each file, so an update is a visible change in a diff rather than something that happens quietly.

The distinction matters because it keeps both layers small. A rule that starts to read like a tutorial is probably a skill.

Strict types make wrong code fail early

An agent produces plausible code quickly. Plausible code that is wrong in a subtle way is the expensive kind. The cheapest place to catch it is before a test even runs.

Contamos runs PHPStan at level: max over app/, with Larastan. Array payloads get shape annotations, so a webhook handler declares exactly what it reads:

/**
 * @param  array{data: array{id: string, status: string, canceled_at: string}}  $payload
 */
protected function handleSubscriptionCanceled(array $payload): void

On the frontend, TypeScript types mirror the JSON the server sends. Pint and Rector keep formatting and modernisation mechanical, so review never spends attention on style. One Composer script, ci:check, runs the frontend checks, the type check and the test suite, and the same command runs in CI.

None of this is specific to AI. A codebase legible to a new engineer is legible to a model for the same reasons. The difference is volume: an agent writes more code per hour than a person, so the checks run far more often and pay for themselves sooner.

Tests are the contract

The test suite is how I decide whether a change is correct, not how I decide whether it looks correct. Contamos has more than 1,500 Pest tests, and CI runs them in parallel on every push to main and every pull request.

Two habits make tests useful as a contract with an agent:

  • Test behaviour and its failure modes. For a transfer between accounts: the balances on both sides, the exchange rate recorded, what happens when the source has not enough funds, what happens when the user lacks permission. Not the private method that formats the amount.
  • Do not let the agent grade its own homework. When a change touches money, permissions or data shape, I read the tests first. If the tests are wrong, the implementation passing them means nothing.

A failing test is also the best prompt there is. "Make this pass without changing the test" is more precise than a paragraph of description.

Write the spec before the prompt

For anything larger than a small fix, I write down what the change should do before any code exists: the behaviour, the edge cases, what is out of scope. Sometimes that is a few bullet points. For bigger features, the to-spec skill turns the conversation into an issue.

The spec does two jobs. It forces the decision about what is worth building, which is the part no agent can do for me. And it gives the review something to compare against. Without a spec, reviewing a diff becomes a matter of taste.

Where review stays human

AI moves the bottleneck from writing code to judging it. Some things I always review line by line, whatever produced them:

  • Money. Anything that computes, stores or converts an amount.
  • Permissions and tenancy. Who can see or change what, and whether a query can cross from one team into another.
  • Migrations. Schema changes are the hardest thing to undo in production.
  • The rules themselves. A bad rule is worse than a bad function, because it repeats.

Everything else still gets read, but with the tests and the type checker having done the first pass.

A minimal setup you can copy

If you are starting with Claude Code on an existing project, this order gets the most out of the least effort:

  1. A short CLAUDE.md: versions, conventions, boundaries, how to run the tests. Only what is true everywhere.
  2. The strictest type checking the codebase can pass today, raised over time.
  3. One command that runs everything CI runs.
  4. Path-scoped rules for the traps you keep correcting by hand.
  5. Skills for repeated procedures, pinned to a version.
  6. A spec, however short, before anything non-trivial.

Most of that work is making the system explain itself. That is worth doing whether or not a model is reading it, and it is the reason the model helps at all.

For how this played out on a whole product, see the Contamos case study.

Santiago Cajal · Published