Give Every Coding Agent One Codebase Briefing
Coding agents produce generic output when nothing tells them how your repository is actually put together. CodeAI.md is a ready-made AGENTS.md: project layout, build and test commands, naming rules, error handling, and integration points, in the plain format every major tool now reads.
Because precedence is nearest-file-wins, a monorepo carries one root briefing plus small per-package overrides. Claude Code merges AGENTS.md with CLAUDE.md, so adopting the standard costs nothing if you already keep project instructions.
The gap between a tool that helps and one that generates rework is almost never the model. It is whether anybody wrote the briefing down.
CodeAI Context Best Practices
How to write an AGENTS.md that yields correct output on the first attempt instead of a plausible guess you have to unwind.
Map the Directory Layout
Annotate each top-level directory with what it holds and what it must never import. Layout plus import rules is what makes generated files land in the right place with working paths.
State the Build and Test Commands
The highest-value line in the whole file is the exact command that runs the tests. A tool that can verify its own work converges on something correct. One that cannot will hand you output that merely looks finished.
Document Error Handling
Capture your error classes, logging format, user-facing message style, and retry policy. Error paths are where generated code diverges from house style fastest, because they are the part nobody ever demos.
Specify Test Structure
Describe how tests are organized, what gets mocked, and which assertion style you use. Tests that match your patterns get reviewed and kept. Tests written against a generic template get deleted in the next cleanup.
Describe the Seams
Map how modules actually connect: API contracts, event names, shared state, data access boundaries. Most integration bugs come from an assumption about a seam that nobody thought to write down.
List What Is Off Limits
Banned libraries, deprecated patterns, directories that must not be edited, and legacy code that should not be imitated. Prohibitions are cheap to write and disproportionately effective.
Show One Real Example
For each pattern, paste a short excerpt from your actual source. One concrete example carries far more signal than a paragraph describing the same thing in the abstract.
Split the File Before It Sprawls
When the root briefing grows past what anyone will read, move package-specific rules into package-level files and let precedence do the work. A short file that is right beats a long one that gets skimmed.
Write It Once, Not Once Per Tool
Agent instructions used to be a per-vendor problem: one file for this editor, another for that command line, each quietly drifting from the others. AGENTS.md ended that. It is plain markdown, it carries no schema, and it now sits under the Linux Foundation rather than any single vendor. The practical consequence is that your codebase briefing outlives your tool choices. Switch editors, add a second agent, hand the repo to a contractor - the file still applies.
The CodeAI Template
# CodeAI.md - AI Coding Assistant Context
<!-- Content for your AGENTS.md / CLAUDE.md: what an agent must know to write correct code HERE -->
<!-- Codebase map, patterns, anti-patterns, MCP access, and what to confirm before implementing -->
<!-- Last updated: 2026-08-26 -->
## Where This File Lives
This is content for the agent context file at the root of your repository. Two filenames matter:
- **AGENTS.md** - the cross-tool standard for agent instructions. Plain Markdown, no YAML frontmatter, no required schema. It is read natively by the major coding agents, editor assistants, and CLIs, so one file serves whatever tool a teammate happens to use.
- **CLAUDE.md** - Claude Code's own context file. Claude Code reads AGENTS.md as well and merges both, plus any files in subdirectories, so you do not need to duplicate content between them.
**Precedence is nearest-file-wins.** The context file closest to the file being edited governs, and an explicit instruction in the prompt overrides every file on disk. In a monorepo that means you should not write one enormous root file:
```text
repo/
AGENTS.md # what the product is, how to build and test everything
packages/
api/AGENTS.md # API-only: router pattern, migration workflow
web/AGENTS.md # UI-only: component conventions, design tokens
worker/AGENTS.md # queue semantics, idempotency requirements
```
Keep each file short and specific. Analysis across thousands of public repositories found that the most effective context files were roughly 20 to 30 lines. Three sections earn their place almost everywhere, in this order:
1. **Project overview** - what it is, language and framework, with versions pinned somewhere the agent can read rather than restated in prose.
2. **Build and test commands** - the exact command with its flags, not the name of the tool. "Run the tests with Vitest" is useless; `npm run test:unit -- --run` is actionable.
3. **Code style rules that DIFFER from language defaults** - do not restate what your formatter already enforces.
Everything else belongs somewhere that loads on demand: a skill, a runbook, a linked document the agent opens when the task calls for it. Context is not free. Every token in an always-loaded file competes with the code the agent actually needs to read.
Rules that must never be skipped do not belong in prose at all. A context file is advisory - a model can decide a paragraph does not apply. Encode non-negotiables as a hook, a lint rule, or a CI gate so they execute whether or not the agent read this far. See `coding.md` for the enforcement hierarchy.
## Codebase Overview
**Project**: Vaultline - Secure Document Management Platform
**Language**: TypeScript, strict mode
**Runtime**: Node, current LTS line - the exact version is pinned in `.nvmrc`
**Framework**: Next.js App Router with tRPC
**Database**: PostgreSQL via Drizzle ORM
**Auth**: NextAuth with Google and email/password providers
Confirm the environment before you start, rather than assuming versions from memory:
```bash
node --version # expect the version pinned in .nvmrc
npm ls next drizzle-orm @trpc/server --depth=0 # expect the ranges pinned in package.json
```
### High-Level Architecture
```text
vaultline/
src/
app/ Next.js App Router pages
(auth)/ Auth route group (login, register, reset)
(dashboard)/ Authenticated route group
documents/ Document listing, search, filters
folders/ Folder management and navigation
settings/ User and org settings
layout.tsx Dashboard shell with sidebar
api/trpc/ tRPC endpoint handler
layout.tsx Root layout (providers, fonts, metadata)
page.tsx Landing page (marketing)
components/
ui/ Design-system primitives (button, dialog, input)
documents/ Document-specific components
folders/ Folder tree and navigation
shared/ Cross-feature components (search, empty states)
server/
db/ Drizzle schema, migrations, seed
schema.ts All table definitions
migrations/ SQL migration files
index.ts DB connection singleton
trpc/
routers/ Feature-specific routers
context.ts Request context (session, db)
index.ts Root router + type exports
services/ Business logic, no framework dependencies
document.service.ts
folder.service.ts
permission.service.ts
storage.service.ts
lib/ Shared utilities (utils, constants, validators)
hooks/ Custom React hooks
types/ Shared TypeScript types
tests/
unit/ Fast, isolated tests
integration/ tRPC router tests against a real database
e2e/ Playwright browser tests
drizzle.config.ts
next.config.ts
```
Data flows in one direction. An agent that puts logic in the wrong layer produces code that passes review by looking familiar and fails later:
```mermaid
graph LR
A[Server Component] --> B[tRPC Router]
C[Client Component] --> B
B --> D[Permission Service]
B --> E[Domain Service]
E --> F[(Drizzle / PostgreSQL)]
E --> G[Storage Service]
```
### Key Entry Points
- **App shell**: `src/app/(dashboard)/layout.tsx` - sidebar, header, auth guard
- **tRPC root router**: `src/server/trpc/index.ts` - every API procedure
- **Database schema**: `src/server/db/schema.ts` - every table definition
- **Auth config**: `src/lib/auth.ts` - providers, callbacks, session strategy
## MCP Servers This Repo Expects
Static prose cannot tell an agent the current schema, the open issues, or last night's error rate. Tool access can. Document which Model Context Protocol servers this repository expects, what each is for, what credential scope it needs, and - most importantly - whether it can write. Declare them in the repo's MCP config (commonly `.mcp.json`) so a fresh clone gets the same wiring.
| Server | Purpose | Credential and scope | Access |
|--------|---------|----------------------|--------|
| `postgres-dev` | Schema introspection, query plans, seed-data inspection | `MCP_PG_URL`, points at the local dev database only | Read-only |
| `postgres-replica` | Verify query shape against realistic row counts | Read-replica role, no write grants, no PII columns | Read-only |
| `github` | Issues, pull requests, CI status, cross-repo code search | Fine-grained token: contents read, issues and PRs write, this repo only | Write-capable |
| `sentry` | Recent error groups and stack traces for this service | Project-scoped read token | Read-only |
| `storage-dev` | Presigned upload URLs for exercising the upload path | Bucket-scoped role, `vaultline-dev-uploads` only | Write-capable |
**Rules for tool access**:
- Never point a write-capable server at production. If a task genuinely needs production data, use the read-only replica and redact before it enters context.
- Prefer the read-only server when both exist. Reach for the write-capable one only when the task is to change something.
- If an expected server is unavailable, say so and stop. Guessing at a schema you could have introspected is the most expensive kind of confident wrong answer.
- Record what a server is NOT for. `github` is for issues and CI status, not for pushing branches - branches go through the normal git workflow in `coding.md`.
## Conventions Specific to This Repo
The full naming table and the team-wide standards live in `coding.md` - that file owns them, and this one does not repeat them. What follows is only the set of rules that are specific to this codebase or that differ from the language defaults, which is the part an agent cannot infer.
```typescript
// Strict mode is ON. No implicit any, no unused variables.
// Type-only imports use the 'type' keyword so the bundler can drop them.
import type { Document } from '@/types';
import { formatDate } from '@/lib/utils';
// Interfaces for object shapes, type aliases for unions and intersections.
interface DocumentListProps {
folderId: string;
sortBy: 'name' | 'created' | 'modified';
onSelect: (doc: Document) => void;
}
// Exported functions always declare an explicit return type.
export function calculateStorageUsage(files: FileRecord[]): number {
return files.reduce((total, file) => total + file.sizeBytes, 0);
}
// 'as const' objects instead of enums.
export const DOCUMENT_STATUS = {
DRAFT: 'draft',
PUBLISHED: 'published',
ARCHIVED: 'archived',
} as const;
// The service layer returns a Result type instead of throwing.
type Result<T> = { success: true; data: T } | { success: false; error: string };
```
Repo-specific naming deltas on top of the table in `coding.md`:
- Database columns are `snake_case` in the schema and `camelCase` in TypeScript. Drizzle does the mapping; do not hand-write either side.
- Environment variables carry the app prefix: `VAULTLINE_DATABASE_URL`, `VAULTLINE_S3_BUCKET`. An unprefixed variable is a bug, not a shortcut.
- Hook files are named for the hook they export: `use-document-search.ts` exports `useDocumentSearch`.
- Service files end in `.service.ts` and export functions, not a default class.
### Component Boundary
```typescript
// Server Components are the default. Add 'use client' only for hooks,
// event handlers, or browser APIs - never "just in case".
// Server Component: fetch at the component level.
export default async function DocumentsPage({ params }: { params: { folderId: string } }) {
const documents = await trpc.document.listByFolder({ folderId: params.folderId });
return <DocumentList documents={documents} />;
}
// Client Component: interaction only.
'use client';
export function DocumentSearch({ onResultClick }: DocumentSearchProps) {
const [query, setQuery] = useState('');
const results = trpc.document.search.useQuery({ query }, { enabled: query.length > 2 });
// ...
}
```
Server components pass data down; client components handle interaction. Do not fetch in a client component when a server component above it already can.
## Patterns to Follow
The examples below come from one stack - App Router, tRPC, Drizzle. Replace them with your own. The point is not the stack; it is that a concrete, copyable example outperforms an abstract rule by a wide margin. An agent asked to "follow the service layer pattern" will invent one. An agent shown twelve lines of the real thing will match it.
### Router Pattern
```typescript
// File: src/server/trpc/routers/document.ts
import { z } from 'zod';
import { protectedProcedure, router } from '../trpc';
import * as documentService from '@/server/services/document.service';
export const documentRouter = router({
getById: protectedProcedure
.input(z.object({ id: z.string().uuid() }))
.query(async ({ input, ctx }) => {
const doc = await documentService.getById(input.id);
if (!doc) throw new TRPCError({ code: 'NOT_FOUND' });
// Permission check happens before the record leaves the router.
await ctx.permissions.assertCanView(ctx.session.userId, doc);
return doc;
}),
create: protectedProcedure
.input(createDocumentSchema) // Zod schema imported from lib/validators.ts
.mutation(async ({ input, ctx }) => {
return documentService.create({ ...input, ownerId: ctx.session.userId });
}),
});
```
### Service Layer Pattern
```typescript
// Services hold business logic and import nothing from the HTTP or tRPC layer.
// File: src/server/services/document.service.ts
import { db } from '@/server/db';
import { documents } from '@/server/db/schema';
import { eq } from 'drizzle-orm';
export async function getById(id: string): Promise<Document | null> {
const [doc] = await db.select().from(documents).where(eq(documents.id, id)).limit(1);
return doc ?? null;
}
export async function create(data: CreateDocumentInput): Promise<Document> {
const [doc] = await db.insert(documents).values(data).returning();
return doc;
}
// One service per domain entity. Cross-entity logic gets its own service,
// for example permission.service.ts.
```
### Error Handling Pattern
```typescript
// Transport layer: throw a typed error with the right code.
throw new TRPCError({ code: 'NOT_FOUND', message: 'Document not found' });
// Service layer: return a Result and let the caller decide.
export async function moveDocument(
docId: string,
targetFolderId: string,
): Promise<Result<Document>> {
const folder = await folderService.getById(targetFolderId);
if (!folder) return { success: false, error: 'Target folder not found' };
const [updated] = await db
.update(documents)
.set({ folderId: targetFolderId, updatedAt: new Date() })
.where(eq(documents.id, docId))
.returning();
return { success: true, data: updated };
}
// UI layer: error boundaries and toasts. Never surface a raw error string.
```
## Anti-Patterns to Avoid
Negative constraints are worth writing down. An agent will not infer that something is forbidden just because no example does it.
```typescript
// DO NOT fetch in a client component when a server component could.
// Bad:
'use client';
export function DocumentList() {
const { data } = trpc.document.list.useQuery();
return <ul>{data?.map(d => <li key={d.id}>{d.name}</li>)}</ul>;
}
// Good: fetch in the parent server component and pass the data down.
// DO NOT put business logic in a router.
// Bad: multi-step transforms and validation inside the procedure body.
// Good: thin routers that delegate to a service.
// DO NOT write raw SQL strings. Use the query builder so types stay honest.
// DO NOT skip a permission check. Every read and every write verifies access.
// Bad: return documentService.getById(input.id);
// Good: const doc = await documentService.getById(input.id);
// await ctx.permissions.assertCanView(ctx.session.userId, doc);
// DO NOT import server-only modules into a client component.
// Bad: import { db } from '@/server/db'; // inside a 'use client' file
// Good: call through tRPC.
// DO NOT add a dependency to solve a ten-line problem. Ask first.
// DO NOT edit generated files: drizzle migrations already applied,
// lockfiles, or anything under a directory marked generated.
```
## Testing Strategy
```text
tests/
unit/ Fast and isolated
services/ Service functions with the database layer mocked
utils/ Utility functions
validators/ Schema validation
integration/ Real database, real router
routers/ Procedure-level tests
services/ Service tests against a test database
e2e/ Browser tests
auth.spec.ts
documents.spec.ts
sharing.spec.ts
```
```bash
npm run verify # lint + types + unit tests: run this before saying "done"
npm test # unit tests only
npm run test:integration # needs a running PostgreSQL
npm run test:e2e # Playwright
npm run test:coverage # coverage report
```
```typescript
// Unit test: mock the database boundary, not the function under test.
import { describe, it, expect, vi } from 'vitest';
import * as documentService from '@/server/services/document.service';
vi.mock('@/server/db', () => ({
db: { select: vi.fn(), insert: vi.fn(), update: vi.fn(), delete: vi.fn() },
}));
describe('documentService.create', () => {
it('creates a document with the default status', async () => {
// Arrange, Act, Assert
});
it('rejects files over the size limit', async () => {
// Validation path
});
});
// E2E test: assert on roles and text, not CSS classes.
import { test, expect } from '@playwright/test';
test('a user can upload and view a document', async ({ page }) => {
await page.goto('/documents');
await page.getByRole('button', { name: 'Upload' }).click();
// ...
});
```
New behaviour ships with a test. A bug fix ships with the test that would have caught it. A change that only touches types still has to pass `npm run verify`.
## Agent Guidelines
### When Writing New Code
- Read a similar existing file first and match it. Consistency beats your preferred style.
- Use the ORM query builder for every database operation.
- Add a Zod schema in `src/lib/validators.ts` for every procedure input.
- Server Components by default.
- Add the permission check before returning any record.
- Prefer editing an existing file over creating a new one.
### When Refactoring
- Run `npm run verify` after every change. Zero type errors is the bar, not a goal.
- Preserve existing coverage. If behaviour changes, change the test in the same commit and say so.
- Keep the service layer framework-agnostic.
- If you split a file, update every import in the same change - a half-moved module is worse than an unmoved one.
### Common Gotchas
- **ORM identity**: this project uses Drizzle. The Prisma API is different and the two do not mix; check `package.json` before writing query code.
- **Router style**: App Router only. There is no `pages/` directory and no `getServerSideProps`.
- **Client hooks**: the procedure hook is `trpc.router.procedure.useQuery()`, not a single top-level hook.
- **Vendored UI**: the primitives under `src/components/ui/` are copied into the repo, not installed from a package. Edit them directly; do not try to override them through a theme.
- **File uploads** always go through `storage.service.ts`, which issues presigned URLs. A procedure must never accept file bytes.
- **Timestamps** are stored as UTC. Converting for display is the client's job.
### Confirm Before Implementing
Ask these out loud in your plan rather than guessing. A wrong assumption here costs more than the question does:
- Is there an existing service function that already does this?
- Does this path need a permission check, and at which access level?
- Server component or client component?
- Which Zod schema validates this input, and does it already exist?
- Does this change the database schema? If so, where is the migration and is it reversible?
- Is there a closely similar pattern already in the codebase to match?
- Does this need a new dependency, and has anyone approved it?
Why Markdown Matters for AI-Native Development
AGENTS.md for Your Repository
AGENTS.md is the file coding agents look for first, and it is plain markdown with no frontmatter and no schema to learn. Put your project layout, build commands, conventions, and integration points there and Claude Code, Codex CLI, Cursor, Copilot, Gemini CLI, Aider, and Windsurf all read the same brief. CodeAI.md is a filled-in starting point for that file.
Nearest-File-Wins in a Monorepo
One root file cannot describe a repository where the Go service, the React app, and the batch jobs each have their own rules. Precedence is nearest-file-wins, so a package-level file overrides the root for anything beneath it. State the shared truth once at the top and let each package correct only what differs.
MCP Servers for Live Codebase Context
Some context should never be copied into a document because it changes hourly. The Model Context Protocol lets a tool query the live source instead - your database catalog, your issue tracker, your build results - through one interface, with thousands of public servers already available. Record which servers your project expects and what each one is allowed to reach.
"A coding agent is only as good as its briefing. CodeAI.md is the briefing: layout, conventions, seams, and the things that break loudly when you get them wrong, written once and read by every tool your team happens to use."
Frequently Asked Questions
What is CodeAI.md?
CodeAI.md is a platform for structuring your codebase knowledge in markdown so AI coding assistants understand your architecture, patterns, and conventions. It transforms tribal knowledge into permanent, AI-consumable context.
How does CodeAI.md improve AI code generation?
By documenting your file structure, naming conventions, and integration patterns in a structured format, CodeAI.md gives AI assistants the context they need to generate code that matches your team's standards from the first attempt.
What should I include in a CodeAI.md file?
Include your directory layout, naming conventions, error handling patterns, testing standards, integration points, and anti-patterns. The more specific your context, the better your AI assistant's output will be.
Does CodeAI.md work with all AI coding assistants?
Yes, CodeAI.md templates use standard markdown that any AI coding assistant can consume. Tools like Claude, Copilot, and Cursor all benefit from structured codebase context in your repository.
How often should I update my CodeAI.md file?
Update your CodeAI.md whenever architecture patterns change. Stale context actively misleads AI assistants. Many teams add context updates to their definition of done for architectural changes.
Can CodeAI.md help with onboarding new developers?
Absolutely. The same structured context that helps AI assistants understand your codebase helps new developers absorb your patterns quickly. One document serves both audiences effectively.
Is CodeAI.md free?
Yes, CodeAI.md templates are completely free to copy or download. Place the file in your repository root and start benefiting immediately.
Explore More Templates
About CodeAI.md
Our Mission
CodeAI.md is an RJL project, maintained alongside the codebases it was originally written for.
Most disappointing agent output traces back to a missing sentence rather than a missing capability. Nothing said the project uses a custom error class, or that the API layer never talks to the database directly. Writing those sentences down once removes an entire category of rework, and the file keeps paying out every time a new tool joins the stack.
The other half of the job is knowing what does not belong in a file at all. Anything that changes faster than you can review a pull request - schema, ticket state, build health - should be reached live through a server rather than pasted into a document that will be wrong by Thursday. CodeAI.md tries to make that split explicit instead of accidental.
Why Markdown Matters
AI-Native
LLMs parse markdown better than any other format. Fewer tokens, cleaner structure, better results.
Version Control
Context evolves with code. Git tracks changes, PRs enable review, history preserves decisions.
Human Readable
No special tools needed. Plain text that works everywhere. Documentation humans actually read.
Questions about structuring a repository so agents get it right the first time? Send them along.