diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 21c82e0..1e2d68e 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.38.0", + "version": "1.41.0", "description": "The disciplined dev lifecycle \u2014 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. Start with /make-no-mistakes:domain-driven-advisor \u2014 the canonical entry point that inspects your repo, asks a few plain-language questions, and routes you across the six-family audit engine (schema-drift, contract-drift, ddd, explicit-architecture, strangler, enforcement-hooks), then runs a premortem on the aggregated remediation plan. Also ships disciplined Linear issue execution with worktree isolation, PR review with Greptile gating, team release sync, E2E test generation/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. 37 commands, 11 auto-activating skills, 2 specialized agents.", - "version": "1.38.0", + "version": "1.41.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 f2af3c5..bc071a2 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "make-no-mistakes", - "version": "1.38.0", + "version": "1.41.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/CHANGELOG.md b/CHANGELOG.md index 5e48682..0151bb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.41.0] - 2026-08-02 + +### Added +- **`block-no-verify` — the flag that had a rule about it and no rule enforcing + it.** Non-negotiable 21 names four bypasses: `--no-verify`, `--admin`, + `--force`, and merging past a check. Measured across all 40 rules: `--admin`, + `push --force` and `--force-with-lease` each had one. **`--no-verify` had + none.** + + That asymmetry is worse than a plain gap. Three of the four are covered, so a + reader has every reason to assume the fourth is too — the rule reads as + enforced and is not. + + Found the way these things are always found. On 2026-08-02 an agent resolving + a merge conflict ran `git commit --no-verify`, by reflex and with no + justification. It reported the violation itself, discarded the commit, redid + the merge and recommitted with hooks enabled. **Nothing stopped it, because + there was nothing to stop it** — it did not route around a guard, it walked + through a gap. + + **The near-miss the rule must not get wrong**, and the reason the short form + is scoped to one subcommand: `git commit -n` IS `--no-verify`, while + `git push -n` is `--dry-run`. Same letter, opposite meaning. Blocking a + dry-run would refuse the safest command in git, and a guard that refuses + ordinary work gets bypassed — which is how a guard stops carrying + information at all. Asserted by `push-dry-run-allowed`. + + **No bypass marker, deliberately** (`bypass_marker: null`), and the refusal + message says why rather than asserting it. The incident's own honest attempt + first failed with `vitest: command not found`, because a fresh worktree had + no dependencies. The fix was `bun install --frozen-lockfile`. **A hook that + cannot run is a broken toolchain, not a gate to step over** — so the message + names that command, because a refusal that leaves you stuck is a refusal that + gets worked around next time. + + Six tests, all green, covering both spellings on both subcommands, the + dry-run near-miss, an ordinary commit, and a quoted mention (documenting the + flag performs no skip). Hook suite 346/346, vitest 60/60. + + ## [1.38.0] - 2026-07-31 ### Changed @@ -950,6 +990,7 @@ installed caches) but had no representation on `main`; this release lands them. ([PR #4](https://github.com/DojoCodingLabs/make-no-mistakes-toolkit/pull/4)). [Unreleased]: https://github.com/DojoCodingLabs/make-no-mistakes-toolkit/compare/v1.38.0...HEAD +[1.41.0]: https://github.com/DojoCodingLabs/make-no-mistakes-toolkit/releases/tag/v1.41.0 [1.38.0]: https://github.com/DojoCodingLabs/make-no-mistakes-toolkit/releases/tag/v1.38.0 [1.37.0]: https://github.com/DojoCodingLabs/make-no-mistakes-toolkit/releases/tag/v1.37.0 [1.36.0]: https://github.com/DojoCodingLabs/make-no-mistakes-toolkit/releases/tag/v1.36.0 diff --git a/hooks/rules/rules.json b/hooks/rules/rules.json index 17bca55..9ebd3e1 100644 --- a/hooks/rules/rules.json +++ b/hooks/rules/rules.json @@ -1572,6 +1572,86 @@ } ] }, + { + "id": "block-no-verify", + "description": "Block a commit or push that skips the hooks", + "applies_to": [ + "Bash" + ], + "match": [ + { + "field": "command", + "pattern": "(git[[:space:]]+(commit|push)[^|;&]*--no-verify|git[[:space:]]+commit[^|;&]*[[:space:]]-n([[:space:]]|$))" + }, + { + "field": "command", + "not_pattern": "['\\\"]--no-verify" + } + ], + "action": "block", + "bypass_marker": null, + "memory_ref": "feedback_agents_never_self_authorize_documented_overrides.md", + "references": [ + "dojo-os CLAUDE.md non-negotiable 21" + ], + "message": "BLOCKED: this skips the hooks.\n\nThe hooks are the only thing standing between a commit and the checks it\nwas supposed to pass. Skipping them does not make a commit faster -- it\nmakes an UNVERIFIED commit indistinguishable from a verified one, and\nnothing downstream can tell them apart afterwards.\n\nNon-negotiable 21 names this flag alongside --admin and --force. Those\nthree had rules; this one did not, which is why you are reading a new\nmessage. An agent used it on 2026-08-02, reported it, and reverted it.\n\nTHE HOOKS FAILING IS NOT A REASON TO SKIP THEM. In that same incident the\nhonest attempt failed with `vitest: command not found` because a fresh\nworktree had no dependencies. The fix was:\n\n bun install --frozen-lockfile\n\nA hook that cannot run is a broken toolchain, not a gate to step over. If\nthe hooks are wrong, fix the hooks or say so -- both leave a trace, and\nthat trace is the entire point.\n\nThere is deliberately NO bypass marker. A gate whose refusal hands you the\nway around it is not a gate (DOJ-6247).\n", + "tests": [ + { + "name": "commit-long-flag-blocked", + "input": { + "tool_input": { + "command": "git commit --no-verify -m 'wip'" + } + }, + "expected_exit": 2 + }, + { + "name": "commit-short-flag-blocked", + "input": { + "tool_input": { + "command": "git commit -n -m 'wip'" + } + }, + "expected_exit": 2 + }, + { + "name": "push-long-flag-blocked", + "input": { + "tool_input": { + "command": "git push --no-verify origin main" + } + }, + "expected_exit": 2 + }, + { + "name": "push-dry-run-allowed", + "input": { + "tool_input": { + "command": "git push -n origin main" + } + }, + "expected_exit": 0 + }, + { + "name": "ordinary-commit-allowed", + "input": { + "tool_input": { + "command": "git commit -m 'feat: add thing'" + } + }, + "expected_exit": 0 + }, + { + "name": "quoted-mention-allowed", + "input": { + "tool_input": { + "command": "grep -rn '--no-verify' docs/" + } + }, + "expected_exit": 0 + } + ] + }, { "id": "block-git-force-push-no-lease", "description": "Block `git push --force` / `-f` without `--force-with-lease` (overwrites collaborator pushes silently)", diff --git a/hooks/rules/rules.yaml b/hooks/rules/rules.yaml index ad93fd2..481be05 100644 --- a/hooks/rules/rules.yaml +++ b/hooks/rules/rules.yaml @@ -1309,6 +1309,73 @@ command: 'npx playwright test # hook-bypass: playwright-headless-allowed' expected_exit: 0 +- id: block-no-verify + description: Block a commit or push that skips the hooks + applies_to: [Bash] + match: + # `git commit -n` IS --no-verify. `git push -n` is --dry-run and must NOT + # match, so the short form is scoped to commit only. Two flags, one letter, + # opposite meanings -- the same near-miss shape the force-push rule guards. + - field: command + pattern: '(git[[:space:]]+(commit|push)[^|;&]*--no-verify|git[[:space:]]+commit[^|;&]*[[:space:]]-n([[:space:]]|$))' + # Mention is not execution. A quoted occurrence -- documenting the flag, + # grepping for it, writing a rule about it -- performs no skip. + - field: command + not_pattern: "['\\\"]--no-verify" + action: block + bypass_marker: null + memory_ref: feedback_agents_never_self_authorize_documented_overrides.md + references: + - "dojo-os CLAUDE.md non-negotiable 21" + message: | + BLOCKED: this skips the hooks. + + The hooks are the only thing standing between a commit and the checks it + was supposed to pass. Skipping them does not make a commit faster -- it + makes an UNVERIFIED commit indistinguishable from a verified one, and + nothing downstream can tell them apart afterwards. + + Non-negotiable 21 names this flag alongside --admin and --force. Those + three had rules; this one did not, which is why you are reading a new + message. An agent used it on 2026-08-02, reported it, and reverted it. + + THE HOOKS FAILING IS NOT A REASON TO SKIP THEM. In that same incident the + honest attempt failed with `vitest: command not found` because a fresh + worktree had no dependencies. The fix was: + + bun install --frozen-lockfile + + A hook that cannot run is a broken toolchain, not a gate to step over. If + the hooks are wrong, fix the hooks or say so -- both leave a trace, and + that trace is the entire point. + + There is deliberately NO bypass marker. A gate whose refusal hands you the + way around it is not a gate (DOJ-6247). + tests: + - name: commit-long-flag-blocked + input: { tool_input: { command: "git commit --no-verify -m 'wip'" } } + expected_exit: 2 + - name: commit-short-flag-blocked + input: { tool_input: { command: "git commit -n -m 'wip'" } } + expected_exit: 2 + - name: push-long-flag-blocked + input: { tool_input: { command: "git push --no-verify origin main" } } + expected_exit: 2 + # THE NEAR-MISS THIS RULE MUST NOT GET WRONG. `-n` is --no-verify on + # commit and --dry-run on push: same letter, opposite meaning. Blocking a + # dry-run would refuse the safest command in git, and a guard that refuses + # ordinary work gets bypassed. + - name: push-dry-run-allowed + input: { tool_input: { command: "git push -n origin main" } } + expected_exit: 0 + - name: ordinary-commit-allowed + input: { tool_input: { command: "git commit -m 'feat: add thing'" } } + expected_exit: 0 + # Mention is not execution: documenting the flag performs no skip. + - name: quoted-mention-allowed + input: { tool_input: { command: "grep -rn '--no-verify' docs/" } } + expected_exit: 0 + - id: block-git-force-push-no-lease description: Block `git push --force` / `-f` without `--force-with-lease` (overwrites collaborator pushes silently) applies_to: [Bash] diff --git a/package.json b/package.json index 55db7af..fa96f05 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lapc506/make-no-mistakes", - "version": "1.38.0", + "version": "1.41.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",