Skip to content

Using with LLMs

The MCP server only helps if the agent calls it before shipping a database change, and agents often don’t — they decide an edit isn’t “database work” and skip it. Adding one column to a SELECT can turn a fast query into a full table scan, and by the time CI catches it you’re a push and a pipeline run behind.

The hooks below make an agent check its data-layer edits locally. Examples are for Claude Code; adapt the triggers to your host.

Two things make edits slip past an agent’s own judgement:

  • Nothing forces a check on the data layer. UI edits often sit behind a review step; a query or migration edit usually fires nothing.
  • ORM queries aren’t in a db/ folder. They sit inline in route and service files, so a path-based guard misses them. Match on what the edit contains.

Register the MCP server and install the companion skill. The skill shifts the agent’s default toward using the tools; the hooks are the backup for when it doesn’t.

Guardrail 1 — a reminder hook that reads content

Section titled “Guardrail 1 — a reminder hook that reads content”

A PostToolUse hook that inspects what was written and, if it looks like database code, reminds the agent to validate it. Create .claude/hooks/qd-db-nudge.sh:

#!/bin/bash
# Nudge the agent to validate data-layer edits against the Query Doctor MCP.
# Content-based: fires on inline ORM/SQL calls, not just files under a db/ folder.
file_path=$(jq -r '.tool_input.file_path // ""')
# Reconstruct the text that was written, regardless of the edit tool used.
content=$(jq -r '
.tool_input.content
// .tool_input.new_string
// ([.tool_input.edits[]?.new_string] | join("\n"))
// ""
')
# Only consider source files.
case "$file_path" in
*.ts | *.tsx | *.js | *.jsx | *.sql | *.py | *.rb | *.go | *.cs) ;;
*) exit 0 ;;
esac
# Content signals: ORM query builders, raw SQL tags, and migration/schema paths.
query_signal='\.select\(|\.insert\(|\.update\(|\.delete\(|\bdb\.|sql`|CREATE (TABLE|INDEX)|ALTER TABLE'
path_signal='migration|schema|/db/'
if echo "$content" | grep -qiE "$query_signal" \
|| echo "$file_path" | grep -qiE "$path_signal"; then
cat >&2 <<'EOF'
Query Doctor: this edit touches the data layer. Before you finish, validate it
against the Query Doctor MCP:
- analyze_query / explain_query — check the plan for seq scans and bad sorts
- recommend_indexes — confirm supporting indexes exist for ORDER BY / WHERE / JOIN
Even projection-only changes (adding a column to a SELECT) can change the plan.
After the change lands in CI, check the latest CI run before calling it done.
EOF
exit 2
fi
exit 0

Register it in .claude/settings.json:

{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/qd-db-nudge.sh"
}
]
}
]
}
}

chmod +x the script and restart the agent. Exit code 2 surfaces the message to the agent; it doesn’t block the edit.

Tune the signals to over-fire. A false alarm costs one cheap analyze_query call; a miss costs a CI round-trip.

Guardrail 2 — lock down migration folders

Section titled “Guardrail 2 — lock down migration folders”

Agents should regenerate migrations through the ORM, not hand-write the SQL — a hand-edited migration breaks the ORM’s snapshot chain. Block direct edits with a PreToolUse hook (.claude/hooks/block-manual-migrations.sh, Drizzle shown):

#!/bin/bash
file_path=$(jq -r '.tool_input.file_path // ""')
if echo "$file_path" | grep -qE 'drizzle/'; then
echo "Blocked: do not hand-edit generated migrations. Modify the schema, then run your ORM's generate step (e.g. drizzle-kit generate)." >&2
exit 2
fi
exit 0
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/block-manual-migrations.sh"
}
]
}
]
}
}

Point it at wherever your ORM writes migrations.

Guardrail 3 — a final check before “done”

Section titled “Guardrail 3 — a final check before “done””

A backstop for when the per-edit reminder is missed: scan the whole diff at the end of a task. Add this to a completion checklist or /finish command:

Terminal window
# Run at the end of a task, before declaring the work done.
changed=$(git diff --name-only origin/staging... )
if echo "$changed" | grep -qE '\.(ts|tsx|js|jsx|sql|py|rb|go|cs)$'; then
# Look at the actual added lines, not just filenames — inline queries count.
if git diff origin/staging... \
| grep -E '^\+' \
| grep -qiE '\.select\(|\.insert\(|\.update\(|\.delete\(|\bdb\.|sql`|CREATE (TABLE|INDEX)|ALTER TABLE'; then
echo "Data-layer changes detected in this diff."
echo "Validate them with the Query Doctor MCP (analyze_query / recommend_indexes)"
echo "and check the latest CI run before declaring the work complete."
fi
fi