When our team started using AI coding agents daily, the same mistakes repeated across people and sessions. The agent ran npm test when our suite needs pnpm test:unit, edited files under generated/ that are overwritten on build, formatted code with the wrong style, and once read a local .env into its context while "looking for configuration". Each person fixed it with their own prompt habits, and none of that knowledge was shared.
Agents are new contributors who join every session with no memory. The repo already has a place for knowledge every contributor needs: the repo itself. This post covers the files and settings we added so any agent, and any teammate, gets the same context and guard rails. Our examples use Claude Code because that is what most of the team runs, but the principles carry to other agents.
Principle: make the repo legible before making it clever
Before writing any agent-specific file, we fixed what confuses humans too:
- One command per task.
make test,make lint,make dev. If running tests requires three environment variables and a running container, the agent will guess wrong. - Fast, scoped commands. A way to run one test file or one package. Agents iterate in small loops; a twelve-minute suite means they stop verifying.
- Clear generated-code markers. A header comment such as
// Code generated by ... DO NOT EDIT.plus a folder convention. - Accurate README and architecture notes. Agents read them literally. A stale README is worse than none.
The instruction file
Claude Code loads CLAUDE.md from the project root at session start. Many other agents read AGENTS.md, an emerging cross-tool convention. We keep the real content in one file and point the other at it, so there is a single source of truth.
What goes in, and what we leave out:
# Project notes for coding agents
## Commands
- Install: `pnpm install`
- Unit tests (fast): `pnpm test:unit -- path/to/file.test.ts`
- Full checks before finishing: `make check`
- Never run `pnpm db:reset` (drops the shared dev database)
## Layout
- `apps/api` Fastify service, `apps/web` Next.js app, `packages/shared` types
- `generated/` is produced by `make codegen`; edit the `.proto` files instead
## Conventions
- TypeScript strict; no `any` without a comment explaining why
- Money is integer minor units (`amountCents`), never floats
- New API routes need a contract test in `apps/api/test/contracts`
## More detail
- Architecture: @docs/architecture.md
- Database rules: @docs/database.md
Rules we follow for this file:
- Short. Under about 150 lines. It is loaded into every session, so every line costs context and attention.
- Specific and checkable. "Write clean code" changes nothing. "Money is integer minor units" prevents a real bug.
- Imports for depth. The
@pathsyntax pulls in another file, so long architecture notes live indocs/where humans read them too. - Reviewed like code. It goes through pull requests. When an agent makes a mistake twice, we add a line; when a line stops being true, we delete it.
There are also personal layers: ~/.claude/CLAUDE.md for preferences that apply across all your projects, and nested CLAUDE.md files inside subdirectories that load when the agent works there. We keep personal style preferences out of the shared file.
Permissions: allow the boring, deny the dangerous
.claude/settings.json is checked in and shared; .claude/settings.local.json is for personal overrides and is git-ignored. Permission rules let routine commands run without prompts and block reads or commands we never want:
{
"permissions": {
"allow": [
"Bash(pnpm test:unit:*)",
"Bash(pnpm lint:*)",
"Bash(make check)",
"Bash(git diff:*)",
"Bash(git status)"
],
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)",
"Bash(pnpm db:reset:*)",
"Bash(git push:*)"
]
}
}
Deny rules are a guard rail, not a sandbox. Secrets should still not be on disk in plain text where avoidable, and anything high stakes belongs behind real access controls. But the rules removed the most common accidental exposure and turned the dangerous command into a hard stop instead of a hope.
Hooks: deterministic steps the model cannot skip
Instructions are suggestions to a model. Hooks are shell commands the agent runtime executes at defined events, so they always run. We use two.
A PostToolUse hook formats any file the agent edits, so style never needs to be mentioned in the instruction file:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format.sh" }
]
}
],
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-generated.sh" }
]
}
]
}
}
The hook receives the tool call as JSON on stdin:
#!/usr/bin/env bash
# .claude/hooks/format.sh
set -euo pipefail
file=$(jq -r '.tool_input.file_path // empty')
case "$file" in
*.ts|*.tsx|*.js|*.json|*.md) pnpm exec prettier --write "$file" >/dev/null ;;
esac
exit 0
A PreToolUse hook blocks edits to generated code. Exiting with status 2 blocks the call and sends the stderr message back to the agent, so it learns why and edits the source instead:
#!/usr/bin/env bash
# .claude/hooks/protect-generated.sh
set -euo pipefail
file=$(jq -r '.tool_input.file_path // empty')
if [[ "$file" == *"/generated/"* ]]; then
echo "generated/ is build output. Edit the .proto source and run make codegen." >&2
exit 2
fi
exit 0
Keep hooks fast and quiet. A slow hook runs after every edit and adds up quickly.
Project MCP config
Some context lives outside the repo: the issue tracker, the database schema, internal docs. A .mcp.json file at the root declares project-scoped MCP servers so everyone gets the same tools:
{
"mcpServers": {
"dev-db": {
"command": "uv",
"args": ["--directory", "tools/db-mcp", "run", "server.py"],
"env": { "DATABASE_URL": "${DEV_DATABASE_URL}" }
}
}
}
Two rules: reference secrets through environment variable expansion, never inline, and point database servers at a read-only role on a dev or replica database. Claude Code asks each user to approve project-scoped servers the first time, which is the right default for a file anyone can edit through a pull request.
Reusable commands and subagents
Repeated multi-step requests become files under .claude/commands/, invoked as slash commands. Ours include /fix-issue, which reads an issue number, finds related code, writes a failing test first, then fixes it. Specialised helpers go under .claude/agents/, for example a reviewer with read-only tools that checks a diff against the conventions. Both are plain Markdown with front matter and live in version control.
What changed after a month
| Before | After |
|---|---|
| Agents guessed test commands | One documented command, allowed without prompts |
| Edits to generated files reverted on build | Pre-edit hook blocks them with an explanation |
| Style fixes in every review | Formatter hook runs after each edit |
| Personal prompt tricks | Shared, reviewed instruction file |
| Risky reads of local secrets | Deny rules plus secrets moved to a manager |
Key takeaways
- Fix repo legibility first: single commands, scoped tests, clear generated-code boundaries.
- Keep the instruction file short, specific and reviewed; import longer docs instead of pasting them.
- Use permission rules to allow routine commands and deny sensitive reads and destructive ones.
- Put must-always-happen behaviour in hooks, not in prose instructions.
- Share external context through a checked-in MCP config with env-var secrets and read-only credentials.
References
- Claude Code memory and CLAUDE.md: https://docs.claude.com/en/docs/claude-code/memory
- Claude Code settings and permissions: https://docs.claude.com/en/docs/claude-code/settings
- Claude Code hooks reference: https://docs.claude.com/en/docs/claude-code/hooks
- Claude Code MCP: https://docs.claude.com/en/docs/claude-code/mcp
- AGENTS.md: https://agents.md/
Comments
0No comments yet. Be the first to comment!
Sign In to post a comment.