The agent doesn't touch my Postgres until it has read the schema
How I wired Claude Code to reason about the database first, scan for secrets before every write, and refuse to merge an endpoint without an integration test.
I have a rule that predates any of this AI stuff: the first thing you do before changing a backend is read what is already there. The schema, the constraints, the migration history. Most outages I have cleaned up came from somebody writing a handler against a mental model of the table that was three months out of date. So when I started letting Claude Code into our NestJS services, I did not give it a blank cheque. I gave it the same discipline I would expect from a junior who wants to keep their access.
This is the setup I actually run, day to day, on a payments-adjacent API. Model is Claude Opus 4.8. Four MCP servers, three subagents, three hooks. Nothing exotic. The point was never to look clever. The point was to make the agent fast where it is safe and slow where it can hurt me.
The shape of it
| Piece | What I run | Why |
|---|---|---|
| Model | Claude Opus 4.8 | I want the long-horizon reasoning for schema work, not the cheapest token |
| MCP | postgres, github, sentry, filesystem | Read the DB, the PRs, the live errors, the tree |
| Subagents | schema-designer, implementer, security-reviewer | Split planning, writing, and auditing into separate contexts |
| Hooks | secret-scan, migration dry-run, contract tests | Block the three ways I have actually been burned |
The Postgres MCP is the one that earns its keep. Without it the agent guesses at column types and writes handlers that compile and then explode on the first real row. With it, the agent runs \d+ orders and reads the actual constraints before it writes a line. That sounds small. It is the difference between a green CI and a 2am page.
CLAUDE.md: the non-negotiables
My CLAUDE.md is short on purpose. A long memory file is a memory file nobody reads, including the model. I keep it to the three things I am not willing to argue about on a PR.
# API service rules
## Boundaries
- Validate every input at the boundary with a DTO + class-validator. No "trust me" payloads.
- No raw SQL without parameterized queries. If you reach for string concat on a query, stop.
- Every endpoint ships with an integration test that hits the real router, not a mocked service.
## Database
- Read the schema before writing handlers. Use the postgres MCP, run \d on the table.
- Migrations are forward-only. No editing an already-applied migration.
- Never DROP or TRUNCATE in a migration the agent authored. Flag it for a human.
## Style
- NestJS conventions: module, controller, service, no logic in controllers.
- Errors throw typed exceptions, never bare strings.
- If you are unsure about a foreign key, ask. Do not invent one.The schema-designer subagent
This is the part people skip and then wonder why the agent writes garbage migrations. The schema-designer runs in its own context, with read access to Postgres and nothing that can write. Its whole job is to come back with a plan: tables, columns, indexes, the migration order. The implementer never gets to design schema. It gets handed one.
---
name: schema-designer
description: Plans database schema changes. Reads the live schema, proposes migrations, never writes application code.
tools: Read, Grep, mcp__postgres__query
model: opus
---
You design Postgres schema changes for a payments-adjacent API.
Before proposing anything:
1. Inspect the current schema for every table you intend to touch.
2. Check existing indexes and foreign keys. Do not duplicate them.
3. Confirm nullability and defaults against real data shape.
Output a plan, not code:
- The DDL for each migration, in order.
- A one-line rollback note per migration.
- Any data backfill that the migration assumes.
Hard rules:
- Forward-only migrations.
- No DROP / TRUNCATE without an explicit human-approval flag.
- Money columns are NUMERIC, never float. If you see a float column for
currency, call it out as a bug.Splitting it out like this matters more than the prompt wording. When the implementer is also trying to design the database, both jobs get worse. The plan gets sloppy because it is racing to the code, and the code gets sloppy because the plan was an afterthought. Two contexts, two jobs.
12:36Hooks: the parts I don't trust the prompt to enforce
Rules in CLAUDE.md are suggestions. The model usually follows them. Usually is not good enough when the thing on the line is a credentials leak or a destructive migration. So the three things I genuinely care about are not rules, they are hooks. Code runs, deterministically, whether the model feels like it or not.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "scripts/secret-scan.sh \"$CLAUDE_FILE_PATH\""
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "scripts/migrations-dry-run.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "pnpm test:contract"
}
]
}
]
}
}The secret-scan hook is a thirty-line shell script wrapping gitleaks against the file about to be written. If it finds an AWS key or a DB URL with a password baked in, it exits non-zero and the write never lands. The migration dry-run applies pending migrations against a throwaway database and rolls back. If the DDL would fail, I find out before it touches anything real. The Stop hook runs contract tests so the session cannot end on a green vibe and a red suite.
What a real session looks like
What I would not do
- I do not give the implementer write access to Postgres. It writes migrations, a human or the dry-run hook applies them.
- I do not skip the contract tests on the Stop hook to go faster. The whole point is that the session cannot lie to me.
- I do not let the agent edit an already-applied migration. Forward-only, every time. Editing history is how you desync staging and prod.
- I do not pile rules into CLAUDE.md hoping more text means more safety. The hooks are the safety. The text is etiquette.
The honest tradeoff: this setup is slower per task than just letting the model rip. The schema-designer adds a round trip. The reviewer adds another. The hooks add seconds. But the pass rate is around 90 percent on my work, and the failures it does have are loud and early, not silent and in prod. I will take a slower green over a fast red every single day.
Worth reading
Create custom subagents - Claude Code DocsThe official spec for subagent frontmatter, tool allowlists, and isolated context. Read this before you split your agents.code.claude.comdisler/claude-code-hooks-masteryCovers every hook lifecycle event with working examples. Where I cribbed the structure of my secret-scan and dry-run hooks.GitHub4.1kIf you run a backend with a database you care about, this is the safest way I have found to hand an agent real access without losing sleep. Pull the whole thing, hooks and subagents included, with npx setuproll add claude-code-backend-api, then trim my paranoia down to your own list of incidents. Keep the dry-run. Trust me on the dry-run.