fix(typos-format,markdown-format): single-writer default ends the unconditional rewriter race - #1859
Conversation
…onditional rewriter race typos-format's typos_format_write_changes default flips to false (report-only out of the box; write gate requires the literal "true") and markdown-format's run is gated on a discoverable markdownlint config, so a default install has zero unconditional in-place rewriters racing on any file class. Adds the hook-budget convention doc stating the always-on cost ceiling #1809 adopted. Fixes #1809's items 1 and 2 per the delegated decision recorded on the issue. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVoZoMYXqf8ZVbQYixPVPW
…cannot resolve Review findings: an unresolvable REPO_ROOT left the config walk unbounded (empty root never matched, so it scanned to the filesystem root); the report-only header no longer claims "the default" for an explicitly-pinned false; the budget doc names its measurement baseline (pre-#1809, d5d02a2). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVoZoMYXqf8ZVbQYixPVPW
|
Claude finished @kyle-sexton's task in 1m 50s —— View job Claude finished the reviewTodo list
SummaryThis is a well-scoped, well-documented fix for the undefined-precedence race between the two unconditional in-place rewriters, per the decision recorded on #1809. I reviewed the full diff ( No blocking issues found. A few observations, all non-blocking:
I was unable to execute the hook test suites or shellcheck directly in this sandboxed environment ( |
|
Claude finished @kyle-sexton's task in 2m 0s —— View job Security Review — PR #1859
|
|
Claude has reviewed this PR 3 times. The lane skips further automatic reviews after 5; deleting this comment resets the count. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5ed23360c8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…quisite notice The markdown-format opt-in gate's contract is "no config, no run, no notice", but hook::require_jq ran first: a config-less repository without jq was still nagged once per session to install a prerequisite for a hook that repository had never enabled. The gate now runs ahead of that notice. It stays authoritative on the jq-parsed path; the pre-check fires only when jq is actually missing, decodes the jq-free raw path's JSON escapes, and requires an existing file — any escape it cannot undo yields a name nothing answers to and falls through to the notice rather than silencing it wrongly. Both setup skills still described the pre-#1809 behavior: markdown-format's told `check` that a missing config means markdownlint's defaults apply (it now means the hook is inactive), and typos-format's told `check` that an unset `typos_format_write_changes` means `true` (it now ships `false`, so report-only is what a default install does). Both are the command advertised for diagnosing why nothing is happening, so both would have named the wrong cause. Regenerates docs/CATALOG.md for the two manifest descriptions this branch changed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 3m 45s —— View job Code review — PR #1859Todo list
SummaryCommit
|
|
Claude finished @kyle-sexton's task in 2m 28s —— View job Security Review — PR #1859Scope: full diff Result: No security issues foundWhat's new since the last review — if ! command -v jq >/dev/null 2>&1; then
DECODED_FILE="${RAW_FILE//\\\"/\"}"
DECODED_FILE="${DECODED_FILE//\\\//\/}"
DECODED_FILE="${DECODED_FILE//\\\\/\\}"
if [[ -f "$DECODED_FILE" ]] &&
! markdownlint_config_discoverable "$DECODED_FILE" \
"$(hook::repo_root "$(dirname "$DECODED_FILE")")"; then
exit 0
fi
fi
hook::require_jq PostToolUse markdown-format "$INPUT"Traced this for injection/bypass risk:
Rest of the diff (re-confirmed, consistent with the prior two reviews on this PR):
No CRITICAL, IMPORTANT, or SUGGESTION-level security findings to report. (Note: the two Codex P2 comments on this PR concern setup-skill diagnostic staleness — correctness/UX, not security — and are out of scope for this lane.) |
check-silent-skips.sh reads any `exit 0` inside a `command -v` guard as a missing-CLI skip that owes the user a notice. This one is not: the verdict it reports is the opt-in's, and a repository that never enabled the hook is owed no notice about a prerequisite for it. jq's own absence stays visible for every repository that did opt in, through the hook::require_jq call below. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 1m 41s —— View job Code review — PR #1859Todo list
SummaryThe new commit
I was unable to execute Everything from the prior reviews on this PR (the fail-closed |
|
Claude finished @kyle-sexton's task in 58s —— View job Security Review — PR #1859Scope: full diff Result: No security issues foundWhat's new since the last review — Re-confirmed on the full diff (nothing new to add beyond the last two passes on this PR):
No CRITICAL, IMPORTANT, or SUGGESTION-level security findings to report. |
…at can block them (#1858) (#1860) ## Summary Stage 2 of #1809's recorded ordering decision, filed as #1858. `block-noncanonical-commit` blocked `ps::classify_git_command` rc 2 — "not faithfully tokenizable, and something git-shaped is in there" — with a message naming a canonical commit shape it never got to read, while `block-dangerous-git` blocked the same input with a message describing what was actually observed. The rc-2 arm now defers, taking the same `exit 0` the sibling content gate `block-convention-violation` already takes; both nonzero arms collapse into one deferral. The deferral emits a new telemetry `form` value, `powershell-deferred`, so it stays distinguishable from an evaluated allow — rc 1 previously exited with no telemetry record at all, which would have made the change invisible in a fleet. Also corrects five telemetry schemas (`block-noncanonical-commit`, `block-no-verify`, `block-dangerous-git`, `block-hook-bypass`, `flag-commit-pr-skill-bypass`) that described `tool` as always `"Bash"` and `subject` as always the tokenized `Bash:<first-token>` form. `hooks.json` registers all five on `Bash|PowerShell`, each emits the payload's real `tool_name`, and `hook::extract_bash_subject` returns the bare tool name for a PowerShell call rather than tokenizing it. Pre-existing drift on the same telemetry surface this change touches; descriptions corrected, no payload change. ## Premise challenge — recommend adjudicating before merge This implements #1858 exactly as decided, but the decision's justification does not survive verification. Full write-up posted on the issue; summary here. **The cited precedent is category-mismatched.** On `origin/main`, four guards call `ps::classify_git_command` with identical arguments: | Guard | ps rc=2 | Class | | --- | --- | --- | | `block-dangerous-git.sh:1072` | `exit 2` | blocking mechanic | | `block-no-verify.sh:231` | `exit 2` | blocking mechanic | | `block-noncanonical-commit.sh:797` | `exit 2` | blocking mechanic | | `block-convention-violation.sh:375` | `exit 0` | content gate | Three blocking guards hold the fail-closed posture; the one that defers is the content gate. #1858 cites that content gate as precedent for changing a blocking guard. **"Coverage cost is provably zero" is false, not merely overclaimed.** Each guard carries its own kill switch. `block_dangerous_git_enabled=false` + `block_no_verify_enabled=false` + `block_noncanonical_commit_enabled=true` now allows a git-shaped unparsable PowerShell commit through. Under a default install, and under any configuration retaining either sibling, coverage is unchanged — that narrower claim is what the CHANGELOG states, and the contract test pins the residual at exactly that width so it cannot silently widen. **Deferred alternative, named not built.** If adjudication keeps fail-closed, the motivating complaint — a double block whose second message names a commit shape the guard never read — is fixable by message accuracy rather than deletion: give `block-noncanonical-commit` and `block-no-verify` an unparsable message describing what was observed, the way `block-dangerous-git` already does. Zero coverage change. ## Test plan - `plugins/guardrails/hooks/block-noncanonical-commit.test.sh` — **172 passed, 0 failed**. New coverage: - the two rc-2 fixtures now assert `exit 0` here, relabelled "deferred — classifier rc 2" (not "allowed" — the semantic is deferral); - each fixture asserted against **both** `block-dangerous-git` and `block-no-verify`: exit 2 **and** stderr matching the unparsable reason, so a sibling blocking these for an unrelated reason cannot keep the test green while the coupling breaks; - the residual pinned directly — with both sibling kill switches off, all three guards allow the rc-2 commit. - Sibling suites, all green at HEAD: `block-dangerous-git` 319/0, `block-no-verify` 112/0, `block-convention-violation` 31/0, `flag-commit-pr-skill-bypass` 28/0. Captured before the schema-only follow-up round, and unaffected by it since no script changed: `block-hook-bypass` 211/0, `secret-pattern-detection` 42/0, `hardcoded-path-check` 72/0. - Follow-up commit `e35deb47` lands the `tool` description fix in the four sibling schemas: the first commit's search string spelled the JSON value's inner quotes unescaped, so it matched only `block-noncanonical-commit` while the quote-free `subject` correction landed in all five. Caught by Codex review on this PR; verified by re-parsing all five schemas. - `shellcheck -x` clean on all three touched shell files. - `scripts/check-changelog-parity.sh` `--check`, `--check-bump origin/main`, `--check-order` — all pass. - `scripts/check-changed-skills.sh origin/main` — no skills touched. - `markdownlint-cli2` clean on the CHANGELOG; all 31 telemetry schemas re-parse as valid JSON. ## Related - Closes #1858 - #1809 — the ordering decision; stage 1 is PR #1859 - **Constraint disposition:** #1858 restates #1809's hard constraint that the `block-hook-bypass.sh` write-bypass-leak closure must not merge before #1859. That closure is a *separate* item, not touched here — this PR's whole scope is the rc-2 deferral, so the constraint does not gate it. #1859 is still open; the write-leak work remains gated on it. - **Sibling-lane overlap:** the in-flight wrapper-argv lane (#1814/#1811/#1810, branch `fix/1814-wrapper-argv-resolver`) also edits `block-noncanonical-commit.sh` and `block-convention-violation.sh`. Different code path (`hook::git_resolve_index` rc 2, not `ps::classify_git_command` rc 2) and non-overlapping hunks. Worth noting: that lane reaches the **opposite** conclusion on its neighbouring rc-2 surface, writing the doctrine explicitly into `block-dangerous-git` and `block-no-verify` — *"Each blocking guard holds this posture itself (per-hook kill switches mean none may delegate it to a sibling)."* Independent corroboration of the premise challenge above. Based on `origin/main` only. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

Fixes #1809
Summary
Implements the single-writer-by-default decision recorded on #1809 (delegated-authority comment), dissolving the undefined-precedence race between the marketplace's two unconditional in-place rewriters instead of adjudicating it:
typos_format_write_changesdefault flipstrue→falsein both the manifest and the script fallback. Out of the box the hook is report-only and never modifies a file; write mode is an explicit opt-in whose gate now requires the literaltrue(the mutating direction needs the exact opt-in spelling). Removes the silent-corruption class including typos-format: hook silently corrupts abbreviated git SHAs, including inside backticks #1257, and lands before anyblock-hook-bypasswrite-leak closure per the hard ordering constraint in the issue.--fixrewrites nor default-rule findings (the ~115-finding MD013 class) are imposed on a repo that never chose a Markdown style. Same doctrine asbash-format's shfmt gate. Apackage.jsonmarkdownlint-cli2property does not open the gate (honored by markdownlint-cli2 only under an explicit--configflag; its README, fetched 2026-07-31).docs/conventions/hook-budget/README.md(new): the always-on cost ceiling marketplace: two unconditional in-place rewriters race on every Write with no defined precedence, nobody sums the always-on per-turn cost, and thirteen CRITICALs need an ordering #1809's item 2 asked for — ≤ 1 s typical / 2 s worst-case parallel wall per tool call, ≤ 500 ms per turn — with the measured 2026-07-31 accounting (per-Bash-call always-on set ≈ 5.9 s parallel wall on the Windows reference host; per-Write set ≈ 1.9 s) and the rule that the ceiling never relaxes to absorb an overage.Resulting invariant: a default install has zero unconditional writers on any file class. The one re-openable overlap — typos write mode opted on in a repo that also carries a markdownlint config — is a deliberate double opt-in documented in both READMEs as last-writer-wins. Residual scoped-writer overlap stays tracked in #875; the evidence-packet integrity item routes to #1808; stage 2 of the ordering (guardrails rc=2 one-liner) is #1858.
Test plan
plugins/typos-format/hooks/typos-format.test.sh: 81/81 pass. Newstub/default-report-onlycase pins the race fix — with no write-mode option set the file stays byte-identical while findings are still reported (fails if the default ever flips back); explicit-false override retained; all write-path disclosure/scale/telemetry contracts re-pinned under the opt-in.plugins/markdown-format/hooks/markdown-format.test.sh: 120/120 pass. New opt-in-gate cases: a config-less repo with fixable violations is left byte-identical with no output; a config nested below the root still opens the gate and--fixapplies.check-shell-portability --paths(clean),check-changelog-parity--check/--check-bump origin/main/--check-order(all pass),check-silent-skips,check-hook-userconfig-argv,check-changed-skills(no skills changed), markdownlint-cli2 on all five touched/added Markdown files (0 issues), manifests parse.Related
block-noncanonical-commitrc=2 exit), staged per the ordering decision🤖 Generated with Claude Code
https://claude.ai/code/session_01FVoZoMYXqf8ZVbQYixPVPW