A Working CLAUDE.md/AGENTS.md Template You Can Copy Today

If you've added a CLAUDE.md or AGENTS.md file to your repo and felt like your coding agent still ignores half of it, you're not alone. Most of these files are just vague prose — "write clean code," "follow best practices," "use good error handling" — and vague prose doesn't change agent behavior any more than it changes a new hire's behavior on day one.

The fix isn't a longer file. It's a differently structured one. Below is a template you can copy straight into your repo today, plus the reasoning behind each section so you're not just cargo-culting it.

Why most AGENTS.md files fail

Here's the pattern almost everyone starts with:

## Code style
- Write clean, maintainable code
- Use good error handling
- Follow best practices

None of this is wrong, exactly. It's just useless to a model. "Good error handling" means nothing without a concrete shape to imitate. Compare that to this:

## Error handling

### Avoid
catch (e) {
  console.log(e);
}

### Preferred
catch (e) {
  logger.error('checkout.payment_failed', { orderId, cause: e });
  throw new PaymentError(orderId, e);
}

The second version gives the model an actual pattern to pattern-match against — a logger call with a namespaced event, structured metadata, and a typed error thrown upward. That's the single biggest lever in this whole exercise: replace adjectives with code blocks.

The anatomy of a file that actually works

A good context file has six parts, in this order:

Let's build the file section by section.

1. Metadata header

---
last_updated: 2026-09-14
owner: platform-team
scope: global
review_cadence: quarterly
---

This looks like overhead, but it's the difference between a file that rots silently and one that gets maintained. When a rule looks outdated, whoever finds it knows exactly who to ping. Without this, stale context files just accumulate contradictions nobody catches until an agent trips on one.

2. Project context

This section exists to state the things a competent engineer would otherwise have to reverse-engineer from the codebase — especially anything that goes against the obvious default.

## Project context

- Stack: Node.js 20, Express, PostgreSQL (raw SQL via `pg`, no ORM — see ADR-014)
- Monorepo managed with pnpm workspaces
- Auth: sessions via `iron-session`, not JWTs — do not introduce JWT-based auth
- All API responses follow the envelope in `src/lib/response.ts`; never return raw objects from route handlers

Notice the ORM line. Any model trained broadly will default to reaching for an ORM the moment it touches a database. Stating the constraint and pointing to the reasoning (an ADR link) heads that off before it happens.

3. Coding conventions (Preferred/Avoid blocks)

Pick your 3-5 highest-value conventions — the ones that actually recur in every PR — rather than trying to cover everything. More isn't better here; it's context rot.

API responses

Avoid:

res.json({ id: user.id, name: user.name });

Preferred:

res.json(successResponse({ id: user.id, name: user.name }));

Async error handling

Avoid:

app.get('/users/:id', async (req, res) => {
  const user = await getUser(req.params.id);
  res.json(user);
});

Preferred:

app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await getUser(req.params.id);
  res.json(successResponse(user));
}));

Naming

Avoid:

const d = new Date();
const u = await getUser(id);
function calc(x, y) { return x * y * 0.08; }

Preferred:

const requestTimestamp = new Date();
const user = await getUser(id);
function calculateSalesTax(subtotal, taxRate = 0.08) {
  return subtotal * taxRate;
}

Dependency access

Avoid:

import { db } from '../../../lib/db';

export async function getOrders(userId) {
  return db.query('SELECT * FROM orders WHERE user_id = $1', [userId]);
}

Preferred:

import { OrdersRepository } from './orders.repository';

export async function getOrders(userId, ordersRepo = new OrdersRepository()) {
  return ordersRepo.findByUserId(userId);
}

(Repos are injectable so tests can pass a fake — see tests/api/orders.test.ts for the pattern.)

Each pair takes about 30 seconds to write and saves you from re-explaining the same thing in code review, repeatedly, forever.

4. Testing requirements

Be specific about what "done" means. "Write tests" is not a requirement an agent (or a junior engineer) can act on.

## Testing

- Every new route handler needs an integration test in `tests/api/`, following the pattern in `tests/api/users.test.ts`
- Run `pnpm test:unit` before considering any change complete
- Do not mock the database in integration tests — use the test containers setup in `tests/setup.ts`
- Minimum coverage for new files: 80% (checked in CI, not enforced locally, but treat it as a gate)

5. Do-not-touch list

This is the highest-leverage section for preventing damage, and the one most files skip entirely.

## Do not touch

- `migrations/` — migrations are hand-reviewed only; never generate or edit these
- `src/legacy/billing/` — frozen code pending a rewrite; bug fixes only, no refactors
- `.github/workflows/` — CI changes require a platform-team review; flag instead of editing directly

If you've ever had an agent "helpfully" refactor a file that was explicitly untouchable, this section is why it happened — nobody told it not to.

6. Command reference

## Commands

- Install: `pnpm install`
- Run dev server: `pnpm dev`
- Run all tests: `pnpm test`
- Run a single test file: `pnpm test -- tests/api/users.test.ts`
- Lint: `pnpm lint`
- Type check: `pnpm typecheck`

Trivial, but it removes an entire category of wasted agent turns spent guessing your package.json scripts.

The full template

Here's everything assembled — copy this, then replace every line with your own project's specifics.

