Skip to content

Commit e3161a0

Browse files
lapc506claude
andcommitted
feat(rules): add 4 migration-discipline PreToolUse rules
Adds a new rule family — "Database / migration discipline" — that prevents the class of drift seen when a teammate's PR shipped supabase/migrations files that did not auto-run on staging, and the manual SQL Editor workaround introduced silent skew between the migrations directory and other devs' local databases. Layered defense (4 new rules, 21 new tests): 1. schema-sql-outside-migrations (BLOCK on Edit/Write/MultiEdit) Blocks ALTER/CREATE/DROP/GRANT/REVOKE/CREATE POLICY in any .sql file outside supabase/migrations/, supabase/tests/, supabase/seed. 2. warn-psql-against-supabase-remote (WARN on Bash) Nudges away from psql/SQL Editor execution against any *.supabase.co host — that workflow is the primary source of drift vs versioned migrations. 3. pr-create-with-migrations-needs-deploy-note (WARN on Bash) Reminds the author to describe migration deployment in the PR body when calling gh pr create. Pure heuristic — does not inspect the diff; bypass marker available for migration-free PRs. 4. block-supabase-db-push-prod (BLOCK on Bash) Hard stop on supabase db push aimed at the PRODUCTION project ref (ukwovawzehnebuoowcec). Only the create-release SOP may apply migrations to prod. Test count: 59 → 80 (21 new tests, all 80 pass) Version: 1.6.0 → 1.7.0 (minor bump per semver — feature add) Files: - hooks/rules/rules.yaml — 4 new rule entries with full test arrays - hooks/rules/rules.json — regenerated via npm run build-rules - hooks/rules/README.md — new "Database / migration discipline" family - package.json, .claude-plugin/{plugin,marketplace}.json — version bump Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7043033 commit e3161a0

6 files changed

