Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
"name": "make-no-mistakes",
"version": "1.35.0",
"version": "1.36.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",
Expand All @@ -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.35.0",
"version": "1.36.0",
"author": {
"name": "Luis Andres Pena Castillo",
"email": "lapc506@users.noreply.github.com"
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "make-no-mistakes",
"version": "1.35.0",
"version": "1.36.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",
Expand Down
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.36.0] - 2026-07-31

### Added
- **`discard-stderr` hook rule** — blocks a `Bash` command that routes stderr to
`/dev/null`. A failing command with its stderr discarded is *indistinguishable*
from a succeeding one that printed nothing, so the empty result gets read as
"none found" rather than "it errored". That is not a hypothetical: on
2026-07-31 a `gh api --jq --arg ... 2>/dev/null` — `gh` rejects that flag
combination — produced an empty file that was reported to the team as "0 red
PRs". Sixteen of 41 were red, twelve of them on TypeScript. The discarded
stderr said exactly what was wrong.

**The rule matches on order, which is the whole difficulty.** `>/dev/null 2>&1`
is blocked: stdout is redirected first, then stderr is pointed at wherever
stdout now goes, so both die. `2>&1 >/dev/null` is allowed: stderr is
duplicated to the *original* stdout before the redirect, so it survives.
Identical token sets, opposite outcomes — a matcher that keyed on the tokens
alone would get one of the two wrong, and it is the permissive error that
costs, because a rule that blocks working commands gets removed.

Three shapes stay allowed and each has a test pinning it: `cmd >/dev/null`
(stderr still reaches you), `command -v x >/dev/null` (the existence probe,
which appears throughout these very hooks), and `cmd 2>&1 >/dev/null`. A
quoted mention — `git grep '2>/dev/null'` — performs no redirect and is not
blocked, because mention is not execution.

**Ships with `bypass_marker: null`**, the first rule here to do so. Every case
a bypass would have covered is already allowed above, so a marker would only
buy a way past a rule nobody needs to get past. The precedent is dojo-os
`pre-bash-block-main-target.sh`, which accepted `DOJO_HOTFIX_TO_MAIN=1` *and*
printed that literal in its own refusal: the thing meant to stop you handed
you the way through, and two agents filed false P0-hotfix claims that way
(DOJ-6247). A gate whose refusal message prints the way around it is not a
gate.

9 tests (4 blocking, 5 allowing). Rule count: 39 → 40.

## [1.35.0] - 2026-07-29

### Added
Expand Down
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# make-no-mistakes