---
last_updated: YYYY-MM-DD
owner: team-name
scope: global
review_cadence: quarterly
---

## Project context

- Stack: [languages, frameworks, database]
- Architecture: [monorepo/polyrepo, key services]
- Non-obvious constraints: [things that go against the default assumption]

## Conventions

### [Convention name]
Avoid:
[bad example]

Preferred:
[good example]

(repeat for 3-5 highest-value conventions)

## Testing

- [what every change requires]
- [how to run tests]
- [coverage or quality gates]

## Do not touch

- [path]: [reason]
- [path]: [reason]

## Commands

- Install: [command]
- Dev: [command]
- Test: [command]
- Lint: [command]

A real-world example: ASP.NET Core project

The template above is deliberately generic. Here's what it looks like filled in for an actual project — a mid-sized ASP.NET Core Web API with Entity Framework Core and a Clean Architecture-style layout. Use this as a second reference point alongside the generic template; between the two you should be able to adapt this to pretty much any stack.

---
last_updated: 2026-09-14
owner: payments-team
scope: global
review_cadence: quarterly
---

## Project context

- Stack: .NET 8, ASP.NET Core Web API, EF Core 8, SQL Server
- Architecture: Clean Architecture — `Api/`, `Application/`, `Domain/`, `Infrastructure/`
- CQRS via MediatR — every write is a `Command`, every read is a `Query`, handled in `Application/`
- Do not call EF Core directly from controllers — always go through a MediatR handler
- Validation via FluentValidation, wired in automatically through a MediatR pipeline behavior
- Dependency injection only — no `new SomeService()` inside business logic; register in `Program.cs`

Controllers stay thin

Avoid:

[HttpPost]
public async Task<IActionResult> CreateOrder(CreateOrderDto dto)
{
    var order = new Order { CustomerId = dto.CustomerId, Total = dto.Total };
    _dbContext.Orders.Add(order);
    await _dbContext.SaveChangesAsync();
    return Ok(order);
}

Preferred:

[HttpPost]
public async Task<IActionResult> CreateOrder(CreateOrderCommand command)
{
    var result = await _mediator.Send(command);
    return CreatedAtAction(nameof(GetOrder), new { id = result.OrderId }, result);
}

Controllers only translate HTTP in and out. All logic lives in the handler.

Nullable reference handling

Avoid:

public string GetCustomerName(int id)
{
    var customer = _repository.Find(id);
    return customer.Name; // throws NullReferenceException if not found
}

Preferred:

public async Task<Result<string>> GetCustomerNameAsync(int id)
{
    var customer = await _repository.FindAsync(id);
    return customer is null
        ? Result.Failure<string>($"Customer {id} not found")
        : Result.Success(customer.Name);
}

We use the Result<T> pattern (see Domain/Common/Result.cs) instead of throwing for expected failure cases. Reserve exceptions for truly unexpected states.

Async naming and cancellation

Avoid:

public Task<List<Order>> GetOrders(int customerId)
{
    return _dbContext.Orders.Where(o => o.CustomerId == customerId).ToListAsync();
}

Preferred:

public Task<List<Order>> GetOrdersAsync(int customerId, CancellationToken cancellationToken)
{
    return _dbContext.Orders
        .Where(o => o.CustomerId == customerId)
        .ToListAsync(cancellationToken);
}

Every async method ends in Async and accepts a CancellationToken as the last parameter — this is enforced by an analyzer, so missing it will fail the build, not just review.

## Testing

- Every new `Command`/`Query` handler needs a test in `Application.Tests/`, using the in-memory EF Core provider pattern in `TestBase.cs`
- Controller-level tests use `WebApplicationFactory` — see `Api.Tests/OrdersControllerTests.cs`
- Run `dotnet test` before considering any change complete
- Do not mock `DbContext` directly; use the SQLite in-memory provider configured in `TestBase.cs`

## Do not touch

- `Migrations/` — EF Core migrations are hand-reviewed only; never hand-edit a generated migration
- `Infrastructure/Legacy/` — frozen pending the billing-system rewrite; bug fixes only
- `appsettings.Production.json` — never edit directly; changes go through the deployment pipeline config

## Commands

- Restore: `dotnet restore`
- Run API locally: `dotnet run --project Api`
- Run all tests: `dotnet test`
- Add a migration: `dotnet ef migrations add <Name> --project Infrastructure --startup-project Api`
- Format check: `dotnet format --verify-no-changes`

A few things worth noticing about this version versus the generic one:

How to verify your file is actually pulling weight

Don't just ship this and assume it's working. Run a five-minute experiment:

  1. Pick a real, moderately complex task from your backlog.
  2. Give it to your agent with the context file temporarily renamed/removed.
  3. Give it the exact same prompt with the file restored.
  4. Diff the two outputs.

If the two diffs look nearly identical, your file isn't doing anything — go back and turn more of your prose into Preferred/Avoid code blocks. If the second run correctly follows a convention the first one violated, you've got a working file.

Common mistakes to avoid

Wrap-up

The difference between a context file that gets ignored and one that actually shapes agent behavior isn't length or thoroughness — it's specificity. Adjectives don't transfer; code blocks do. Start with the six sections above, keep it lean, and run the five-minute verification test before you assume it's working.

← Back to all posts