diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index e770c27..fe6be04 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,7 +1,7 @@ { "$schema": "https://anthropic.com/claude-code/marketplace.schema.json", "name": "make-no-mistakes", - "version": "1.7.0", + "version": "1.8.0", "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.", "owner": { "name": "Luis Andres Pena Castillo", @@ -11,7 +11,7 @@ { "name": "make-no-mistakes", "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.", - "version": "1.7.0", + "version": "1.8.0", "author": { "name": "Luis Andres Pena Castillo", "email": "lapc506@users.noreply.github.com" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 95c8a76..364094a 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "make-no-mistakes", - "version": "1.7.0", + "version": "1.8.0", "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.", "author": { "name": "Luis Andres Pena Castillo", diff --git a/hooks/rules/README.md b/hooks/rules/README.md index ea3920a..9a1901e 100644 --- a/hooks/rules/README.md +++ b/hooks/rules/README.md @@ -145,17 +145,21 @@ Adding a new family is fine — just keep ids unique and follow the schema. - **Database / migration discipline** (`schema-sql-outside-migrations`, `warn-psql-against-supabase-remote`, `pr-create-with-migrations-needs-deploy-note`, - `block-supabase-db-push-prod`) — keep schema mutations inside - versioned `supabase/migrations/` files, nudge developers away from - direct `psql` / `pg_dump` / `pg_restore` execution against - `*.supabase.co` hosts, remind PR authors to document migration - deployment, and hard-block `supabase db push` aimed at the production - project ref or `--linked` (which transparently resolves to whichever - project was last linked, possibly production). The production project - ref is configured per install via the substitutions mechanism described - above (`PROD_SUPABASE_REF` token). Added after a discussion surfaced - drift between manually-applied SQL and the migrations directory when - migrations failed to auto-run after a teammate's PR merged. + `block-supabase-db-push-prod`, `warn-curl-mutating-supabase-rest`) — + keep schema mutations inside versioned `supabase/migrations/` files, + nudge developers away from direct `psql` / `pg_dump` / `pg_restore` + execution against `*.supabase.co` hosts, remind PR authors to document + migration deployment, hard-block `supabase db push` aimed at the + production project ref or `--linked` (which transparently resolves to + whichever project was last linked, possibly production), and warn on + ad-hoc mutating `curl` (`POST` / `PATCH` / `PUT` / `DELETE`) against + the Supabase PostgREST endpoint (drift risk equivalent to direct `psql` + + RLS bypass — same memory ref `feedback_scripts_not_db.md`). The + production project ref is configured per install via the substitutions + mechanism described above (`PROD_SUPABASE_REF` token). Added after a + discussion surfaced drift between manually-applied SQL and the + migrations directory when migrations failed to auto-run after a + teammate's PR merged. ## Tier 2 — decomposing non-deterministic memories diff --git a/hooks/rules/rules.json b/hooks/rules/rules.json index 77df8d4..221762e 100644 --- a/hooks/rules/rules.json +++ b/hooks/rules/rules.json @@ -1229,5 +1229,152 @@ "expected_exit": 0 } ] + }, + { + "id": "warn-curl-mutating-supabase-rest", + "description": "Warn when curl issues a mutating HTTP method (POST/PATCH/PUT/DELETE) against the Supabase PostgREST endpoint (drift risk vs versioned migrations + RLS bypass)", + "applies_to": [ + "Bash" + ], + "match": [ + { + "field": "command", + "pattern": "curl.*((-X[[:space:]]*(POST|PATCH|PUT|DELETE).*\\.supabase\\.co/rest/v1/)|(\\.supabase\\.co/rest/v1/.*-X[[:space:]]*(POST|PATCH|PUT|DELETE))|((-d[[:space:]]|--data([[:space:]]|=|-raw[[:space:]]|-raw=|-binary[[:space:]]|-binary=|-urlencode[[:space:]]|-urlencode=)).*\\.supabase\\.co/rest/v1/)|(\\.supabase\\.co/rest/v1/.*(-d[[:space:]]|--data([[:space:]]|=|-raw[[:space:]]|-raw=|-binary[[:space:]]|-binary=|-urlencode[[:space:]]|-urlencode=))))", + "flags": "i" + } + ], + "action": "warn", + "bypass_marker": "curl-supabase-rest-mutation", + "memory_ref": "feedback_scripts_not_db.md", + "message": "WARNING: mutating curl against the Supabase PostgREST endpoint.\n\nDirect REST mutations bypass:\n - the versioned migrations workflow (drift across local DBs / envs),\n - the RLS + admin-role checks the Supabase clients enforce, and\n - the access pattern already encapsulated in src/services/api/.\n\nCorrect outlets:\n - Schema changes -> supabase/migrations/_.sql\n - Data mutations -> an Edge Function or a committed script\n - One-shot fixes -> use the API client from a versioned script\n\nUse the bypass marker (curl-supabase-rest-mutation) only for documented\nhotfixes that the team has explicitly approved.\n", + "tests": [ + { + "name": "warns-curl-post-supabase-rest", + "input": { + "tool_input": { + "command": "curl -X POST https://example.supabase.co/rest/v1/profiles -H 'apikey: x' -d '{\"id\":1}'" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "warn-curl-mutating-supabase-rest" + }, + { + "name": "warns-curl-patch", + "input": { + "tool_input": { + "command": "curl -X PATCH https://example.supabase.co/rest/v1/profiles?id=eq.1 -d '{\"name\":\"a\"}'" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "warn-curl-mutating-supabase-rest" + }, + { + "name": "warns-curl-put", + "input": { + "tool_input": { + "command": "curl -X PUT https://example.supabase.co/rest/v1/profiles?id=eq.1 -d '{\"name\":\"b\"}'" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "warn-curl-mutating-supabase-rest" + }, + { + "name": "warns-curl-delete", + "input": { + "tool_input": { + "command": "curl -X DELETE https://example.supabase.co/rest/v1/profiles?id=eq.1" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "warn-curl-mutating-supabase-rest" + }, + { + "name": "warns-curl-xpost-no-space", + "input": { + "tool_input": { + "command": "curl -XPOST https://example.supabase.co/rest/v1/profiles -d '{\"id\":1}'" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "warn-curl-mutating-supabase-rest" + }, + { + "name": "warns-curl-url-before-flag", + "input": { + "tool_input": { + "command": "curl https://example.supabase.co/rest/v1/profiles -X POST -d '{\"id\":1}'" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "warn-curl-mutating-supabase-rest" + }, + { + "name": "allows-curl-get-supabase-rest", + "input": { + "tool_input": { + "command": "curl https://example.supabase.co/rest/v1/profiles?select=id,name" + } + }, + "expected_exit": 0 + }, + { + "name": "allows-curl-non-supabase", + "input": { + "tool_input": { + "command": "curl -X POST https://example.com/api/v1/users -d '{\"id\":1}'" + } + }, + "expected_exit": 0 + }, + { + "name": "warns-curl-implicit-post-via-d", + "input": { + "tool_input": { + "command": "curl -d '{\"id\":1}' https://example.supabase.co/rest/v1/profiles" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "warn-curl-mutating-supabase-rest" + }, + { + "name": "warns-curl-implicit-post-via-data-raw", + "input": { + "tool_input": { + "command": "curl --data-raw '{\"id\":1}' https://example.supabase.co/rest/v1/profiles" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "warn-curl-mutating-supabase-rest" + }, + { + "name": "warns-curl-supabase-then-d", + "input": { + "tool_input": { + "command": "curl https://example.supabase.co/rest/v1/profiles -d '{\"id\":1}'" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "warn-curl-mutating-supabase-rest" + }, + { + "name": "warns-curl-implicit-post-via-data-urlencode", + "input": { + "tool_input": { + "command": "curl --data-urlencode 'name=Alice' https://example.supabase.co/rest/v1/profiles" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "warn-curl-mutating-supabase-rest" + }, + { + "name": "allows-bypass-marker", + "input": { + "tool_input": { + "command": "curl -X POST https://example.supabase.co/rest/v1/profiles -d '{\"id\":1}' # hook-bypass: curl-supabase-rest-mutation" + } + }, + "expected_exit": 0 + } + ] } ] diff --git a/hooks/rules/rules.yaml b/hooks/rules/rules.yaml index 2ccb787..21127ae 100644 --- a/hooks/rules/rules.yaml +++ b/hooks/rules/rules.yaml @@ -985,3 +985,119 @@ tool_input: command: 'supabase db push --linked # hook-bypass: prod-db-push-approved' expected_exit: 0 + + +- id: warn-curl-mutating-supabase-rest + description: Warn when curl issues a mutating HTTP method (POST/PATCH/PUT/DELETE) against the Supabase PostgREST endpoint (drift risk vs versioned migrations + RLS bypass) + applies_to: [Bash] + match: + # The pattern catches three mutation vectors against Supabase PostgREST: + # 1. Explicit `-X METHOD` (with or without space; `-X POST` and the + # no-space `-XPOST` shorthand both fire). + # 2. URL-before-flag ordering (`curl -X POST ...`), which is + # common in shell scripts that build the URL into a variable. + # 3. Implicit POST via `-d` / `--data` / `--data-raw` / `--data-binary`. + # Curl auto-promotes to POST when these flags are present even + # without `-X`, so a one-liner like `curl -d '...' ` is a + # live mutation that would otherwise pass silently. + # The match is narrowed to `.supabase.co/rest/v1/` (PostgREST) so this + # rule does NOT fire for Supabase Auth / Storage / Edge Function + # endpoints — those have separate guidance and Edge Functions are the + # intended outlet. + - field: command + pattern: 'curl.*((-X[[:space:]]*(POST|PATCH|PUT|DELETE).*\.supabase\.co/rest/v1/)|(\.supabase\.co/rest/v1/.*-X[[:space:]]*(POST|PATCH|PUT|DELETE))|((-d[[:space:]]|--data([[:space:]]|=|-raw[[:space:]]|-raw=|-binary[[:space:]]|-binary=|-urlencode[[:space:]]|-urlencode=)).*\.supabase\.co/rest/v1/)|(\.supabase\.co/rest/v1/.*(-d[[:space:]]|--data([[:space:]]|=|-raw[[:space:]]|-raw=|-binary[[:space:]]|-binary=|-urlencode[[:space:]]|-urlencode=))))' + flags: i + action: warn + bypass_marker: curl-supabase-rest-mutation + memory_ref: feedback_scripts_not_db.md + message: | + WARNING: mutating curl against the Supabase PostgREST endpoint. + + Direct REST mutations bypass: + - the versioned migrations workflow (drift across local DBs / envs), + - the RLS + admin-role checks the Supabase clients enforce, and + - the access pattern already encapsulated in src/services/api/. + + Correct outlets: + - Schema changes -> supabase/migrations/_.sql + - Data mutations -> an Edge Function or a committed script + - One-shot fixes -> use the API client from a versioned script + + Use the bypass marker (curl-supabase-rest-mutation) only for documented + hotfixes that the team has explicitly approved. + tests: + - name: warns-curl-post-supabase-rest + input: + tool_input: + command: "curl -X POST https://example.supabase.co/rest/v1/profiles -H 'apikey: x' -d '{\"id\":1}'" + expected_exit: 0 + expected_stderr_contains: 'warn-curl-mutating-supabase-rest' + - name: warns-curl-patch + input: + tool_input: + command: "curl -X PATCH https://example.supabase.co/rest/v1/profiles?id=eq.1 -d '{\"name\":\"a\"}'" + expected_exit: 0 + expected_stderr_contains: 'warn-curl-mutating-supabase-rest' + - name: warns-curl-put + input: + tool_input: + command: "curl -X PUT https://example.supabase.co/rest/v1/profiles?id=eq.1 -d '{\"name\":\"b\"}'" + expected_exit: 0 + expected_stderr_contains: 'warn-curl-mutating-supabase-rest' + - name: warns-curl-delete + input: + tool_input: + command: 'curl -X DELETE https://example.supabase.co/rest/v1/profiles?id=eq.1' + expected_exit: 0 + expected_stderr_contains: 'warn-curl-mutating-supabase-rest' + - name: warns-curl-xpost-no-space + input: + tool_input: + command: "curl -XPOST https://example.supabase.co/rest/v1/profiles -d '{\"id\":1}'" + expected_exit: 0 + expected_stderr_contains: 'warn-curl-mutating-supabase-rest' + - name: warns-curl-url-before-flag + input: + tool_input: + command: "curl https://example.supabase.co/rest/v1/profiles -X POST -d '{\"id\":1}'" + expected_exit: 0 + expected_stderr_contains: 'warn-curl-mutating-supabase-rest' + - name: allows-curl-get-supabase-rest + input: + tool_input: + command: 'curl https://example.supabase.co/rest/v1/profiles?select=id,name' + expected_exit: 0 + - name: allows-curl-non-supabase + input: + tool_input: + command: "curl -X POST https://example.com/api/v1/users -d '{\"id\":1}'" + expected_exit: 0 + - name: warns-curl-implicit-post-via-d + input: + tool_input: + command: "curl -d '{\"id\":1}' https://example.supabase.co/rest/v1/profiles" + expected_exit: 0 + expected_stderr_contains: 'warn-curl-mutating-supabase-rest' + - name: warns-curl-implicit-post-via-data-raw + input: + tool_input: + command: "curl --data-raw '{\"id\":1}' https://example.supabase.co/rest/v1/profiles" + expected_exit: 0 + expected_stderr_contains: 'warn-curl-mutating-supabase-rest' + - name: warns-curl-supabase-then-d + input: + tool_input: + command: "curl https://example.supabase.co/rest/v1/profiles -d '{\"id\":1}'" + expected_exit: 0 + expected_stderr_contains: 'warn-curl-mutating-supabase-rest' + - name: warns-curl-implicit-post-via-data-urlencode + input: + tool_input: + command: "curl --data-urlencode 'name=Alice' https://example.supabase.co/rest/v1/profiles" + expected_exit: 0 + expected_stderr_contains: 'warn-curl-mutating-supabase-rest' + - name: allows-bypass-marker + input: + tool_input: + command: "curl -X POST https://example.supabase.co/rest/v1/profiles -d '{\"id\":1}' # hook-bypass: curl-supabase-rest-mutation" + expected_exit: 0 diff --git a/openspec/changes/2026-05-hook-curl-mutating-supabase-rest/design.md b/openspec/changes/2026-05-hook-curl-mutating-supabase-rest/design.md new file mode 100644 index 0000000..88a18bf --- /dev/null +++ b/openspec/changes/2026-05-hook-curl-mutating-supabase-rest/design.md @@ -0,0 +1,87 @@ +# Design — `warn-curl-mutating-supabase-rest` + +## Pattern (final, after Greptile rounds 1-3) + +``` +'curl.*((-X[[:space:]]*(POST|PATCH|PUT|DELETE).*\.supabase\.co/rest/v1/)|(\.supabase\.co/rest/v1/.*-X[[:space:]]*(POST|PATCH|PUT|DELETE))|((-d[[:space:]]|--data([[:space:]]|=|-raw[[:space:]]|-raw=|-binary[[:space:]]|-binary=|-urlencode[[:space:]]|-urlencode=)).*\.supabase\.co/rest/v1/)|(\.supabase\.co/rest/v1/.*(-d[[:space:]]|--data([[:space:]]|=|-raw[[:space:]]|-raw=|-binary[[:space:]]|-binary=|-urlencode[[:space:]]|-urlencode=))))' +``` + +The pattern is a 4-clause alternation that catches every realistic +mutation vector against `.supabase.co/rest/v1/`: + +1. **Explicit `-X METHOD`** — `-X[[:space:]]*` covers both `-X POST` and + the no-space `-XPOST` shorthand. Methods covered: `POST`, `PATCH`, + `PUT`, `DELETE`. (Round 1 fix.) +2. **URL-before-flag ordering** — `curl -X POST ...` is common in + shell scripts that build the URL into a variable. The second + alternation clause catches this. (Round 1 fix.) +3. **Implicit POST via `-d` / `--data` / `--data-raw` / `--data-binary`** + — curl auto-promotes to POST when these flags are present even + without `-X`. (Round 2 fix.) +4. **Implicit POST via `--data-urlencode`** — same auto-promotion as + `-d`, common for form-style payloads. (Round 3 fix.) + +Match is narrowed to `.supabase.co/rest/v1/` (PostgREST) so this rule +does NOT fire for Supabase Auth / Storage / Edge Function endpoints — +those have separate guidance and Edge Functions are the intended outlet. + +The `i` flag is used for case-insensitivity on the method names. + +### Accepted false negatives + +- `curl --request POST` — uncommon long form; authors using it are + expected to know the rule and self-flag. +- `curl -G ... -d ...` — `-G` overrides curl's auto-promotion and forces + GET; `-d` becomes a query-string. Treating this as a mutation would be + incorrect. + +## Action + +`warn`, not `block`. The rule is a nudge, not a hard stop — there are +legitimate reasons to issue a one-shot REST mutation (e.g. seeding a +local-only fixture, debugging an Edge Function invocation chain). The +`warn-psql-against-supabase-remote` neighbor uses the same severity; they +are the same class of nudge. + +## Bypass marker + +`curl-supabase-rest-mutation` — kebab-case, unique. Documented as +"hotfix only". + +## Test fixtures + +Use a **sanitized** Supabase host (`example.supabase.co`) — same convention +as the migration-discipline rules added in PR #15 round-2. Real project +refs would either leak in the public toolkit or trip the IP-leak guard. + +Final tests after Greptile rounds 1+2 (12 cases): + +1. `warns-curl-post-supabase-rest` — POST mutation, expects warn. +2. `warns-curl-patch` — PATCH mutation, expects warn. +3. `warns-curl-put` — PUT mutation, expects warn. +4. `warns-curl-delete` — DELETE mutation, expects warn. +5. `warns-curl-xpost-no-space` — no-space `-XPOST` shorthand, expects warn. +6. `warns-curl-url-before-flag` — URL precedes `-X POST`, expects warn. +7. `warns-curl-implicit-post-via-d` — `-d` body without `-X`, expects warn. +8. `warns-curl-implicit-post-via-data-raw` — `--data-raw` body without `-X`, + expects warn. +9. `warns-curl-supabase-then-d` — URL first then `-d`, expects warn. +10. `allows-curl-get-supabase-rest` — GET (read), expects allow. +11. `allows-curl-non-supabase` — POST against `example.com`, expects allow. +12. `allows-bypass-marker` — POST + bypass comment, expects allow. + +Action is `warn`, so `expected_exit` is `0` everywhere and we use +`expected_stderr_contains` to assert the warning fired. + +Round-1 Greptile findings closed: `-XPOST` no-space + URL-before-flag +ordering. Round-2 finding closed: implicit POST via `-d` / `--data*` is +also caught. + +## Why warn, not block + +A `block` would frustrate legitimate one-off REST debugging. The author of +`feedback_scripts_not_db.md` is consistent: they prefer a warn for +psql-against-remote (the documented neighbor rule), and the same severity +logic applies here. If repeat offenders surface in code review, we can +escalate the action to `block` in a follow-up PR without changing the +schema. diff --git a/openspec/changes/2026-05-hook-curl-mutating-supabase-rest/proposal.md b/openspec/changes/2026-05-hook-curl-mutating-supabase-rest/proposal.md new file mode 100644 index 0000000..69f66ee --- /dev/null +++ b/openspec/changes/2026-05-hook-curl-mutating-supabase-rest/proposal.md @@ -0,0 +1,43 @@ +# Proposal — `warn-curl-mutating-supabase-rest` + +## Why + +`feedback_scripts_not_db.md` mandates that schema and data mutations always go +through versioned migrations or committed scripts — never via ad-hoc `psql`, +the SQL Editor, or direct REST calls against the live Supabase project. The +existing `warn-psql-against-supabase-remote` rule covers the `psql` / +`pg_dump` / `pg_restore` path, but `curl -X POST/PATCH/PUT/DELETE` against +PostgREST (`https://.supabase.co/rest/v1/...`) is an equivalent +escape hatch that the manifest does not yet warn about. + +PostgREST mutations: + +- bypass the versioned migrations workflow (drift vs other devs' local DBs), +- skip the RLS / role checks the Supabase clients enforce, +- and circumvent the API-client layer in `src/services/api/` that already + encapsulates the same access pattern. + +## What + +Add **one new warn-only rule** to the manifest: + +``` +warn-curl-mutating-supabase-rest + applies_to: Bash + match: command contains `curl ... -X (POST|PATCH|PUT|DELETE) ... .supabase.co/rest/v1/` + action: warn + bypass_marker: curl-supabase-rest-mutation + memory_ref: feedback_scripts_not_db.md +``` + +`GET` requests (read-only inspection) are intentionally not flagged; the rule +targets mutations only. + +## Impact + +- `hooks/rules/rules.yaml` — append the new rule + ≥5 tests +- `hooks/rules/rules.json` — regenerated artifact +- `hooks/rules/README.md` — extend the "Database / migration discipline" + family entry to mention the new rule +- 3 manifests bumped `1.7.0 → 1.8.0` (`package.json`, `.claude-plugin/plugin.json`, + `.claude-plugin/marketplace.json` top-level + nested plugin entry) diff --git a/openspec/changes/2026-05-hook-curl-mutating-supabase-rest/tasks.md b/openspec/changes/2026-05-hook-curl-mutating-supabase-rest/tasks.md new file mode 100644 index 0000000..eaad32e --- /dev/null +++ b/openspec/changes/2026-05-hook-curl-mutating-supabase-rest/tasks.md @@ -0,0 +1,11 @@ +# Tasks — `warn-curl-mutating-supabase-rest` + +- [x] Append rule to `hooks/rules/rules.yaml` with ≥5 tests +- [x] Run `npm run build-rules` to regenerate `hooks/rules/rules.json` +- [x] Run `npm run test-hooks` — all tests pass (target: 86 → 93+) +- [x] Update `hooks/rules/README.md` family entry +- [x] Bump version `1.7.0 → 1.8.0` in 3 manifests (package.json, + .claude-plugin/plugin.json, .claude-plugin/marketplace.json) +- [x] Commit + push + open PR + tag Greptile +- [x] Loop until Greptile 5/5 (substantive findings only) +- [x] `gh pr merge --squash --delete-branch` diff --git a/package.json b/package.json index b2d6c18..259d3da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lapc506/make-no-mistakes", - "version": "1.7.0", + "version": "1.8.0", "description": "The disciplined dev lifecycle — implement issues, review PRs, sync releases, test E2E, manage sessions, stash secrets, and enforce manifest-driven tool-call hooks (no SSH+DB, no manual prod, no minified build, no secret leaks, Slack format). OpenCode + Claude Code plugin.", "type": "module", "main": "./dist/index.js",