Compatible with PostgreSQL, Supabase, Neon, AWS RDS/Aurora, Cloud SQL, TimescaleDB & CockroachDB. Whether the migration was written by you, a teammate, or an AI coding agent.
npx mgcheckQuickstart Β· How it works Β· Safety rules Β· Showcase Β· AI agents Β· CI Β· Configuration Β· API Β· Security Β· Roadmap Β· Contributing
Free and open source (MIT). Migration Guardian is an early-stage, community-driven project. If you've ever been paged because a migration locked a table, you already have the domain knowledge to help β new rules, framework detectors and fixes are very welcome.
You can roll back application code in seconds. A bad schema migration can take your database down in milliseconds β and it usually looks completely harmless in review.
-- Looks fine. Passes local tests on 10 rows. Gets approved in review.
CREATE INDEX idx_users_email ON users(email);
-- In production:
-- β’ Takes a SHARE lock on "users" β every INSERT/UPDATE/DELETE blocks
-- β’ Requests queue behind the lock, the connection pool saturates
-- β’ Your API starts timing out while the index buildsThis class of bug is getting more common, not less. AI coding agents (Claude Code, Cursor, Copilot, Windsurf, β¦) are great at writing the DDL that produces the schema you asked for, but they have no idea how big your tables are or how PostgreSQL locks behave under load.
Migration Guardian sits between whoever writes the migration and the database that runs it. It parses every statement with the real PostgreSQL grammar, flags dangerous lock and rewrite patterns, dry-runs the migration on a throwaway shadow database, and makes a human sign off on anything risky before it reaches a real database.
| π¬ Real PostgreSQL parser | Uses pgsql-parser β PostgreSQL's own grammar compiled to WebAssembly β so rules inspect real AST nodes rather than guessing from text. Statements the parser can't read are flagged (MG014) and checked by text matching. |
| π§ͺ Shadow database dry-run | Executes the migration on an in-process PGlite instance (no Docker) or a disposable postgres:16-alpine container, then throws it away. |
| π¦ Risk-based approval gate | Every migration is classified π’ LOW / π‘ MEDIUM / π΄ HIGH. HIGH risk is never auto-approved, under any configuration. |
| π€ MCP server for AI agents | One command wires check_migration, analyze_migration and apply_migration tools into 13 AI clients. |
| ποΈ Live terminal watcher | Approve, reject or override agent migrations in real time. Switch auto β manual mode with Shift+Tab. |
| π Agent retry loop | When a fixable violation is found, the agent gets the suggested safe SQL and is asked to rewrite and retry (up to 3 attempts). |
| π οΈ Auto-fix | Rewrites common unsafe patterns into zero-downtime equivalents, shows a diff, and re-verifies the fix on a shadow DB. |
| πͺ Pre-commit hook & CI | Block unsafe migrations at git commit and in pull requests (bundled GitHub Action included). |
| π Audit log | Every intercepted migration, its risk level and the human decision are appended to .mgcheck/audit.jsonl. |
| π§© Framework-aware | Auto-detects Prisma, Drizzle, Supabase, Flyway, Goose, dbmate, golang-migrate and plain .sql. |
Requirements: Node.js 18+. Docker is optional β use
--provider pgliteto run everything in-process.
| Method | Command | Usage |
|---|---|---|
| No install (npx) (Recommended) | None needed | npx mgcheck <command> |
| Global install (npm) | npm install -g mgcheck |
mgcheck <command> |
| Project devDependency | npm install -D mgcheck |
npx mgcheck <command> or npm script |
1. Connect your AI agents and start the watcher
npx mgcheckThe setup wizard detects installed AI clients, registers the MCP server, and opens the live approval watcher.
2. Lint migrations statically (no database needed)
npx mgcheck analyze ./migrations3. Lint + dry-run on a shadow database
npx mgcheck run ./migrations --provider pglite --seed ./schema.sql4. Auto-fix an unsafe migration
npx mgcheck fix ./migrations/20260101_add_email_index.sql5. Guard every commit
npx mgcheck hook install Developer / AI coding agent
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β Detect Prisma Β· Drizzle Β· Supabase Β· Flyway Β· Goose Β· β
β dbmate Β· golang-migrate Β· raw .sql β
ββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β‘ Analyze PostgreSQL AST (WASM) checked against 14 rules β
β β locks, table rewrites, data loss, rollbacks β
ββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β’ Shadow run Execute on throwaway PGlite or Docker Postgres β
β (optionally seeded with your existing schema) β
ββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β£ Gate π’ LOW β approved β
β π‘ MEDIUM β y/N prompt (or agent retry, auto) β
β π΄ HIGH β blocked unless human types β
β "override" β
ββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β€ Apply Optional: run verified SQL on the target DB β
β Every decision β .mgcheck/audit.jsonl β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
error rules block the migration (π΄ HIGH risk). warning rules are reported and require approval (π‘ MEDIUM risk).
| Rule | Severity | Flags | Why it's dangerous | Safe pattern | Auto-fix |
|---|---|---|---|---|---|
MG001 |
error | CREATE INDEX without CONCURRENTLY |
Blocks all writes to the table for the whole index build. | CREATE INDEX CONCURRENTLY |
β |
MG002 |
error | ALTER COLUMN ... TYPE |
Full table rewrite under an ACCESS EXCLUSIVE lock. |
Expand/contract: add new column, backfill, switch, drop. | β |
MG003 |
error | ADD COLUMN ... NOT NULL without DEFAULT |
Fails on tables with existing rows. | Add with a DEFAULT, or add nullable β backfill β SET NOT NULL. |
β |
MG004 |
warning | ADD CONSTRAINT without NOT VALID |
Scans every row while holding a restrictive lock. | ADD CONSTRAINT ... NOT VALID, then VALIDATE CONSTRAINT. |
β |
MG005 |
error | DROP COLUMN |
Irreversible data loss; breaks code still reading the column. | Deprecate first; requires --confirm-destructive. |
β |
MG006 |
error | DROP TABLE |
Irreversible data loss and immediate downtime. | Requires --confirm-destructive. |
β |
MG007 |
warning | RENAME COLUMN |
Breaks queries from app instances still on the old code. | Add new column, dual-write, backfill, drop old. | β |
MG008 |
warning | RENAME TABLE |
Breaks running queries during rolling deploys. | Rename, then create a view with the old name during the transition. | β |
MG009 |
warning | DDL without lock_timeout |
A blocked DDL statement queues every query behind it. | SET lock_timeout = '5s'; before DDL. |
β |
MG010 |
warning | No down / rollback migration | No clean way back if the deploy goes wrong. | Ship a matching rollback (e.g. .down.sql). |
β |
MG011 |
error | TRUNCATE |
Instantly deletes every row (and with CASCADE, rows in referencing tables). |
Delete specific rows in batches; requires --confirm-destructive. |
β |
MG012 |
error | DROP SCHEMA |
Deletes the schema and, with CASCADE, everything in it. |
Drop objects individually after review; requires --confirm-destructive. |
β |
MG013 |
error | DROP DATABASE |
Deletes an entire database. | Never in a migration; requires --confirm-destructive. |
β |
MG014 |
warning | SQL the parser can't read | Usually a typo that fails at deploy time; the other rules can only text-match it. | Fix the syntax, or verify with mgcheck run. Set to error to block. |
β |
Any rule can be re-leveled or disabled per project β see Configuration.
Example: what mgcheck fix does
Input:
CREATE INDEX idx_users_email ON users(email);
ALTER TABLE users ADD COLUMN is_active boolean NOT NULL;Rewritten (and re-verified on a shadow DB before you're asked to accept it):
SET lock_timeout = '5s';
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email ON users(email);
ALTER TABLE users ADD COLUMN is_active boolean DEFAULT false NOT NULL;Destructive statements (DROP TABLE, DROP COLUMN, TRUNCATE, DROP SCHEMA, DROP DATABASE) are never rewritten automatically.
Migration Guardian ships an MCP server so AI agents check their own migrations before running them β and so you stay in control of what reaches the database.
| Claude Code | Claude Desktop | Cursor | Windsurf |
| Cline | Roo Code | Continue.dev | Zed |
| Goose | Aider | OpenHands | VS Code + GitHub Copilot |
| Google Antigravity |
Target a single client, or also write workspace instruction files (CLAUDE.md, AGENTS.md, .cursorrules, .windsurfrules, .cursor/rules/, .github/copilot-instructions.md):
npx mgcheck setup --client cursor
npx mgcheck setup --client all --with-rules --yesFor any other MCP-compatible client:
{
"mcpServers": {
"migration-guardian": {
"command": "npx",
"args": ["-y", "mgcheck", "mcp"]
}
}
}| Tool | What it does |
|---|---|
check_migration |
AST analysis and shadow-DB execution (pglite by default). Verifies only; it applies to the target database only when called with auto_apply: true. |
analyze_migration |
Fast static analysis only β no database required. |
apply_migration |
Applies verified SQL to the real target database. The agent must pass confirm_user_approved: true; π’ LOW (and π‘ MEDIUM in auto mode) then run without a watcher prompt, anything else needs approval in the watcher. Target URL falls back to DATABASE_URL / TARGET_DATABASE_URL. |
npx mgcheck (or mgcheck setup) leaves a watcher running in your terminal. Requests from every project and editor stream into it:
ποΈ Guardian Active β Watching for AI migration activity...
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β manual mode (shift+tab to cycle) Β· type /auto or /manual
π Approval Request [HIGH RISK]
Client: Cursor (my-web-app)
Query: DROP TABLE "legacy_users";
Violations:
β’ [MG006] DROP TABLE is destructive and causes permanent data loss
Type "override" to approve this query or press Enter to reject: _
Switch approval mode on the fly with Shift+Tab / Shift+Enter, or type /auto or /manual.
| Risk | manual (default) |
auto |
|---|---|---|
| π’ LOW β no violations | Approved | Approved |
| π‘ MEDIUM β warnings only | Human confirms [y/N] |
Agent is asked to rewrite with the suggested safe SQL, then approved |
| π΄ HIGH β any error | Blocked β human must type override |
Blocked β human must type override |
Important
high can never be added to autoApprove. Config that tries to do so is rejected with an error.
npx mgcheck mode # show active mode and where it came from
npx mgcheck mode auto # set globally (~/.mgcheck/settings.json)
npx mgcheck mode manual --project # set per project (.mgcheckrc.json)
npx mgcheck mode auto --levels low # auto mode, but only auto-approve LOWResolution order: MGCHECK_MODE env var β project config β global settings β manual.
π‘ Tip: Every command below can be run without install via
npx mgcheck <command>. If installed globally (npm install -g mgcheck), you can usemgcheck <command>directly.
| Command | Description |
|---|---|
npx mgcheck Β· npx mgcheck setup |
Detect AI clients, register the MCP server, launch the watcher. Aliases: init, install-mcp. |
npx mgcheck analyze <path> |
Static AST analysis against the safety rules. No database needed. |
npx mgcheck run <path> |
Analysis + shadow-DB execution, with optional apply to a target DB. |
npx mgcheck fix <path> |
Rewrite unsafe SQL into zero-downtime patterns and verify on a shadow DB. |
npx mgcheck mode [auto|manual] |
View or change approval mode (--project, --levels). |
npx mgcheck log Β· npx mgcheck audit |
Show the audit log (--all, --risk <level>, --tail, --json, --global). |
npx mgcheck hook [install|uninstall|run] |
Manage the Git pre-commit hook. run checks staged .sql files. |
npx mgcheck mcp |
Start the MCP server over stdio (normally launched by your editor). |
npx mgcheck uninstall |
Remove mgcheck from all AI clients, rule files and hooks (--purge also deletes audit logs). |
npx mgcheck run options
| Flag | Default | Description |
|---|---|---|
--provider <docker|pglite> |
docker |
Shadow database engine. pglite needs no Docker. |
--seed <path> |
β | Base schema SQL applied before the migration. |
--db-url <url> |
β | Use an existing Postgres as the shadow DB instead of spinning one up. |
--docker-image <image> |
postgres:16-alpine |
Image for the Docker provider. |
--target-db-url <url> |
β | Real database to apply to after verification passes. |
--apply |
false |
Apply to the target without prompting once verification passes. |
--fix |
false |
On failure, try an auto-fix and prompt with the verified diff. |
--confirm-destructive |
false |
Allow DROP TABLE / DROP COLUMN. |
--format <terminal|json|markdown> |
terminal |
Output format. |
--ci |
false |
JSON output, no prompts, exit code only. |
--verbose |
false |
Print progress logs. |
--config <path> |
β | Explicit config file. |
analyze supports --format, --ci, --confirm-destructive, --verbose and --config. fix supports --seed, --provider (default pglite) and --config.
| Code | Meaning |
|---|---|
0 |
Passed, no violations |
1 |
Failed β blocking violation or shadow execution error |
2 |
Passed with warnings |
3 |
Tool error (bad path, invalid config, β¦) |
Compatible with standard PostgreSQL and all major Postgres-wire and compatible cloud engines:
| Platform / Database | Status | Notes |
|---|---|---|
| PostgreSQL (12β17) | β Native | Full AST rule engine + PGlite & Docker shadow testing |
| Supabase | β Native | Auto-detects supabase/migrations + shadow validation |
| Neon | β Native | Serverless branching & zero-downtime DDL |
| AWS Aurora & RDS (Postgres) | β Native | Verifies zero-downtime lock thresholds |
| Google Cloud SQL & AlloyDB | β Native | Standard PostgreSQL grammar & lock safety |
| TimescaleDB | β Native | PostgreSQL hypertable and continuous aggregate DDL |
| CockroachDB & YugabyteDB | β Compatible | PostgreSQL dialect and AST parsing |
| MySQL, SQLite & ClickHouse | πΊοΈ Roadmap | In active development (help wanted) |
Point npx mgcheck at any migration directory or file β the format is detected automatically:
| Framework | Detected by |
|---|---|
| Prisma | migration.sql files in prisma/migrations/*/ |
| Drizzle | meta/_journal.json next to .sql files |
| Supabase | supabase/migrations/ layout |
| Flyway | V*__*.sql / U*__*.sql |
| golang-migrate | *.up.sql / *.down.sql pairs |
| Goose | -- +goose directives |
| dbmate | -- migrate: directives |
| Raw SQL | Any .sql file or directory of .sql files |
name: Migration safety
on:
pull_request:
paths: ['migrations/**', 'prisma/migrations/**', 'drizzle/**', 'supabase/migrations/**']
jobs:
mgcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Aditya31032006/agent-safe-migrate/action@main
with:
migration-path: ./migrations
seed-path: ./schema.sql # optional
provider: pglite # or docker| Input | Default | Description |
|---|---|---|
migration-path |
(required) | File or directory to check |
seed-path |
'' |
Base schema SQL |
provider |
pglite |
pglite or docker |
docker-image |
postgres:16-alpine |
Image for the Docker provider |
confirm-destructive |
false |
Allow DROP TABLE / DROP COLUMN |
Outputs: result (JSON report) and passed (true / false). Warnings-only runs (exit code 2) count as passed.
npx mgcheck run ./migrations --ci --provider pglitenpx mgcheck hook installStaged .sql files are checked with fast static analysis. Errors block the commit; warnings are printed but don't. Using Husky or Lefthook? Call the check directly:
# .husky/pre-commit
npx --yes mgcheck hook runmgcheck looks for .mgcheckrc.json, .mgcheckrc or mgcheck.config.json in the current directory (JSON). Use --config <path> to point elsewhere.
{
"approvalMode": "manual",
"autoApprove": ["low"],
"shadowDb": {
"provider": "pglite",
"dockerImage": "postgres:16-alpine"
},
"rules": {
"MG008": "error",
"MG010": "off"
},
"output": {
"format": "terminal",
"verbose": false
},
"confirmDestructive": false
}Each entry in rules can be "error", "warning" or "off".
Priority: CLI flags β environment variables β config file β defaults.
| Variable | Description |
|---|---|
MGCHECK_MODE |
Approval mode: auto or manual |
MGCHECK_AUTO_APPROVE |
Comma-separated levels to auto-approve (with MGCHECK_MODE), e.g. low,medium |
MGCHECK_PROVIDER |
Shadow engine: pglite or docker |
MGCHECK_DB_URL |
Existing Postgres to use as the shadow DB |
MGCHECK_DOCKER_IMAGE |
Image for the Docker provider |
MGCHECK_STORAGE_DIR |
Override where audit logs and watcher state are stored |
TARGET_DATABASE_URL / DATABASE_URL |
Target database for apply operations |
| Path | Contents |
|---|---|
<project>/.mgcheck/audit.jsonl |
Per-project audit log |
~/.mgcheck/audit.jsonl |
Global audit log (npx mgcheck log --global) |
~/.mgcheck/settings.json |
Global approval mode settings |
Install via npm:
npm install mgcheckmgcheck is also an ESM library with full TypeScript types.
import { loadConfig, runCheck, renderReport, getExitCode, fixMigrationSql } from 'mgcheck';
const config = loadConfig({
shadowDb: { provider: 'pglite', dockerImage: 'postgres:16-alpine' },
});
// 'analyze' = static rules only, 'run' = rules + shadow DB execution
const report = await runCheck('./migrations', config, 'run');
console.log(report.passed); // boolean
console.log(report.violations.map((v) => v.ruleId)); // e.g. ['MG001', 'MG009']
console.log(renderReport(report, 'markdown')); // 'terminal' | 'json' | 'markdown'
process.exitCode = getExitCode(report);
// Rewrite unsafe SQL into zero-downtime patterns
const sql = 'CREATE INDEX idx_users_email ON users(email);';
const fix = fixMigrationSql(sql, report.violations);
if (fix.fixed) console.log(fix.diff);Also exported: detectMigration, analyze, ALL_RULES, createShadowDatabase, getApprovalMode / setApprovalMode, readAuditLog, and the installer helpers. See src/index.ts for the full list.
npx mgcheck uninstall # add --purge to also delete audit logsThis removes the migration-guardian entry from every editor's MCP config, strips the injected blocks from agent rule files, restores your original pre-commit hook, and leaves everything else alone.
These are real gaps in the current codebase β each one is a self-contained, well-scoped contribution. Comment on (or open) an issue before starting so work isn't duplicated.
π’ Good first contributions
- New rule:
ALTER COLUMN ... SET NOT NULLβ scans the whole table under anACCESS EXCLUSIVElock; the safe path is aCHECK (col IS NOT NULL) NOT VALIDconstraint, validate, thenSET NOT NULL. - New rule:
VACUUM FULL/CLUSTERβ full table rewrites that block reads and writes. - YAML config β
.mgcheckrc.ymlis in the config search list but is currently parsed as JSON. - More test fixtures β real-world unsafe migrations from Prisma, Drizzle and Supabase projects make the rules better for everyone.
π‘ Bigger pieces
- Auto-fixes for
MG007,MG008andMG010β rename and rollback rules have no automatic rewrite yet. - AST-based fixer β the fixer in
src/fixer/rules-fixer.tsrewrites SQL with regexes; generating fixes from the parsed AST would make them far more robust. - Table-size awareness β use
--db-urlstats (row counts, table size) to downgrade warnings on tiny tables and escalate them on huge ones. - More frameworks β e.g. Knex, TypeORM, Sequelize, Rails, Alembic, Liquibase.
- More AI clients β any MCP-capable editor or agent that isn't auto-configured yet.
Have an idea that isn't here? Open an issue β "this migration took down our prod and mgcheck didn't catch it" is the best kind of bug report.
Contributions of all sizes are welcome: a new rule, a detector for your favorite migration tool, a better error message, a docs fix, or a test fixture from a migration that once burned you.
No Docker or running Postgres needed β the test suite uses in-process PGlite.
git clone https://github.com/Aditya31032006/agent-safe-migrate.git
cd agent-safe-migrate
npm install
npm test # unit + integration tests
npm run lint # type check
npm run build # bundle to dist/
# Try the CLI from source
npm run dev -- analyze ./test/fixtures/migrations/unsafe-create-index.sqlCI runs the same lint β test β build steps on Node 18, 20 and 22 across Ubuntu and Windows.
Rules are small, deterministic and isolated β the easiest way into the codebase.
-
Create
src/analyzer/rules/<rule-name>.tsimplementing theRuleinterface:import type { Rule } from './types.js'; export const vacuumFull: Rule = { id: 'MG015', name: 'vacuum-full', severity: 'error', description: 'VACUUM FULL rewrites the table under an exclusive lock', check(statements, context) { // Walk statement.ast and return RuleViolation[] with a plain-English // message explaining the danger and a `suggestion` with the safe pattern. return []; }, };
-
Register it in
src/analyzer/rules/index.ts. -
Test it in
test/unit/rules.test.tsβ cover both the unsafe and the safe form. Add a fixture undertest/fixtures/migrations/if useful. -
Document it in the rules table above.
Look at create-index-not-concurrent.ts for a complete example.
-
npm run lintandnpm testpass - New behavior has tests
- Commit messages follow Conventional Commits (
feat:,fix:,docs:β¦) - README / rules table updated if user-facing behavior changed
See CONTRIBUTING.md for more detail.
src/
βββ analyzer/ # PostgreSQL AST parsing + the safety rules
βββ detector/ # Migration framework detection
βββ shadow/ # PGlite and Docker shadow databases
βββ fixer/ # Zero-downtime auto-fix + verification
βββ mcp/ # MCP server and tools
βββ installer/ # AI client setup, watcher, Git hook
βββ reporter/ # terminal / json / markdown output
βββ core/ # engine, config, approval modes, audit log, applier
Thanks to everyone who has helped make migrations safer:
If Migration Guardian saved you from a bad deploy, give it a β β it helps other developers find the project.
MIT Β© 2026 Aditya and contributors. Found a security issue? See SECURITY.md.