**Version: 1.35.0** · [CHANGELOG](./CHANGELOG.md) · [Marketplace](https://github.com/DojoCodingLabs/make-no-mistakes-toolkit)
**Version: 1.36.0** · [CHANGELOG](./CHANGELOG.md) · [Marketplace](https://github.com/DojoCodingLabs/make-no-mistakes-toolkit)

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

Expand Down Expand Up @@ -391,6 +391,7 @@ ships 10 rules:
- `destructive-db-ops` — blocks `supabase db reset|push|repair` and inline `DROP/TRUNCATE/DELETE FROM`
- `manual-edge-fn-deploy` — blocks `supabase functions deploy` (forces CI-only deploys)
- `gcloud-missing-project` — warns when a `gcloud` subcommand is missing `--project=`
- `discard-stderr` (v1.36.0) — blocks a command that routes stderr to `/dev/null` (`2>/dev/null`, `&>/dev/null`, `>/dev/null 2>&1`). A failing command with its stderr discarded is indistinguishable from a succeeding one that printed nothing, so the empty result reads as *"none found"* rather than *"it errored"*. `cmd >/dev/null` alone is untouched — stderr still reaches you — and so is `cmd 2>&1 >/dev/null`, where stderr is duplicated to the original stdout *before* stdout is redirected and therefore survives. Same tokens, opposite outcomes; the rule matches on order. **No bypass marker** — see below.

**PreToolUse on `Edit | Write | MultiEdit` (block):**
- `minified-build-output` — blocks writing minified content to `amd/build/*.min.js` or `dist/*.min.{js,css}`
Expand Down Expand Up @@ -430,6 +431,22 @@ becomes a no-op in that repo. The current sentinel filenames are:
Bypasses are explicit acknowledgements — they sit inside the command/content
itself, not as silent flags.

**Not every rule has one, on purpose.** `discard-stderr` (v1.36.0) ships with
`bypass_marker: null`, because the legitimate cases it might otherwise need a
bypass for are already *allowed* by the rule itself: `cmd >/dev/null` for noisy
stdout, `cmd 2>&1 >/dev/null` when stderr must survive, `command -v x >/dev/null`
for existence probes, and `cmd 2>>"$log"` to keep stderr somewhere readable.
With no case left over, a marker would only buy a way past a rule nobody needs
to get past.

The general shape matters beyond this one rule: a gate whose refusal message
*prints the way around it* is not a gate. `pre-bash-block-main-target.sh` in
dojo-os accepted `DOJO_HOTFIX_TO_MAIN=1` **and** quoted that literal in its own
refusal, so typing the string it handed you WAS the authorization; two agents
filed false P0-hotfix claims that way on 2026-07-28 (DOJ-6247). A bypass is
worth its cost when it names a real case the rule cannot express. When it does
not, it is a password printed on the lock.

### Adding your own rules

Edit `hooks/rules/rules.yaml`, run `npm run build-rules`, run
Expand Down
108 changes: 108 additions & 0 deletions hooks/rules/rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -3788,5 +3788,113 @@
"expected_exit": 0
}
]
},
{
"id": "discard-stderr",
"description": "Block a command that routes stderr to /dev/null",
"applies_to": [
"Bash"
],
"match": [
{
"field": "command",
"pattern": "(2>>?[[:space:]]*/dev/null|&>>?[[:space:]]*/dev/null|>&[[:space:]]*/dev/null|[^0-9&]1?>[[:space:]]*/dev/null[[:space:]]+2>&1)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 P2 (major) — This is the compiled JSON counterpart to the pattern regex. Updating this ensures consistency with the corrected ERE pattern that matches start-of-command redirections.

[pass 1]

},
{
"field": "command",
"not_pattern": "['\\\"]2>>?[[:space:]]*/dev/null"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 P3 (minor) — This is the compiled JSON counterpart to the not_pattern quote detection improvement. Updating this ensures consistency with the rules.yaml definition and robustly prevents false positives in JSON runtime evaluations.

[pass 1]

}
],
"action": "block",
"bypass_marker": null,
"memory_ref": "feedback_never_redirect_errors_to_devnull.md",
"references": [
"dojo-os CLAUDE.md non-negotiable 24",
"DOJ-6391"
],
"message": "BLOCKED: this command discards stderr.\n\nA failing command with its stderr discarded is INDISTINGUISHABLE from a\nsucceeding one that printed nothing. The empty result then reads as\n\"none found\" rather than \"it errored\".\n\nMeasured, 2026-07-31: `gh api --jq --arg ... 2>/dev/null` -- gh rejects that\nflag combination -- produced an empty file that was reported to the team as\n\"0 red PRs\". Sixteen of 41 were red, twelve on TypeScript.\n\nKeep stderr and put it somewhere:\n cmd 2>>\"$logfile\" # separate file, exit code preserved\n cmd 2>&1 | tee \"$logfile\" # interleaved, readable now and later\n out=$(cmd 2>&1); rc=$? # captured, inspected, still branchable\n\nOnly stdout is noisy? Already allowed, unchanged:\n cmd >/dev/null # stderr still reaches you\n cmd 2>&1 >/dev/null # stderr to the ORIGINAL stdout\n command -v x >/dev/null # the existence probe still passes\n\nNo bypass marker: the alternatives above cover every legitimate case, and\na gate whose refusal hands you the way around it is not a gate (DOJ-6247).\n",
"tests": [
{
"name": "blocks-bare-2-dev-null",
"input": {
"tool_input": {
"command": "gh api foo 2>/dev/null"
}
},
"expected_exit": 2
},
{
"name": "blocks-append-form",
"input": {
"tool_input": {
"command": "node x.mjs 2>> /dev/null"
}
},
"expected_exit": 2
},
{
"name": "blocks-ordered-both-streams",
"input": {
"tool_input": {
"command": "ls -la >/dev/null 2>&1"
}
},
"expected_exit": 2
},
{
"name": "blocks-ampersand-form",
"input": {
"tool_input": {
"command": "find . -name x &>/dev/null"
}
},
"expected_exit": 2
},
{
"name": "allows-stdout-only",
"input": {
"tool_input": {
"command": "ls -la >/dev/null"
}
},
"expected_exit": 0
},
{
"name": "allows-existence-probe",
"input": {
"tool_input": {
"command": "command -v jq >/dev/null"
}
},
"expected_exit": 0
},
{
"name": "allows-reversed-order-stderr-survives",
"input": {
"tool_input": {
"command": "cmd 2>&1 >/dev/null"
}
},
"expected_exit": 0
},
{
"name": "allows-stderr-to-a-file",
"input": {
"tool_input": {
"command": "node s.mjs 2>>\"$log\""
}
},
"expected_exit": 0
},
{
"name": "allows-single-quoted-mention",
"input": {
"tool_input": {
"command": "git grep '2>/dev/null' -- scripts/"
}
},
"expected_exit": 0
}
]
}
]
76 changes: 76 additions & 0 deletions hooks/rules/rules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3127,3 +3127,79 @@
tool_input:
command: 'gcloud sql import sql my-instance gs://backups/dump.sql --database=mydb # hook-bypass: db-mutation-rule'
expected_exit: 0

