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

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

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:

// 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:

  • Metadata header - so humans (and eventually you, in six months) know if the file is still trustworthy
  • Project context - stack, architecture, and the non-obvious constraints a new engineer wouldn't guess
  • Coding conventions - Preferred/Avoid blocks, not adjectives
  • Testing requirements - what "done" actually means in your repo
  • Do-not-touch list - the fastest way to prevent a confidently wrong 200-line diff
  • Command reference - how to build, test, and lint, so the agent isn't guessing or hallucinating flags

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.

2. Project Context

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

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 heads that off before it happens.

3. Coding Conventions (Preferred/Avoid Blocks)

Pick your 3-5 highest-value conventions 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

## 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%

5. Do-Not-Touch List

## 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`
- Lint: `pnpm lint`
- Type check: `pnpm typecheck`

The Full Template

---
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]

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

A Real-World Example: ASP.NET Core Project

Here's the same template filled in for a mid-sized ASP.NET Core Web API with EF Core and Clean Architecture:

---
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
- Do not call EF Core directly from controllers - always go through a MediatR handler
- Dependency injection only - no `new SomeService()` inside business logic

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);
}

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);
}

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 - enforced by an analyzer, so missing it fails the build, not just review.

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

  • The CQRS/MediatR rule is the single highest-leverage line - without it, an agent trained on typical ASP.NET tutorials will default to injecting DbContext straight into a controller.
  • The Result<T> pattern matters more than it looks - .NET samples online overwhelmingly favor exceptions for control flow, so the file has to actively counteract that default.
  • The analyzer-enforced convention is worth stating explicitly even though tooling enforces it - it saves the agent a wasted turn discovering the build failure itself.

How to Verify Your File Is Actually Pulling Weight

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. If the second run correctly follows a convention the first one violated, you've got a working file.

Common Mistakes to Avoid

  • Too long. A 500-line context file loaded on every turn is context rot in disguise.
  • Duplicating the README. If it's already documented for humans, link to it instead.
  • Never updated. A stale "do not touch" list teaches the team to ignore the file entirely.

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.

Originally published at Dhrutika's Blog - I write about .NET, Angular migrations, and AI-assisted development.

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.