{"slug": "a-working-claude-md-agents-md-template-you-can-copy-today", "title": "A Working CLAUDE.md/AGENTS.md Template You Can Copy Today", "summary": "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.", "body_md": "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.\n\nThe 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.\n\nHere's the pattern almost everyone starts with:\n\n```\n## Code style\n- Write clean, maintainable code\n- Use good error handling\n- Follow best practices\n```\n\nNone 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:\n\n```\n// Avoid\ncatch (e) {\n  console.log(e);\n}\n\n// Preferred\ncatch (e) {\n  logger.error('checkout.payment_failed', { orderId, cause: e });\n  throw new PaymentError(orderId, e);\n}\n```\n\nThe 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.**\n\nA good context file has six parts, in this order:\n\nLet's build the file section by section.\n\n```\n---\nlast_updated: 2026-09-14\nowner: platform-team\nscope: global\nreview_cadence: quarterly\n---\n```\n\nThis 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.\n\nState the things a competent engineer would otherwise have to reverse-engineer from the codebase — especially anything that goes *against* the obvious default.\n\n```\n## Project context\n\n- Stack: Node.js 20, Express, PostgreSQL (raw SQL via `pg`, no ORM — see ADR-014)\n- Monorepo managed with pnpm workspaces\n- Auth: sessions via `iron-session`, not JWTs — do not introduce JWT-based auth\n- All API responses follow the envelope in `src/lib/response.ts`; never return raw objects from route handlers\n```\n\nAny 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.\n\nPick your 3-5 highest-value conventions rather than trying to cover everything. More isn't better here; it's context rot.\n\n**API responses**\n\n```\n// Avoid\nres.json({ id: user.id, name: user.name });\n\n// Preferred\nres.json(successResponse({ id: user.id, name: user.name }));\n```\n\n**Async error handling**\n\n``` js\n// Avoid\napp.get('/users/:id', async (req, res) => {\n  const user = await getUser(req.params.id);\n  res.json(user);\n});\n\n// Preferred\napp.get('/users/:id', asyncHandler(async (req, res) => {\n  const user = await getUser(req.params.id);\n  res.json(successResponse(user));\n}));\n```\n\n**Naming**\n\n``` js\n// Avoid\nconst d = new Date();\nconst u = await getUser(id);\nfunction calc(x, y) { return x * y * 0.08; }\n\n// Preferred\nconst requestTimestamp = new Date();\nconst user = await getUser(id);\nfunction calculateSalesTax(subtotal, taxRate = 0.08) {\n  return subtotal * taxRate;\n}\n```\n\n**Dependency access**\n\n``` js\n// Avoid\nimport { db } from '../../../lib/db';\n\nexport async function getOrders(userId) {\n  return db.query('SELECT * FROM orders WHERE user_id = $1', [userId]);\n}\n\n// Preferred\nimport { OrdersRepository } from './orders.repository';\n\nexport async function getOrders(userId, ordersRepo = new OrdersRepository()) {\n  return ordersRepo.findByUserId(userId);\n}\n```\n\n(Repos are injectable so tests can pass a fake — see `tests/api/orders.test.ts` for the pattern.)\n\nEach pair takes about 30 seconds to write and saves you from re-explaining the same thing in code review, repeatedly, forever.\n\n```\n## Testing\n\n- Every new route handler needs an integration test in `tests/api/`, following the pattern in `tests/api/users.test.ts`\n- Run `pnpm test:unit` before considering any change complete\n- Do not mock the database in integration tests — use the test containers setup in `tests/setup.ts`\n- Minimum coverage for new files: 80%\n## Do not touch\n\n- `migrations/` — migrations are hand-reviewed only; never generate or edit these\n- `src/legacy/billing/` — frozen code pending a rewrite; bug fixes only, no refactors\n- `.github/workflows/` — CI changes require a platform-team review; flag instead of editing directly\n```\n\nIf 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.\n\n```\n## Commands\n\n- Install: `pnpm install`\n- Run dev server: `pnpm dev`\n- Run all tests: `pnpm test`\n- Lint: `pnpm lint`\n- Type check: `pnpm typecheck`\n---\nlast_updated: YYYY-MM-DD\nowner: team-name\nscope: global\nreview_cadence: quarterly\n---\n\n## Project context\n- Stack: [languages, frameworks, database]\n- Architecture: [monorepo/polyrepo, key services]\n- Non-obvious constraints: [things that go against the default assumption]\n\n## Conventions\n### [Convention name]\nAvoid:\n[bad example]\nPreferred:\n[good example]\n(repeat for 3-5 highest-value conventions)\n\n## Testing\n- [what every change requires]\n- [how to run tests]\n- [coverage or quality gates]\n\n## Do not touch\n- [path]: [reason]\n\n## Commands\n- Install: [command]\n- Dev: [command]\n- Test: [command]\n- Lint: [command]\n```\n\nHere's the same template filled in for a mid-sized ASP.NET Core Web API with EF Core and Clean Architecture:\n\n```\n---\nlast_updated: 2026-09-14\nowner: payments-team\nscope: global\nreview_cadence: quarterly\n---\n## Project context\n- Stack: .NET 8, ASP.NET Core Web API, EF Core 8, SQL Server\n- Architecture: Clean Architecture — Api/, Application/, Domain/, Infrastructure/\n- CQRS via MediatR — every write is a Command, every read is a Query\n- Do not call EF Core directly from controllers — always go through a MediatR handler\n- Dependency injection only — no `new SomeService()` inside business logic\n```\n\n**Controllers stay thin**\n\n```\n// Avoid\n[HttpPost]\npublic async Task<IActionResult> CreateOrder(CreateOrderDto dto)\n{\n    var order = new Order { CustomerId = dto.CustomerId, Total = dto.Total };\n    _dbContext.Orders.Add(order);\n    await _dbContext.SaveChangesAsync();\n    return Ok(order);\n}\n\n// Preferred\n[HttpPost]\npublic async Task<IActionResult> CreateOrder(CreateOrderCommand command)\n{\n    var result = await _mediator.Send(command);\n    return CreatedAtAction(nameof(GetOrder), new { id = result.OrderId }, result);\n}\n```\n\n**Nullable reference handling**\n\n``` js\n// Avoid\npublic string GetCustomerName(int id)\n{\n    var customer = _repository.Find(id);\n    return customer.Name; // throws NullReferenceException if not found\n}\n\n// Preferred\npublic async Task<Result<string>> GetCustomerNameAsync(int id)\n{\n    var customer = await _repository.FindAsync(id);\n    return customer is null\n        ? Result.Failure<string>($\"Customer {id} not found\")\n        : Result.Success(customer.Name);\n}\n```\n\n**Async naming and cancellation**\n\n```\n// Avoid\npublic Task<List<Order>> GetOrders(int customerId)\n{\n    return _dbContext.Orders.Where(o => o.CustomerId == customerId).ToListAsync();\n}\n\n// Preferred\npublic Task<List<Order>> GetOrdersAsync(int customerId, CancellationToken cancellationToken)\n{\n    return _dbContext.Orders\n        .Where(o => o.CustomerId == customerId)\n        .ToListAsync(cancellationToken);\n}\n```\n\nEvery 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.\n\nA few things worth noticing about this version versus the generic one:\n\n`DbContext` straight into a controller.`Result<T>` pattern\nRun a five-minute experiment:\n\nIf 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.\n\nThe 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.\n\n*Originally published at [Dhrutika's Blog](https://dhrutika.github.io/portfolio/blog/agents-md-template.html) — I write about .NET, Angular migrations, and AI-assisted development.*", "url": "https://wpnews.pro/news/a-working-claude-md-agents-md-template-you-can-copy-today", "canonical_source": "https://dev.to/dhrutika_rathod/a-working-claudemdagentsmd-template-you-can-copy-today-2dlh", "published_at": "2026-09-21 03:46:31+00:00", "updated_at": "2026-09-21 04:23:17.405507+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-tools", "agent-protocols"], "entities": ["Node.js", "Express", "PostgreSQL", "pnpm"], "alternates": {"html": "https://wpnews.pro/news/a-working-claude-md-agents-md-template-you-can-copy-today", "markdown": "https://wpnews.pro/news/a-working-claude-md-agents-md-template-you-can-copy-today.md", "text": "https://wpnews.pro/news/a-working-claude-md-agents-md-template-you-can-copy-today.txt", "jsonld": "https://wpnews.pro/news/a-working-claude-md-agents-md-template-you-can-copy-today.jsonld"}}