cd /news/ai-agents/a-working-claude-md-agents-md-templa… · home topics ai-agents article
[ARTICLE · art-135513] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

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

A developer published a copy-paste CLAUDE.md/AGENTS.md template for repository-level coding agent context files, arguing that vague prose instructions like "write clean code" fail to change agent behavior. The template prescribes six sections — frontmatter metadata, project context, conventions, testing, and others — with the core technique of replacing adjectives with concrete avoid/preferred code block pairs. It recommends limiting conventions to the three to five highest-value ones to avoid context rot.

by read6 min views3 publishedSep 21, 2026

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.

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.

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

Let's build the file section by section.

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

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.

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.

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

## Commands

- Install: `pnpm install`
- Run dev server: `pnpm dev`
- Run all tests: `pnpm test`
- Lint: `pnpm lint`
- Type check: `pnpm typecheck`
---
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]

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:

DbContext straight into a controller.Result<T> pattern Run a five-minute experiment:

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.

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.

── more in #ai-agents 4 stories · sorted by recency
── more on @node.js 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/a-working-claude-md-…] indexed:0 read:6min 2026-09-21 ·