Lines changed: 547 additions & 4 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
33
"name": "make-no-mistakes",
4-
"version": "1.6.0",
4+
"version": "1.7.0",
55
"description": "The disciplined dev lifecycle — implement issues, review PRs, sync releases, test E2E, manage sessions, and stash secrets via OS-native prompts. One plugin to make no mistakes.",
66
"owner": {
77
"name": "Luis Andres Pena Castillo",
@@ -11,7 +11,7 @@
1111
{
1212
"name": "make-no-mistakes",
1313
"description": "Dev lifecycle orchestrator: disciplined Linear issue execution with worktree isolation, PR review with Greptile gating, team release sync, E2E test generation and execution, test suite previewer, security pentesting, MoSCoW + RICE prioritization, cross-platform secret stash via OS-native GUI prompts (zenity / kdialog / osascript / Get-Credential), and session management. 18 commands, 6 auto-activating skills, 2 specialized agents.",
14-
"version": "1.6.0",
14+
"version": "1.7.0",
1515
"author": {
1616
"name": "Luis Andres Pena Castillo",
1717
"email": "lapc506@users.noreply.github.com"

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "make-no-mistakes",
3-
"version": "1.6.0",
3+
"version": "1.7.0",
44
"description": "The disciplined dev lifecycle — implement issues, review PRs, sync releases, test E2E, manage sessions, stash secrets, and enforce manifest-driven tool-call hooks. One plugin to make no mistakes.",
55
"author": {
66
"name": "Luis Andres Pena Castillo",

hooks/rules/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,17 @@ Adding a new family is fine — just keep ids unique and follow the schema.
105105
alongside the runtime hook, pre-commit linter, and Storage upload
106106
validator. See
107107
[DOJ-3924](https://linear.app/dojocoding/issue/DOJ-3924).
108+
- **Database / migration discipline**
109+
(`schema-sql-outside-migrations`, `warn-psql-against-supabase-remote`,
110+
`pr-create-with-migrations-needs-deploy-note`,
111+
`block-supabase-db-push-prod`) — keep schema mutations inside
112+
versioned `supabase/migrations/` files, nudge developers away from
113+
SQL-Editor-style direct execution against `*.supabase.co` hosts,
114+
remind PR authors to document migration deployment, and hard-block
115+
`supabase db push` aimed at the production project ref. Added after
116+
a Slack discussion surfaced drift between manually-applied SQL and
117+
the migrations directory when migrations failed to auto-run on
118+
staging after a teammate's PR merged.
108119

109120
## Tier 2 — decomposing non-deterministic memories
110121

hooks/rules/rules.json

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,5 +882,295 @@
882882
"expected_exit": 0
883883
}
884884
]
885+
},
886+
{
887+
"id": "schema-sql-outside-migrations",
888+
"description": "Block schema-changing SQL (ALTER/CREATE/DROP/GRANT/REVOKE/CREATE POLICY) in .sql files outside supabase/migrations/",
889+
"applies_to": [
890+
"Edit",
891+
"Write",
892+
"MultiEdit"
893+
],
894+
"match": [
895+
{
896+
"field": "file_path",
897+
"pattern": "\\.sql$",
898+
"not_pattern": "(supabase/migrations/|supabase/tests/|supabase/seed)"
899+
},
900+
{
901+
"field": "content",
902+
"pattern": "\\b(ALTER TABLE|CREATE TABLE|DROP TABLE|CREATE POLICY|DROP POLICY|GRANT |REVOKE |CREATE FUNCTION|DROP FUNCTION|CREATE TYPE|ALTER TYPE)\\b",
903+
"flags": "i"
904+
}
905+
],
906+
"action": "block",
907+
"bypass_marker": "schema-sql-outside-migrations",
908+
"memory_ref": "feedback_scripts_not_db.md",
909+
"message": "BLOCKED: schema-changing SQL in a file outside supabase/migrations/.\n\nEvery schema mutation (ALTER/CREATE/DROP/GRANT/POLICY) MUST live in a\ntimestamped migration file, never in ad-hoc scripts or runtime queries.\n\nCreate a new file:\n supabase/migrations/$(date -u +%Y%m%d%H%M%S)_<slug>.sql\n\nLegitimate exceptions (use the bypass marker):\n- Schema docs / examples in READMEs — should not be executable SQL\n- Test fixtures under supabase/tests/ — already exempt via not_pattern\n",
910+
"tests": [
911+
{
912+
"name": "blocks-create-table-in-script",
913+
"input": {
914+
"tool_input": {
915+
"file_path": "scripts/setup.sql",
916+
"content": "CREATE TABLE foo (id int primary key);"
917+
}
918+
},
919+
"expected_exit": 2
920+
},
921+
{
922+
"name": "blocks-grant-in-edge-function",
923+
"input": {
924+
"tool_input": {
925+
"file_path": "supabase/functions/foo/migrate.sql",
926+
"content": "GRANT SELECT ON foo TO authenticated;"
927+
}
928+
},
929+
"expected_exit": 2
930+
},
931+
{
932+
"name": "blocks-alter-table-in-readme-example",
933+
"input": {
934+
"tool_input": {
935+
"file_path": "docs/example.sql",
936+
"content": "ALTER TABLE foo ADD COLUMN bar text;"
937+
}
938+
},
939+
"expected_exit": 2
940+
},
941+
{
942+
"name": "allows-schema-in-migrations-folder",
943+
"input": {
944+
"tool_input": {
945+
"file_path": "supabase/migrations/20260509000000_foo.sql",
946+
"content": "CREATE TABLE foo (id int primary key);"
947+
}
948+
},
949+
"expected_exit": 0
950+
},
951+
{
952+
"name": "allows-schema-in-tests-folder",
953+
"input": {
954+
"tool_input": {
955+
"file_path": "supabase/tests/foo_test.sql",
956+
"content": "CREATE TABLE temp_test_t (id int);"
957+
}
958+
},
959+
"expected_exit": 0
960+
},
961+
{
962+
"name": "allows-non-schema-sql-anywhere",
963+
"input": {
964+
"tool_input": {
965+
"file_path": "scripts/query.sql",
966+
"content": "SELECT * FROM foo;"
967+
}
968+
},
969+
"expected_exit": 0
970+
},
971+
{
972+
"name": "allows-bypass-marker",
973+
"input": {
974+
"tool_input": {
975+
"file_path": "scripts/oneshot.sql",
976+
"content": "-- # hook-bypass: schema-sql-outside-migrations\nCREATE TABLE foo (id int primary key);\n"
977+
}
978+
},
979+
"expected_exit": 0
980+
}
981+
]
982+
},
983+
{
984+
"id": "warn-psql-against-supabase-remote",
985+
"description": "Warn when psql/SQL is executed directly against a *.supabase.co host (drift risk vs versioned migrations)",
986+
"applies_to": [
987+
"Bash"
988+
],
989+
"match": [
990+
{
991+
"field": "command",
992+
"pattern": "psql.*\\.supabase\\.co",
993+
"flags": "i"
994+
}
995+
],
996+
"action": "warn",
997+
"bypass_marker": "psql-supabase-remote",
998+
"memory_ref": "feedback_scripts_not_db.md",
999+
"message": "WARNING: direct psql execution against a Supabase remote host.\n\nSQL Editor / psql-against-remote workflows create drift between the\nversioned migrations directory and other devs' local databases.\n\nCorrect workflow:\n 1. Add a migration in supabase/migrations/<timestamp>_<slug>.sql\n 2. Test locally: supabase db reset --local\n 3. Push to develop — CI applies the migration to staging automatically\n 4. For production: William runs the create-release SOP\n\nUse the bypass marker only for legitimate hotfixes where the PR cycle\nis too slow.\n",
1000+
"tests": [
1001+
{
1002+
"name": "warns-on-psql-supabase-remote",
1003+
"input": {
1004+
"tool_input": {
1005+
"command": "psql postgresql://user:pass@db.xepaexmpawtpqtilhcpw.supabase.co:5432/postgres -c 'SELECT 1'"
1006+
}
1007+
},
1008+
"expected_exit": 0,
1009+
"expected_stderr_contains": "warn-psql-against-supabase-remote"
1010+
},
1011+
{
1012+
"name": "warns-on-psql-different-supabase-host",
1013+
"input": {
1014+
"tool_input": {
1015+
"command": "psql -h db.ukwovawzehnebuoowcec.supabase.co -d postgres -c 'CREATE INDEX foo_idx ON foo (id)'"
1016+
}
1017+
},
1018+
"expected_exit": 0,
1019+
"expected_stderr_contains": "warn-psql-against-supabase-remote"
1020+
},
1021+
{
1022+
"name": "allows-psql-localhost",
1023+
"input": {
1024+
"tool_input": {
1025+
"command": "psql -h localhost -p 54322 -d postgres -c 'SELECT 1'"
1026+
}
1027+
},
1028+
"expected_exit": 0
1029+
},
1030+
{
1031+
"name": "allows-non-psql-supabase-references",
1032+
"input": {
1033+
"tool_input": {
1034+
"command": "curl https://xepaexmpawtpqtilhcpw.supabase.co/rest/v1/foo"
1035+
}
1036+
},
1037+
"expected_exit": 0
1038+
},
1039+
{
1040+
"name": "allows-bypass-marker",
1041+
"input": {
1042+
"tool_input": {
1043+
"command": "psql -h db.xepaexmpawtpqtilhcpw.supabase.co -c 'SELECT 1' # hook-bypass: psql-supabase-remote"
1044+
}
1045+
},
1046+
"expected_exit": 0
1047+
}
1048+
]
1049+
},
1050+
{
1051+
"id": "pr-create-with-migrations-needs-deploy-note",
1052+
"description": "Warn when gh pr create body does not mention migrations (caller should verify the PR has no migration files)",
1053+
"applies_to": [
1054+
"Bash"
1055+
],
1056+
"match": [
1057+
{
1058+
"field": "command",
1059+
"pattern": "gh[[:space:]]+pr[[:space:]]+create.*--body",
1060+
"flags": "i"
1061+
},
1062+
{
1063+
"field": "command",
1064+
"not_pattern": "(migration|db push|supabase db|migration-deployment|requires.*staging|requires.*prod)",
1065+
"flags": "i"
1066+
}
1067+
],
1068+
"action": "warn",
1069+
"bypass_marker": "pr-create-no-migration-note",
1070+
"memory_ref": "feedback_scripts_not_db.md",
1071+
"message": "WARNING: gh pr create body does not mention migrations.\n\nIf your PR modifies files under supabase/migrations/, the body MUST\ninclude a \"Migration deployment\" section that lists:\n 1. Which envs need supabase db push (staging auto via CI; prod via release)\n 2. Backfill order if the migrations depend on existing data\n 3. Post-merge verification steps\n\nThis rule does NOT inspect the diff — it only reads the command. Use\nthe bypass marker if your PR has no migrations.\n",
1072+
"tests": [
1073+
{
1074+
"name": "warns-on-pr-create-body-without-migration-mention",
1075+
"input": {
1076+
"tool_input": {
1077+
"command": "gh pr create --base develop --title \"feat: foo\" --body \"Adds feature X\""
1078+
}
1079+
},
1080+
"expected_exit": 0,
1081+
"expected_stderr_contains": "pr-create-with-migrations-needs-deploy-note"
1082+
},
1083+
{
1084+
"name": "allows-pr-create-body-mentioning-migration",
1085+
"input": {
1086+
"tool_input": {
1087+
"command": "gh pr create --base develop --title \"feat: foo\" --body \"Adds X. Migration deployment: requires supabase db push on staging\""
1088+
}
1089+
},
1090+
"expected_exit": 0
1091+
},
1092+
{
1093+
"name": "allows-pr-create-body-mentioning-db-push",
1094+
"input": {
1095+
"tool_input": {
1096+
"command": "gh pr create --body \"Use db push to apply\""
1097+
}
1098+
},
1099+
"expected_exit": 0
1100+
},
1101+
{
1102+
"name": "allows-bypass-marker",
1103+
"input": {
1104+
"tool_input": {
1105+
"command": "gh pr create --body \"release notes only # hook-bypass: pr-create-no-migration-note\""
1106+
}
1107+
},
1108+
"expected_exit": 0
1109+
}
1110+
]
1111+
},
1112+
{
1113+
"id": "block-supabase-db-push-prod",
1114+
"description": "Block supabase db push targeting the PRODUCTION project ref without an explicit bypass marker",
1115+
"applies_to": [
1116+
"Bash"
1117+
],
1118+
"match": [
1119+
{
1120+
"field": "command",
1121+
"pattern": "supabase[[:space:]]+db[[:space:]]+push.*ukwovawzehnebuoowcec"
1122+
}
1123+
],
1124+
"action": "block",
1125+
"bypass_marker": "prod-db-push-approved",
1126+
"memory_ref": "feedback_never_touch_prod_without_approval.md",
1127+
"message": "BLOCKED: supabase db push targeting the PRODUCTION project ref\n(ukwovawzehnebuoowcec).\n\nNEVER run against production without explicit user approval — see\nfeedback_never_touch_prod_without_approval.md.\n\nCorrect workflow:\n 1. Merge to main\n 2. William runs the release per the SOP at\n https://github.com/DojoCodingLabs/dojo-os/blob/develop/.claude/commands/create-release.md\n\nUse the bypass marker ONLY when the user has verbally approved the\noperation.\n",
1128+
"tests": [
1129+
{
1130+
"name": "blocks-supabase-db-push-prod",
1131+
"input": {
1132+
"tool_input": {
1133+
"command": "supabase db push --db-url postgresql://postgres:pwd@db.ukwovawzehnebuoowcec.supabase.co:5432/postgres"
1134+
}
1135+
},
1136+
"expected_exit": 2
1137+
},
1138+
{
1139+
"name": "blocks-supabase-db-push-prod-with-project-ref-flag",
1140+
"input": {
1141+
"tool_input": {
1142+
"command": "supabase db push --project-ref ukwovawzehnebuoowcec"
1143+
}
1144+
},
1145+
"expected_exit": 2
1146+
},
1147+
{
1148+
"name": "allows-supabase-db-push-staging",
1149+
"input": {
1150+
"tool_input": {
1151+
"command": "supabase db push --project-ref xepaexmpawtpqtilhcpw"
1152+
}
1153+
},
1154+
"expected_exit": 0
1155+
},
1156+
{
1157+
"name": "allows-supabase-db-push-local",
1158+
"input": {
1159+
"tool_input": {
1160+
"command": "supabase db push"
1161+
}
1162+
},
1163+
"expected_exit": 0
1164+
},
1165+
{
1166+
"name": "allows-bypass-marker",
1167+
"input": {
1168+
"tool_input": {
1169+
"command": "supabase db push --project-ref ukwovawzehnebuoowcec # hook-bypass: prod-db-push-approved"
1170+
}
1171+
},
1172+
"expected_exit": 0
1173+
}
1174+
]
8851175
}
8861176
]

0 commit comments

Comments
 (0)