- id: discard-stderr
description: Block a command that routes stderr to /dev/null
applies_to: [Bash]
match:
# Every form that sends STDERR to /dev/null. `>/dev/null 2>&1` is included
# by ORDER: stdout dies first, then stderr follows it. The reverse,
# `2>&1 >/dev/null`, duplicates stderr to the ORIGINAL stdout before stdout
# is redirected, so stderr survives -- it is deliberately not matched here.
# Two identical token sets, opposite outcomes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 P2 (major) — A command starting with a redirection (such as >/dev/null 2>&1 command or 1>/dev/null 2>&1 command) will bypass this rule entirely.

Because [^0-9&] requires at least one preceding character, there is no match when the redirection is at the very beginning of the command string. Using (^|[^0-9&]) instead resolves this loophole by correctly matching either the start of the command line or any non-digit/non-ampersand character.

[pass 1]

- field: command
pattern: '(2>>?[[:space:]]*/dev/null|&>>?[[:space:]]*/dev/null|>&[[:space:]]*/dev/null|[^0-9&]1?>[[:space:]]*/dev/null[[:space:]]+2>&1)'
# `git grep '2>/dev/null'` and similar MENTION the pattern inside single
# quotes; they perform no redirect. Mention is not execution.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 P3 (minor) — The current not_pattern only matches quotes that immediately precede the 2 (e.g., '2>/dev/null'). Any mention of 2>/dev/null nested inside a longer quoted string (e.g., git grep "some search 2>/dev/null" or echo "Note: 2>/dev/null is used") will fail to match the exclusion and will trigger a false-positive block.

Additionally, the seven backslashes in "['\\\\\\\"]" parse into ['\\\" in memory, which accidentally includes a literal backslash \ inside the character class.

Using a refined regex pattern like ('[^']*2>>?[[:space:]]*/dev/null[^']*'|\"[^\"]*2>>?[[:space:]]*/dev/null[^\"]*\") robustly handles quotes surrounding the entire substring and eliminates the accidental backslash character matching.

[pass 1]

- field: command
not_pattern: "['\\\"]2>>?[[:space:]]*/dev/null"
action: block
bypass_marker: null
memory_ref: feedback_never_redirect_errors_to_devnull.md
references:
- "dojo-os CLAUDE.md non-negotiable 24"
- "DOJ-6391"
message: |
BLOCKED: this command discards stderr.

A failing command with its stderr discarded is INDISTINGUISHABLE from a
succeeding one that printed nothing. The empty result then reads as
"none found" rather than "it errored".

Measured, 2026-07-31: `gh api --jq --arg ... 2>/dev/null` -- gh rejects that
flag combination -- produced an empty file that was reported to the team as
"0 red PRs". Sixteen of 41 were red, twelve on TypeScript.

Keep stderr and put it somewhere:
cmd 2>>"$logfile" # separate file, exit code preserved
cmd 2>&1 | tee "$logfile" # interleaved, readable now and later
out=$(cmd 2>&1); rc=$? # captured, inspected, still branchable

Only stdout is noisy? Already allowed, unchanged:
cmd >/dev/null # stderr still reaches you
cmd 2>&1 >/dev/null # stderr to the ORIGINAL stdout
command -v x >/dev/null # the existence probe still passes

No bypass marker: the alternatives above cover every legitimate case, and
a gate whose refusal hands you the way around it is not a gate (DOJ-6247).
tests:
- name: blocks-bare-2-dev-null
input: { tool_input: { command: "gh api foo 2>/dev/null" } }
expected_exit: 2
- name: blocks-append-form
input: { tool_input: { command: "node x.mjs 2>> /dev/null" } }
expected_exit: 2
- name: blocks-ordered-both-streams
input: { tool_input: { command: "ls -la >/dev/null 2>&1" } }
expected_exit: 2
- name: blocks-ampersand-form
input: { tool_input: { command: "find . -name x &>/dev/null" } }
expected_exit: 2
# The three below are the reason this rule survives contact with the repo.
# `command -v x >/dev/null` appears throughout these very hooks; a rule that
# failed here would be removed within a day.
- name: allows-stdout-only
input: { tool_input: { command: "ls -la >/dev/null" } }
expected_exit: 0
- name: allows-existence-probe
input: { tool_input: { command: "command -v jq >/dev/null" } }
expected_exit: 0
- name: allows-reversed-order-stderr-survives
input: { tool_input: { command: "cmd 2>&1 >/dev/null" } }
expected_exit: 0
- name: allows-stderr-to-a-file
input: { tool_input: { command: "node s.mjs 2>>\"$log\"" } }
expected_exit: 0
- name: allows-single-quoted-mention
input: { tool_input: { command: "git grep '2>/dev/null' -- scripts/" } }
expected_exit: 0
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lapc506/make-no-mistakes",
"version": "1.35.0",
"version": "1.36.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",
Expand Down
Loading