Skip to content

Commit 239d1d1

Browse files
committed
feat(hooks): block inline DB mutations across any CLI tool (v1.20.0)
Rebased onto main (was branched off 378471f, pre-1.17.0 through 1.19.0). Squashed the 10 original feature/fix commits onto current main, bumped version label from 1.17.0 to 1.20.0 to avoid collision with the existing [1.17.0] warn-greptile-review-extraction-by-created-at release. What this PR ships (unchanged from original PR #25 plan): Six new `inline-db-mutation-*` rules extending the scripts-not-DB discipline beyond Moodle/SSH to every DB CLI: - inline-db-mutation-mysql - inline-db-mutation-psql - inline-db-mutation-sqlite - inline-db-mutation-mongo - inline-db-mutation-redis - inline-db-mutation-gcloud-sql Plus the `disable_if_repo_file` rule-schema field (per-repo opt-out via a sentinel file under repo root) and a shared `db-mutation-rule` bypass marker. Greptile-PR-#25 fixes preserved: - mysql/psql short-option-no-space (-e"...", --execute="...") - mongo --eval with whitespace inside the quoted JS - gcloud --project=PROD sql ... (equals-bound global flags) - disable_if_repo_file walks up to repo root via .git marker Greptile re-review fix (this rebase): - inline-db-mutation-gcloud-sql now also catches SPACE-separated global flags (`--project my-prod`, `--configuration prod`, `--impersonate-service-account svc@host`, `--account user@host`, `--region us-central1`, etc.). Pattern relaxed from a strict `(--?flag(=value)?[[:space:]])*` chain to a generic `([^[:space:]]+[[:space:]]+)*` token-then-space loop; false-positive risk bounded by the literal `sql[[:space:]]+(import|export)[[:space:]]+(sql|csv|bak)` tail. Added 8 new test cases (6 block, 2 allow). Tests: 297 / 297 pass (test-hooks.sh), 5 / 5 pass (test-sentinel-walkup.sh). Created by Claude Code on behalf of @lapc506
1 parent 811eeaa commit 239d1d1

12 files changed

Lines changed: 1738 additions & 6 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.19.0",
4+
"version": "1.20.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.19.0",
14+
"version": "1.20.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.19.0",
3+
"version": "1.20.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",

.github/workflows/test-hooks.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,9 @@ jobs:
3737
3838
- name: Run hook smoke tests
3939
run: bash hooks/test-hooks.sh
40+
41+
- name: Run sentinel walk-up tests
42+
# Exercises the `disable_if_repo_file` lookup from subdirectories
43+
# (Greptile #25 P1 regression — sentinel at repo root must be honored
44+
# even when the hook fires from any nested cwd within the repo).
45+
run: bash hooks/test-sentinel-walkup.sh

CHANGELOG.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,94 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
1919
## [Unreleased]
2020

21+
## [1.20.0] - 2026-05-29
22+
23+
### Added
24+
- **Six new `inline-db-mutation-*` rules extending the scripts-not-DB
25+
discipline (`feedback_scripts_not_db.md`) beyond Moodle/SSH to every DB
26+
CLI.** The pre-existing `ssh-db-mutation` rule only caught
27+
`gcloud compute ssh ... --command=` with Moodle-flavoured payloads
28+
(`mdl_`, `scorm_`, `php -r`). This release blocks inline mutations
29+
across the full surface a developer is likely to reach for:
30+
- `inline-db-mutation-mysql``mysql -e "UPDATE/DELETE/..."`,
31+
`mysql ... < file.sql`, `mysqldump ... | mysql ...`.
32+
- `inline-db-mutation-psql` — `psql -c "UPDATE/INSERT/ALTER/CREATE/
33+
GRANT/REVOKE/REPLACE/RENAME"` and `pg_restore`. Complements the
34+
existing `destructive-db-ops` rule (which already covers DROP /
35+
TRUNCATE / DELETE FROM via psql).
36+
- `inline-db-mutation-sqlite``sqlite3 path "<mutation>"`.
37+
- `inline-db-mutation-mongo` — `mongo|mongosh --eval "db.x.<mutating
38+
method>(...)"` and `mongorestore`.
39+
- `inline-db-mutation-redis` — `redis-cli SET / DEL / FLUSHDB /
40+
FLUSHALL / HSET / HDEL / SADD / SREM / LPUSH / RPUSH / ZADD / ZREM /
41+
EXPIRE / RENAME / MSET / SETEX / SETNX / INCR / DECR / COPY / MOVE /
42+
UNLINK / RESTORE / EVAL`.
43+
- `inline-db-mutation-gcloud-sql``gcloud sql import sql|csv|bak` and
44+
`gcloud sql export sql|csv|bak` (export blocked because PII-bearing
45+
prod exports also belong in versioned scripts).
46+
- **Shared bypass marker `db-mutation-rule`** across all six rules. A single
47+
consistent escape token keeps the muscle memory cheap.
48+
- **Per-repo escape hatch via `disable_if_repo_file`.** New optional rule
49+
schema field: when present, the rule no-ops if a sentinel file with
50+
that exact name exists in the cwd. The inline-DB-mutation family ships
51+
with `disable_if_repo_file: .no-make-no-mistakes-db-mutation`, so a
52+
repo whose entire job is inline DB work can opt out with a one-liner
53+
(`touch .no-make-no-mistakes-db-mutation`). Hardened path validation
54+
(filename must match `^[a-zA-Z0-9._-]+$`, cannot be `.` / `..`) prevents
55+
the runtime lookup from escaping the cwd.
56+
57+
### Fixed
58+
- **Inline-DB-mutation regex bypasses** (Greptile PR #25, P1 + Security):
59+
- `mysql -e"..."` / `mysql --execute="..."` short/long-option-no-space
60+
shapes now block (spacing between `-e`/`--execute` and the SQL keyword
61+
is `[[:space:]]*` instead of `[[:space:]]+`).
62+
- `psql -c"..."` / `psql --command="..."` short/long-option-no-space
63+
shapes now block.
64+
- `mongo --eval "db.x.update(...)"` with whitespace inside the quoted JS
65+
expression now blocks (regex no longer requires the mutation method to
66+
live inside a single non-space token after `--eval`). `--eval=` shape
67+
also covered.
68+
- `gcloud --project=PROD sql export ...` and other variants with global
69+
flags between `gcloud` and `sql` now block (regex tolerates
70+
zero-or-more `--flag[=value]` tokens before the `sql` command group).
71+
Extended in this release to also cover SPACE-separated global flags
72+
(`--project my-prod`, `--configuration prod`,
73+
`--impersonate-service-account svc@example.com`, `--account user@...`,
74+
`--region us-central1`, etc.) — Greptile re-review on PR #25.
75+
- **`disable_if_repo_file` sentinel walks up to repo root** (Greptile PR
76+
#25, P1). Previously the lookup only checked `./<sentinel>` in the
77+
process cwd, so a sentinel placed at the documented location (repo
78+
root) was ignored whenever the hook fired from any subdirectory. The
79+
lookup now walks upward looking for a `.git` marker (file or
80+
directory — worktrees use a file) and resolves the sentinel relative
81+
to that root, with a cwd fallback for non-git deployments. Added
82+
`hooks/test-sentinel-walkup.sh` to lock the behavior in CI.
83+
84+
### Changed
85+
- `hooks/lib/eval-rule.sh` honours `disable_if_repo_file` between the
86+
bypass-marker check and the match-condition loop.
87+
- `scripts/build-rules.mjs` validates the new field's shape at build time
88+
(same kebab-validation defense-in-depth as `bypass_marker`).
89+
- `hooks/rules/README.md` documents the new field and the per-repo escape
90+
hatch pattern. README.md "Hooks" section now lists the six rules and
91+
the bypass marker.
92+
93+
### Notes
94+
- 38 rules total (was 32). Tests pass (210 baseline + new inline-DB cases
95+
+ space-separated Cloud SQL flag cases).
96+
- SELECT-only reads (`SELECT`, `SHOW`, `DESCRIBE`, `EXPLAIN`, `KEYS`,
97+
`GET`, `.find`, `.aggregate`, ...) remain allowed inline. The rules
98+
only fire on the mutation keyword set per CLI tool.
99+
- Commands that START with `./scripts/`, `bash scripts/`, `./bin/`, or
100+
`bash bin/` are exempted via `not_pattern` — the principle is
101+
"versioned scripts: yes; inline one-shots: no". When a wrapper script
102+
is the invocation surface, the sensitive payload lives in git history.
103+
- Memory ref: `feedback_scripts_not_db.md` (already existed for the
104+
Moodle-flavoured `ssh-db-mutation` rule; this release simply expands
105+
the enforcement surface).
106+
- **Parallel-version coordination:** originally claimed `1.17.0`; bumped
107+
to `1.20.0` after rebasing onto main (which had absorbed 1.17.0–1.19.0).
108+
21109
## [1.19.0] - 2026-05-26
22110

23111
### Added

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# make-no-mistakes
22

3-
**Version: 1.19.0** · [CHANGELOG](./CHANGELOG.md) · [Marketplace](https://github.com/DojoCodingLabs/make-no-mistakes-toolkit)
3+
**Version: 1.20.0** · [CHANGELOG](./CHANGELOG.md) · [Marketplace](https://github.com/DojoCodingLabs/make-no-mistakes-toolkit)
44

55
The disciplined dev lifecycle — implement issues, review PRs, sync releases, test E2E, and manage sessions. One plugin to make no mistakes.
66

@@ -245,7 +245,8 @@ the manifest-driven hooks in `hooks/rules/rules.yaml`. The Tier 1 ruleset
245245
ships 10 rules:
246246

247247
**PreToolUse on `Bash` (block by default):**
248-
- `ssh-db-mutation` — blocks `gcloud compute ssh ... --command="...php -r/mysql/set_config..."` (forces use of versioned scripts)
248+
- `ssh-db-mutation` — blocks `gcloud compute ssh ... --command="...php -r/mysql/set_config..."` (Moodle-flavoured SSH payloads)
249+
- `inline-db-mutation-mysql` / `-psql` / `-sqlite` / `-mongo` / `-redis` / `-gcloud-sql` — blocks inline DB mutations across any CLI (`mysql -e "UPDATE..."`, `psql -c "INSERT..."`, `sqlite3 path "DROP..."`, `mongo --eval "db.x.update(...)"`, `redis-cli SET/DEL/FLUSHALL`, `gcloud sql import/export`). Forces use of a versioned script under `scripts/` or `bin/`. SELECT-only reads are never blocked. Per-rule bypass via `# hook-bypass: db-mutation-rule`; per-repo opt-out via `touch .no-make-no-mistakes-db-mutation` at the root (memory: `feedback_scripts_not_db.md`).
249250
- `prod-ops-no-approval` — blocks `--project=*-prod` operations without explicit acknowledgement
250251
- `destructive-db-ops` — blocks `supabase db reset|push|repair` and inline `DROP/TRUNCATE/DELETE FROM`
251252
- `manual-edge-fn-deploy` — blocks `supabase functions deploy` (forces CI-only deploys)
@@ -274,8 +275,18 @@ command or content to acknowledge the rule and proceed:
274275
# // hook-bypass: edge-fn-manual
275276
# // hook-bypass: minified-build
276277
# // hook-bypass: secret-leak
278+
279+
# Bypass marker shipped in v1.17.0 inline-db-mutation family:
280+
# // hook-bypass: db-mutation-rule
277281
```
278282

283+
Some rules (e.g., the v1.17.0 inline-DB-mutation family) also support a
284+
per-repo escape hatch: drop a sentinel file at the repo root and the rule
285+
becomes a no-op in that repo. The current sentinel filenames are:
286+
287+
- `.no-make-no-mistakes-db-mutation` — disables all six
288+
`inline-db-mutation-*` rules in the repo
289+
279290
Bypasses are explicit acknowledgements — they sit inside the command/content
280291
itself, not as silent flags.
281292

hooks/lib/eval-rule.sh

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,64 @@ if [ -n "$BYPASS_MARKER" ]; then
8484
esac
8585
fi
8686

87+
# Per-repo escape hatch: if the rule declares `disable_if_repo_file: <name>`
88+
# and that file exists at the repo root, the rule is a no-op.
89+
# Use case: a repo explicitly opts out of a class of enforcement because its
90+
# workflow legitimately requires the otherwise-blocked operation (e.g., a
91+
# data-migration repo that runs inline DB mutations as part of its job).
92+
# The filename is validated to be a flat, safe name (no slashes / dots /
93+
# wildcards) so the lookup can't escape the cwd or pull in unintended files.
94+
#
95+
# Lookup strategy (Greptile #25 P1): walk upward from cwd looking for a
96+
# repo-root marker (`.git` file or dir — `.git` is a file in worktrees) and
97+
# check the sentinel at that root. If no repo root is found within a
98+
# bounded ascent, fall back to checking cwd itself (preserves the previous
99+
# behavior for repos without `.git`, e.g. tarball deploys).
100+
#
101+
# The ascent is bounded (32 levels) so an unrooted absolute path can't loop
102+
# forever — `/` has no parent, so the loop terminates naturally, but the
103+
# bound is defensive against pathological symlink trees.
104+
DISABLE_IF_REPO_FILE="$(printf '%s' "$RULE_JSON" | jq -r '.disable_if_repo_file // empty')"
105+
if [ -n "$DISABLE_IF_REPO_FILE" ]; then
106+
# Validate filename: only [a-zA-Z0-9._-], and reject "." / ".." sentinels.
107+
# The `/` character falls outside the allowed set so any path-traversal
108+
# attempt (e.g. "../foo", "etc/passwd") is rejected by the first arm.
109+
case "$DISABLE_IF_REPO_FILE" in
110+
*[!a-zA-Z0-9._-]*|.|..)
111+
echo "make-no-mistakes: rule ${RULE_ID} has invalid disable_if_repo_file (must be a flat filename of [a-zA-Z0-9._-]); ignoring escape hatch." >&2
112+
;;
113+
*)
114+
# Walk up looking for a `.git` marker (worktrees use a `.git` *file*,
115+
# standard checkouts use a `.git` *directory* — both satisfy `-e`).
116+
SENTINEL_DIR=""
117+
SEARCH_DIR="$PWD"
118+
DEPTH=0
119+
while [ "$DEPTH" -lt 32 ]; do
120+
if [ -e "${SEARCH_DIR}/.git" ]; then
121+
SENTINEL_DIR="$SEARCH_DIR"
122+
break
123+
fi
124+
PARENT="$(dirname "$SEARCH_DIR")"
125+
# `dirname /` returns `/` — bail when we stop moving up.
126+
if [ "$PARENT" = "$SEARCH_DIR" ]; then
127+
break
128+
fi
129+
SEARCH_DIR="$PARENT"
130+
DEPTH=$((DEPTH + 1))
131+
done
132+
133+
# If we found a repo root, check the sentinel there. Otherwise fall
134+
# back to cwd so non-git deployments still have an opt-out path.
135+
if [ -n "$SENTINEL_DIR" ] && [ -f "${SENTINEL_DIR}/${DISABLE_IF_REPO_FILE}" ]; then
136+
exit 0
137+
fi
138+
if [ -f "./${DISABLE_IF_REPO_FILE}" ]; then
139+
exit 0
140+
fi
141+
;;
142+
esac
143+
fi
144+
87145
# Iterate match conditions. ALL must hold for the rule to fire.
88146
N_CONDITIONS="$(printf '%s' "$RULE_JSON" | jq '.match | length')"
89147
i=0

hooks/rules/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Every rule is one YAML row with these fields:
2424
flags: i # optional; only "i" (case-insensitive) supported
2525
action: block | warn # required
2626
bypass_marker: <kebab-case> | null # optional acknowledgement token
27+
disable_if_repo_file: <flat-filename> # optional per-repo escape hatch (see below)
2728
memory_ref: <filename> # optional metadata only — NOT printed at runtime
2829
message: | # required, multi-line stderr output
2930
Block / warning text shown when this rule fires.
@@ -59,6 +60,25 @@ inside the command/content itself, not as silent flags.
5960

6061
`bypass_marker: null` (or omitted) means the rule cannot be bypassed.
6162

63+
### Per-repo escape hatch (`disable_if_repo_file`)
64+
65+
For rules that are too aggressive in specific repos (e.g., a data-migration
66+
repo whose entire job is to run inline DB mutations), a rule can declare a
67+
sentinel filename. If that file exists in the current working directory when
68+
the hook fires, the rule is a no-op.
69+
70+
```yaml
71+
- id: inline-db-mutation-mysql
72+
# ...
73+
disable_if_repo_file: .no-make-no-mistakes-db-mutation
74+
```
75+
76+
Then in the consuming repo, opting out is `touch .no-make-no-mistakes-db-mutation`
77+
at the repo root. Validation: the filename must match `^[a-zA-Z0-9._-]+$` and
78+
cannot be `.` or `..`, so the runtime lookup `[ -f "./<name>" ]` cannot
79+
escape the cwd. Use this sparingly — the bypass marker is preferred when the
80+
escape is per-command rather than per-repo.
81+
6282
## Adding a rule
6383

6484
1. Edit `rules.yaml` — append a new row following the schema.

0 commit comments

Comments
 (0)