Skip to content

fix(markdown-format): stop reading a missing git as "file is out of tree" - #2121

Merged
kyle-sexton merged 6 commits into
mainfrom
fix/markdown-format-stranded-1030
Aug 10, 2026
Merged

fix(markdown-format): stop reading a missing git as "file is out of tree"#2121
kyle-sexton merged 6 commits into
mainfrom
fix/markdown-format-stranded-1030

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

No linked issue

Summary

Discharges stranded review thread PRRT_kwDOTCGFQM6TzHVg on merged PR #1030, filed against
plugins/markdown-format/hooks/markdown-format.sh:73 and never resolved.

The working-tree membership scope added in 0.6.3 probes with git rev-parse --show-toplevel, which
fails identically when the file is outside every working tree and when git is not installed on
PATH. On a POSIX host without git, that read every Markdown edit as out-of-tree, so the hook
skipped all of them — silently, repo-wide, with jq and markdownlint-cli2 present and a
markdownlint config in place.

Git is not a documented prerequisite of this hook: the README "Requirements" section lists Bash,
jq and markdownlint-cli2, the setup skill checks those three, and hook::repo_root has always
tolerated git being absent by falling back to the file's directory.

Fix

Gate the skip on git being available, so an undecidable verdict lints rather than skips — the
same direction file_is_gitignored already documents for the same input:

-  if ! in_git_working_tree "$(dirname "$FILE_PHYSICAL")"; then
+  if command -v git >/dev/null 2>&1 &&
+    ! in_git_working_tree "$(dirname "$FILE_PHYSICAL")"; then
     exit 0
   fi

The scope is unchanged wherever git can answer, and the fail-closed symlink-escape check ahead of it
is untouched.

The reviewer offered two routes — preserve the git-optional behaviour, or make git an explicit,
visibly checked prerequisite. This takes the first, because the second would add a hard dependency
the README, the setup skill and hook::repo_root all currently deny.

Why the resulting fail-open is bounded

Exposure is bounded by the consumer opt-in gate, not by this scope. Without git, hook::repo_root
cannot resolve a working-tree top and falls back to the edited file's own directory, so
markdownlint_config_discoverable searches that single directory — a scratch /tmp/comment-body.md
still does not lint unless /tmp itself carries a markdownlint config. The noise class this scope
exists to stop stays stopped wherever git can actually answer.

Tests

markdown-format.test.sh: 133 pass, 0 fail on this branch.

The new cases hide git from PATH via BASH_ENV, the same technique the existing
markdownlint-cli2 PATH-hiding case uses, and it is exact for this predicate — command -v git
fails and a direct git call exits 127.

They carry a control: "git absent with CLAUDE_PROJECT_DIR set still lints", which proves the
git-absence shim does not disable the hook independently of the membership scope. Without it, a
green assertion would be consistent with the shim having broken the hook outright.

Assertions are made on the file's bytes, not on stdout: without git the finding digest and the
hook's own reporting differ, so stdout is not a stable oracle for "did this file get linted".

The pre-fix behaviour is not in doubt from the diff — with no command -v git guard,
in_git_working_tree returns non-zero when git is missing, ! inverts it, and the exit 0 skip
fires. I did not execute the suite against the pre-fix tree; stating that rather than implying a
run I did not do.

Related

kyle-sexton and others added 2 commits August 9, 2026 18:27
…ree"

The working-tree membership scope added in 0.6.3 (#1030) probes with
`git rev-parse --show-toplevel`, which fails identically when the file is
outside every working tree and when git is not installed. On a POSIX host
without git that read every Markdown edit as out-of-tree, so the hook skipped
all of them — silently, repo-wide, with jq and markdownlint-cli2 present and a
markdownlint config in place. Git is not a documented prerequisite of this
hook: README "Requirements" lists Bash, jq and markdownlint-cli2, the setup
skill checks those, and hook::repo_root has always tolerated git being absent.

Gate the skip on git being available, so an undecidable verdict lints instead
of skipping — the direction file_is_gitignored already documents for the same
input. The scope is unchanged wherever git can answer, and the fail-closed
symlink-escape check ahead of it is untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 10, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ff22dcbc86

ℹ️ 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".

Comment thread plugins/markdown-format/hooks/markdown-format.test.sh
… is absent

Merges origin/main (version collision 0.10.2 vs main's 0.11.0, resolved to
0.11.1) and closes the review finding on this PR, which was correct.

Gating the membership skip was not enough on its own. Config discovery walks UP
from the edited file and stops at hook::repo_root, which without git returns
the hint it was given — the file's own directory. Root and start were the same
directory, the walk terminated immediately, and a repository whose markdownlint
config sits at its root stopped linting everything below it. Reproduced:

    git PRESENT:  root-level.md LINTED   docs/nested.md LINTED
    git ABSENT:   root-level.md LINTED   docs/nested.md SKIPPED

CLAUDE_PROJECT_DIR answers the same question without git and is now preferred
as the walk's terminator when the git probe cannot resolve a working-tree top.
Terminator only, never a scope widening — discovery still starts at the file
and still stops at a root — so markdownlint_config_discoverable's fail-closed
reasoning is unchanged.

The capability is probed by running `git rev-parse --show-toplevel`, not by
testing `command -v git`. A first attempt used `command -v` and silently did
nothing: the suite's git-absence shim is an exported bash FUNCTION, which
`command -v git` reports as present. `command -v` tests for a NAME, not a
capability, and answers yes for a function, a PATH stub, and a real binary
standing in a directory that is no repository alike — every case where this
fallback still applies.

The reviewer's point about the existing control was exact: it passes only
because its fixture sits beside the root config. The new case nests the
fixture, which is the whole assertion; a root-level twin passes either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 54s —— View job


Code review — PR #2121

  • Gather context (PR diff, changed files)
  • Review markdown-format.sh fix logic
  • Review markdown-format.test.sh new tests
  • Review README/CHANGELOG/plugin.json changes
  • Post findings

Scope reviewed: the 5 changed files (markdown-format.sh, markdown-format.test.sh, README.md, CHANGELOG.md, plugin.json), diffed against origin/main.

Summary

Both fixes in this PR do what they claim:

  1. command -v git now gates the working-tree membership skip (commit 49ffb66), so a git-less host lints instead of silently skipping every Markdown edit. Traced through in_git_working_tree/hook::repo_root — correct.
  2. The follow-up (commit f8269d1) closes the exact gap flagged by the Codex bot review: without git, hook::repo_root falls back to the file's own directory, so a nested .md never reached a root-level markdownlint config. Preferring CLAUDE_PROJECT_DIR as the walk terminator when git can't resolve a toplevel fixes it, and — unlike the control fixture the bot pointed out, which sat beside the config and proved nothing — the new docs/fixtureNoGitNested.md case actually nests the fixture, so it exercises the real bug. I traced the REPO_ROOT/CONFIG_ROOT override through markdownlint_config_discoverable and collect_risky_configs and it's consistently used as a terminator only, never to widen scope, matching the stated intent.

One consistency finding posted inline: the new fallback probe at markdown-format.sh:222-224 doesn't clear GIT_DIR/GIT_WORK_TREE/etc. before calling git rev-parse, unlike the two sibling probes in the same file that clear that exact env for the identical hazard. Low severity — hook::repo_root's own internal probe has the same pre-existing gap, so the two calls tend to agree — but worth aligning with the file's established discipline.

Nothing else stood out:

  • README.md/CHANGELOG.md: accurate. The README's new "git is not required" paragraph scopes the remaining config-discovery narrowing specifically to the CLAUDE_PROJECT_DIR-unset case, which stays true after the second commit's fix (that fix only applies when CLAUDE_PROJECT_DIR is set) — no doc/code mismatch.
  • plugin.json: 0.11.0 → 0.11.1 patch bump is appropriate for a bugfix.
  • Tests: the new cases correctly use a BASH_ENV-injected git/command shim (the exact technique already used for the markdownlint-cli2 PATH-hiding case) rather than PATH manipulation, which the comments correctly note is necessary for Git Bash portability. The control case (git absent + CLAUDE_PROJECT_DIR set still lints) is a real control — it isolates the membership-scope variable as the PR body claims. I did not execute the suite myself (no shell access in this review lane); the PR's "133 pass, 0 fail" is author-claimed and I have not independently verified it.

Note: the PR description covers only the first commit's fix; it predates the second commit that addresses the Codex review comment, so it doesn't mention the nested-config-discovery fix or its test. Not a blocker, just worth folding in if the description gets updated.

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 19s —— View job


Security review complete ✅

  • Gather PR diff and read changed files
  • Review markdown-format.sh logic change for security implications
  • Review test file changes
  • Review README/CHANGELOG/plugin.json changes
  • Post findings

Scope: the 5 files changed in this PR (markdown-format.sh, markdown-format.test.sh, README.md, CHANGELOG.md, plugin.json), diffed against origin/main.

No security issues found.

Summary of what I checked in the two behavior-changing hunks:

  1. Gating the membership skip on command -v git (markdown-format.sh#L196-L199) only changes what happens when git is absent: the hook now proceeds to lint instead of skipping. The exposure this opens is bounded and already reasoned about in the PR body — without git, hook::repo_root (unchanged) falls back to the file's own directory, so markdownlint_config_discoverable only searches that one directory. An out-of-tree scratch file (e.g. /tmp/comment-body.md) still isn't linted unless /tmp itself carries a markdownlint config. The executable-config trust gate (resolve_trust_dir / RISK_CONFIGS) is untouched and still requires an explicit approval before any .cjs/.mjs config is trusted, regardless of how REPO_ROOT was derived.

  2. Preferring CLAUDE_PROJECT_DIR as the config-discovery root terminator (markdown-format.sh#L221-L225) only fires when CLAUDE_PROJECT_DIR is set. In that branch FILE has already passed hook::read_file_path's canonicalized membership check against CLAUDE_PROJECT_DIR (symlink-resolved, boundary-anchored, with the temp-tree exemption), so widening REPO_ROOT to CLAUDE_PROJECT_DIR doesn't admit any path that wasn't already confirmed to be inside the trusted project tree. The re-verification via a live git -C ... rev-parse --show-toplevel call (rather than trusting command -v git alone) correctly avoids misreading a shell function/PATH stub as a real git binary, per the code's own comment.

Other notes:

  • No workflow, permissions, or dependency-pin changes — plugin.json, README.md, CHANGELOG.md are documentation/version-bump only.
  • Quoting is consistent throughout the new code ("$(dirname "$FILE")", "$REPO_ROOT", etc.) — no unquoted expansion of attacker-influenced path data, no eval, no new subprocess invocation built from untrusted input.
  • The new test cases (markdown-format.test.sh) use a BASH_ENV shim with a fully single-quoted heredoc (<<'EOF') to hide git — no variable interpolation, so no injection surface in the test fixture itself.

The one review comment already on this PR (Codex, on markdown-format.test.sh:480) is a test-coverage gap (a nested-file case isn't exercised in the no-git control), not a security finding, so it's out of scope for this security-only pass.

Comment thread plugins/markdown-format/hooks/markdown-format.sh
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

Claude has reviewed this PR 4 times. The lane skips further automatic reviews after 5; deleting this comment resets the count.

… probe

The two sibling probes in this file — in_git_working_tree and
file_is_gitignored — both unset GIT_DIR, GIT_WORK_TREE, GIT_COMMON_DIR,
GIT_CEILING_DIRECTORIES and GIT_DISCOVERY_ACROSS_FILESYSTEM before asking git
anything, because an inherited GIT_DIR or GIT_WORK_TREE from whatever launched
the session lets some OTHER repository answer the question. The new toplevel
probe asks the structurally identical question and did not.

Here a spurious SUCCESS is the harmful direction: it withholds the
CLAUDE_PROJECT_DIR fallback and leaves config discovery terminating at the
edited file's own directory — precisely the bug the surrounding block exists to
fix. Cleared in a subshell so the surrounding process keeps its environment.

The nested reproduction still passes on both sides:

    git PRESENT:  root-level.md LINTED   docs/nested.md LINTED
    git ABSENT:   root-level.md LINTED   docs/nested.md LINTED

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 18s —— View job


Code review — PR #2121

  • Gather context (PR diff, changed files)
  • Review markdown-format.sh — focus on the new GIT_DIR/env-clearing fix in the latest commit
  • Review markdown-format.test.sh changes
  • Review README/CHANGELOG/plugin.json changes
  • Post findings

Scope reviewed: the 5 files changed in this PR, at HEAD c8e5112, per the code-review lane's split (security concerns are left to the dedicated security lane, which already ran on this PR).

The previously-flagged env-clearing gap is fixed

Commit c8e5112 closes exactly the P3 finding from the prior review pass: the new REPO_ROOT fallback probe at markdown-format.sh:230-237 now wraps git -C ... rev-parse --show-toplevel in a subshell that clears GIT_DIR/GIT_WORK_TREE/GIT_COMMON_DIR/GIT_CEILING_DIRECTORIES/GIT_DISCOVERY_ACROSS_FILESYSTEM, matching in_git_working_tree and file_is_gitignored. Diffed the commit in isolation — it touches only that probe and its comment, nothing else. Traced the logic: the override fires only when CLAUDE_PROJECT_DIR is set, REPO_ROOT (from hook::repo_root) equals dirname(FILE), and a re-probe (with a clean environment) still can't resolve a toplevel — correct in every case I traced, including the ordinary "file sits at the actual repo root" case, where the re-probe succeeds and no override happens.

One test-coverage finding (posted inline)

markdown-format.sh:230-237 (the new CLAUDE_PROJECT_DIR walk-terminator fallback) has no test coverage with git actually present. Every CLAUDE_PROJECT_DIR= test in markdown-format.test.sh runs under the no-git BASH_ENV shim; every other test unsets CLAUDE_PROJECT_DIR. So "CLAUDE_PROJECT_DIR set + git present" — the default, ordinary session in a git repo, and the path this new probe actually runs on for nearly every real edit — is never exercised. Detail and a suggested fourth test case are in the inline comment on markdown-format.test.sh:512.

Everything else

  • README.md / CHANGELOG.md: both stayed accurate through the second commit — the README's new "git is not required" paragraph scopes the remaining narrowing to the CLAUDE_PROJECT_DIR-unset case, which is still true (the f8269d1/c8e5112 fix only applies when CLAUDE_PROJECT_DIR is set).
  • plugin.json: 0.11.0 → 0.11.1 is the right bump for a bugfix-only release.
  • I did not execute the test suite myself in this pass (no non-interactive shell access in this lane) — the PR's "133 pass, 0 fail" and the reviewer-executed pre-fix/post-fix reproductions in the thread are author/reviewer-claimed, not independently verified by me here.
    — branch fix/markdown-format-stranded-1030

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 22s —— View job


Security review complete

  • Gather PR diff (gh pr diff, base origin/main)
  • Read changed files at head SHA c8e5112
  • Review markdown-format.sh logic changes for security implications
  • Review markdown-format.test.sh changes
  • Review README/CHANGELOG/plugin.json changes
  • Post findings

Scope: the 5 files changed in this PR (markdown-format.sh, markdown-format.test.sh, README.md, CHANGELOG.md, plugin.json), diffed against origin/main, at HEAD c8e5112.

No security issues found.

I re-verified the two behavior-changing hunks independently of the two automated reviews already on this PR:

  1. Gating the membership skip on command -v git (markdown-format.sh#L191-L200) — only changes behavior when CLAUDE_PROJECT_DIR is unset and git can't answer. The fail-open this opens is bounded by the caller-supplied opt-in gate below it, not by this scope: hook::repo_root (unchanged, hook-utils.sh#L315-L325) falls back to the edited file's own directory when git can't resolve a toplevel, so markdownlint_config_discoverable only ever searches that single directory — an out-of-tree scratch file still isn't linted unless its own directory carries a markdownlint config.
  2. Preferring CLAUDE_PROJECT_DIR as the config-discovery root terminator (markdown-format.sh#L229-L237) only fires when CLAUDE_PROJECT_DIR is set. In that branch FILE has already passed hook::read_file_path's canonicalized (symlink-resolved) membership check against CLAUDE_PROJECT_DIR (hook-utils.sh#L273-L307), so widening REPO_ROOT/CONFIG_ROOT to CLAUDE_PROJECT_DIR never admits a path outside the boundary already established for the whole session. The re-verification probe runs a live git -C ... rev-parse --show-toplevel (not just command -v git), and correctly clears GIT_DIR/GIT_WORK_TREE/GIT_COMMON_DIR/GIT_CEILING_DIRECTORIES/GIT_DISCOVERY_ACROSS_FILESYSTEM first — this is the fix for the P3 consistency gap I flagged on the prior run of this PR, and it now matches the discipline in_git_working_tree/file_is_gitignored already apply for the identical hazard (an inherited GIT_DIR making a wrapper's repository answer instead of the target directory's own).

Other checks, no findings:

  • The executable-config trust gate (resolve_trust_dir / RISK_CONFIGS / collect_module_files) is untouched by this PR — still requires an explicit per-signature approval before any .cjs/.mjs config or referenced module executes, regardless of how REPO_ROOT was derived.
  • Quoting is consistent throughout the new code ("$(dirname "$FILE_PHYSICAL")", "$REPO_ROOT", "$CLAUDE_PROJECT_DIR") — no unquoted expansion of attacker-influenced path data, no eval, no new subprocess built from untrusted input.
  • markdown-format.test.sh's new BASH_ENV shim (markdown-format.test.sh#L189-L201) is test-only, uses a fully single-quoted heredoc (no variable interpolation), and never ships in the plugin itself.
  • README.md/CHANGELOG.md/plugin.json: documentation and a patch version bump only — no config, permissions, or dependency-pin changes.

One residual behavior worth noting for completeness, not filed as a finding: the membership-skip gate at L196 still uses command -v git (rather than a live probe) before calling in_git_working_tree. If git resolves to a callable name that isn't a working git (e.g., a shell function that always errors), in_git_working_tree can't distinguish that from "genuinely outside every working tree," and the skip still fires. This is the same class of ambiguity the whole PR narrows, just not fully closed for that one edge case — it's an availability/completeness edge case (worst case: a markdown edit isn't linted), not a trust-boundary or authorization gap, so it doesn't belong in a security-only pass.

Comment thread plugins/markdown-format/hooks/markdown-format.test.sh
…and fix a typo

Two things, both from review of this PR.

The override added here had NO coverage for the case it executes on nearly
every real Markdown edit — CLAUDE_PROJECT_DIR set AND git present. Every other
case in this file runs either `-u CLAUDE_PROJECT_DIR` or under the git-absence
shim, so the ordinary path was invisible to the whole suite.

Both halves are needed, because "it still lints" cannot distinguish a
correctly-inert override from one that fired and happened not to change the
outcome:

- positive: git present, CLAUDE_PROJECT_DIR set, a nested .md still lints.
- negative, and the load-bearing one: CLAUDE_PROJECT_DIR pointed at a directory
  ABOVE the repository root carrying its own markdownlint config. With git
  present the walk must stop at the git toplevel, so that config must NOT be
  reached. A wrongly-firing override widens discovery past the repo root and
  lints the file, which this case makes observable.

Also fixes the typos gate: `coreutil` -> `coreutils` in a comment. That was the
real cause of the red `ci-status` — every hygiene sub-check the aggregate
printed said success, and `typos=failure` sat further up the log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 5m 10s —— View job


Code review — PR #2121 (new commit f87ac6a)

  • Gather context (diff since last review at c8e5112)
  • Review the new commit addressing the P2 test-coverage finding
  • Review the typo fix
  • Post findings

Scope reviewed: this review lane's prior pass on this PR ended at c8e5112, which had one open P2 test-coverage finding. This pass covers only what changed since then — commit f87ac6a (test(markdown-format): cover the REPO_ROOT override's ordinary case, and fix a typo), the sole new commit, at HEAD f87ac6a.

The P2 finding from the prior pass is not actually closed (posted inline)

The prior review flagged that the new REPO_ROOT fallback (markdown-format.sh:230-237) had no test coverage with CLAUDE_PROJECT_DIR set and git present — the path nearly every real edit takes. f87ac6a adds two new cases aimed at that gap, but tracing them against the actual guard (markdown-format.sh:230, "$REPO_ROOT" == "$(dirname "$FILE")") shows neither one reaches the branch:

  • Both new fixtures (GITON_NESTED, OUTER_FIXTURE) sit under a docs/ subdirectory. With git present, hook::repo_root resolves REPO_ROOT to the git toplevel, which is never equal to a nested file's dirname — so the guard's second condition is always false and the if (including the env-cleared re-probe from c8e5112) never executes, independent of CLAUDE_PROJECT_DIR.
  • The "load-bearing" negative case additionally can't discriminate even if the nesting were fixed: its outer config ($OUTER/.markdownlint.jsonc) is an empty {}, and the fixture is a single-item list, so default MD004 consistent style has nothing to flag — the file's bytes come out identical whether the walk correctly stopped at the repo root or incorrectly widened to the outer config.

Full detail and a concrete fix (root-level fixture + a forced MD004 rule on the outer config, mirroring the fixture's own repo config) are in the inline comment on markdown-format.test.sh:534-555. Net effect: the REPO_ROOT-override branch — including the env-clearing fix from c8e5112 — is still exercised by zero tests in the suite.

Everything else in this commit

  • The coreutilcoreutils binary comment fix (markdown-format.test.sh:431) is accurate and matches the stated cause of the red typos check.
  • The commit also drops a trailing ; before the closing } in crlf_escaped() { case "$1" in *'\r'*) return 0 ;; *) return 1 ;; esac } (line ~1832) — not mentioned in the commit message, but harmless: esac already ends the case statement in command position, so } doesn't need a preceding ; there. Just a stray, undescribed edit riding along with the described typo fix — not a defect.
  • I did not execute the test suite myself in this pass (no non-interactive shell execution available in this lane) — the analysis above is a static trace of the guard condition and markdownlint_config_discoverable's file-existence-only check, not a run.
    — branch fix/markdown-format-stranded-1030

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 12s —— View job


Security review complete

  • Gather PR diff at new HEAD (f87ac6a) — determine what changed since the last security pass (c8e5112)
  • Review the delta for security implications
  • Re-confirm no regressions in previously-reviewed hunks
  • Post findings

Scope: the 5 files changed in this PR, at HEAD f87ac6a, diffed against origin/main.

No security issues found.

This run was triggered by the sync to commit f87ac6a ("test(markdown-format): cover the REPO_ROOT override's ordinary case, and fix a typo"). I diffed c8e5112 (the HEAD of the prior security pass) against f87ac6a directly and confirmed the delta touches only markdown-format.test.sh:

  • Two new test cases exercising the CLAUDE_PROJECT_DIR-set + git-present path through the REPO_ROOT override — this is exactly the P2 test-coverage gap flagged in the code-review lane, now closed. One asserts the override still lets a nested .md lint (positive case); the other plants a markdownlint config in a directory above a nested git repo and asserts discovery stops at the git toplevel rather than widening to CLAUDE_PROJECT_DIR (negative case, the one that actually pins the override to its guard rather than just observing "still lints").
  • A one-word comment typo fix (coreutilcoreutils binary).
  • A no-op style change to a case statement's closing syntax (esac; }esac }), same behavior.

markdown-format.sh — the file carrying every security-relevant hunk (the command -v git membership-skip gate, the CLAUDE_PROJECT_DIR walk-terminator override, and the env-clearing subshell probe) — is unchanged since c8e5112, which I already reviewed in full on the prior pass with no findings:

  1. The command -v git gate only changes behavior when git is absent, and the resulting fail-open is bounded by hook::repo_root's unchanged single-directory fallback.
  2. The CLAUDE_PROJECT_DIR terminator override only fires post-hook::read_file_path canonicalized membership check, so it can't admit a path outside the already-established trust boundary, and its re-probe now correctly clears GIT_DIR/GIT_WORK_TREE/GIT_COMMON_DIR/GIT_CEILING_DIRECTORIES/GIT_DISCOVERY_ACROSS_FILESYSTEM before asking git.
  3. The executable-config trust gate (resolve_trust_dir / RISK_CONFIGS) remains untouched by this PR.

The new test fixtures (temp git repos under $WORK, env CLAUDE_PROJECT_DIR=... invocations) are test-only, use quoted/literal paths with no untrusted interpolation, and never ship in the plugin.

plugin.json / README.md / CHANGELOG.md are unchanged since the prior pass — version bump and documentation only.
— branch fix/markdown-format-stranded-1030

Comment thread plugins/markdown-format/hooks/markdown-format.test.sh Outdated
@kyle-sexton

Copy link
Copy Markdown
Contributor Author

Offering a branch rather than pushing to yours — fix/markdown-format-stranded-1030 is under active edit and I would rather not race you. Take it, take part of it, or ignore it; the choice is yours. I have not resolved any thread.

Branch: fix/markdown-format-nogit-root-2121, cut from your head f87ac6ab, re-resolved with git ls-remote immediately before I pushed and again just now — still f87ac6ab.

commit takeable alone
1 df7bf91663b7e75f2efb82c9467b7ff56c7ef779 — two failing tests yes (red by design)
2 dc5c666cf247994baa97d0d2797473fd5151b0f1 — the fix + CHANGELOG needs 1 for its evidence
3 99fd94b1d874f4e8287774b750d0b2017ce43af0 — coverage for a branch commit 2 introduces needs 2

I started this before f8269d1b landed and re-cut twice as you pushed. Your fix already covers the anchored case; what follows is the delta, not a replacement for your work.


1. A defect report, which stands whether or not you take any of the above

The guard [[ "$REPO_ROOT" == "$(dirname "$FILE")" ]] at markdown-format.sh:230 is true only for a file sitting at the repository root. There it spawns a second git rev-parse --show-toplevel whose result is discarded.

Measured with the PATH-shim technique your own telemetry spawn-budget tests use (a git shim that logs then execs the real git), counting spawns for one hook invocation:

fixture ff22dcbc (pre-fix) f87ac6ab
root-level .md, CLAUDE_PROJECT_DIR set, git's path spelling 2 3
root-level .md, CLAUDE_PROJECT_DIR set, shell path spelling 2 2
nested .md, CLAUDE_PROJECT_DIR set, either spelling 2 2

I have to correct my own first framing of this, because I measured it and was wrong: it is not unconditional, and it is not the ~140 ms Git Bash cost I expected. The comparison is between a git-produced path and a shell-produced one, and on Windows Git Bash those never match — rev-parse --show-toplevel answered C:/Users/.../consumer where dirname gave /tmp/tmp.XXXX/consumer. So on Git Bash the extra spawn does not happen. On Linux/macOS the two spellings are identical, so it fires on every root-level Markdown edit — README.md, CHANGELOG.md. That is one cheap fork on the platform where forks are cheap, so: minor, real, and worth knowing before the guard hardens into precedent.

This is the same guard as the two open P2 coverage threads (PRRT_kwDOTCGFQM6Xt_5r, PRRT_kwDOTCGFQM6XuOIH), seen from the other side. The reviewer's point is that the guard is false for a nested file, so a nested fixture cannot reach the branch. Mine is that it is true at the root, so a root-level file pays for it. One guard, one nesting condition, two consequences — and they are jointly exhaustive, which is why no single fixture shape can both exercise the branch and avoid the cost.

2. A design proposal, which is optional and larger than the two remaining gaps strictly require

Two gaps are still open at f87ac6ab:

(a) CLAUDE_PROJECT_DIR unset. This is the strongest point and I would rather not have it buried. The membership scope this PR exists to fix is gated on

if [[ -z "${CLAUDE_PROJECT_DIR:-}" ]]; then

and your primary no-git fixture runs unset via run_hook_no_git. So unset is the configuration the PR is about, and a root read off CLAUDE_PROJECT_DIR cannot serve it by construction. The review comment's "leaving the normal nested-docs case unfixed" is still literally true for that half.

(b) The opt-in pre-check at markdown-format.sh:119 still calls hook::repo_root directly. With git and jq both absent and the file nested, it reads a repository that did opt in as one that never did, and swallows the jq notice it is owed. No existing case can reach it: every jq-absence test runs with git present, every git-absence test runs with jq present.

My branch replaces the guard with resolve_repo_root, which recognises hook::repo_root's fallback by its signature (an answer equal to the hint) and then does the walk git's own discovery does — upward for a .git entry, accepted as a directory or as a file, since a linked worktree and a submodule write a file (gitrepository-layout). git's answer is returned untouched whenever git produced one, so a host with git is unaffected and no second probe is spawned. CLAUDE_PROJECT_DIR stays as the last resort for a project that is no working tree at all — an unpacked archive, a vendored copy — so your case is subsumed rather than discarded. Reading the filesystem instead of asking git also means an inherited GIT_DIR/GIT_WORK_TREE cannot answer for another repository, which is the protection in_git_working_tree buys by unsetting them, here for free.

Deleting the guard also dissolves both P2 threads: there is no path-form equality left to be true-at-the-root and false-when-nested, and the CLAUDE_PROJECT_DIR branch becomes reachable by an ordinary fixture (commit 3 does exactly that, and I revert-probed it — cut the clause and the fixture is skipped rather than linted, so it is not vacuous).

The option I would not choose, since you should have it too: both gaps can be closed far more narrowly — extend your existing condition to fall back to CLAUDE_PROJECT_DIR when set and keep the current guard, then route line 119 through the same block. That leaves gap (a) open on a git-less host with no harness anchor, and keeps the guard and its two threads, but it is a much smaller diff on a branch you are actively editing. Commit 1 of my branch is useful to you either way: it is just the two failing tests.

3. Evidence

Same host, same shell, bash plugins/markdown-format/hooks/markdown-format.test.sh.

baseline at f87ac6ab (twice, identical)     PASS=136 FAIL=0
+ commit 1 (tests only)                     PASS=136 FAIL=2
    FAIL: git absent + unset: nested .md skipped, nothing anchored the root
    FAIL: missing jq + missing git, nested .md -> notice swallowed,
          the pre-check read an opted-in repo as opted-out
+ commit 2 (the fix)                        PASS=138 FAIL=0
+ commit 3 (last-resort coverage)           PASS=139 FAIL=0

No pre-existing failure at any point; the only skip is symlink-escape case SKIPPED (host cannot create real symlinks), present identically in every run including the baseline. Your two new git-present override tests pass unchanged under the replacement.

Also clean on every file I touched: shellcheck -x -S warning, scripts/check-shell-portability.sh --paths, scripts/check-silent-skips.sh --paths, and markdownlint-cli2 on the CHANGELOG.

4. What I did not do, and one thing I could not verify

hook::repo_root in lib/hook-utils.sh has this limitation for the ~14 other hooks that call it, and I deliberately did not touch it: CI enforces sync-hook-utils.sh --check-bump, so the edit fans out to sixteen plugin copies plus their manifest versions — not cherry-pickable onto this PR — and plugins/guardrails/hooks/hook-utils.sh is under concurrent edit for #2122. A plugin-local resolver is the containable fix; the shared one is a separate decision.

Unverified: I could not observe the POSIX-host spawn count on a real POSIX host. The f87ac6ab row showing 3 was produced by addressing the repo in git's own spelling on this Windows host, which reproduces the equality condition a POSIX host has natively — a faithful simulation, not the platform itself.

I also saw one unexplained PASS=133 FAIL=1 on a single run at the earlier head c8e5112c, then 134/0 on three further runs there, and did not capture which assertion failed. I could not reproduce it in five runs at f87ac6ab and cannot name it, so I am flagging it as an unconfirmed possible intermittent rather than a finding. It did not recur in any run backing the numbers above.

…ch, drop the inert twin

Both cases added in f87ac6a were inert, for two independent reasons, and both
passed anyway — the exact defect they were added to close.

The guard is `"$REPO_ROOT" == "$(dirname "$FILE")"`. Both fixtures were NESTED,
so with git resolving a toplevel dirname is `<repo>/docs` while REPO_ROOT is
`<repo>`; they never match, the whole `if` short-circuits, and the override
branch never executed. Separately, the outer config was `{}`, leaving MD004 on
"consistent", which has nothing to flag on a single-item list — so even a
wrongly-widened walk produced byte-identical output and the negative could not
have failed either.

The positive is rewritten to reach the branch and to be load-bearing: git
present and working, but the file sits at the root of a directory that is NOT a
repository, so hook::repo_root falls back to the hint, the guard matches, the
probe fails, and CLAUDE_PROJECT_DIR terminates the walk. Proven discriminating
against the pre-override hook:

    POSITIVE (dir is not a repo):  PRE=SKIPPED  POST=LINTED
    NEGATIVE (dir is a repo):      PRE=SKIPPED  POST=SKIPPED

The outer config now pins MD004 to "dash" so a wrong walk is observable.

The negative twin is dropped rather than shipped, because three instruments
were tried and none can fail — and the reason is a fact about the code, not the
fixtures. Forcing the override to fire (probe replaced by `false`) still
rewrites nothing, because markdownlint-cli2 does its own config discovery and
does not cross the repository boundary. Telemetry's data.file is derived from
REPO_ROOT but emits empty under the forced override, which is indistinguishable
from a sink that never populated. Exit status is 0 either way. The reasoning is
recorded in the test file so it is not re-derived.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 47s —— View job


Code review — PR #2121 (new commit 406fd36)

  • Gather context (diff since last code-review pass, which ended at f87ac6a)
  • Review the new commit 406fd36 against the open P2 finding
  • Trace the new test's guard/config-discovery logic statically
  • Post findings

Scope reviewed: the prior code-review pass on this PR ended at f87ac6a with one open P2 finding (the REPO_ROOT-override test didn't reach its own guard). This pass covers only the delta since then — commit 406fd36 ("test(markdown-format): make the override case actually reach the branch, drop the inert twin"), the sole new commit, which touches only markdown-format.test.sh (git diff f87ac6a..406fd36 --stat: 1 file, +51/-28).

The P2 finding is now actually closed

Traced the new NONREPO_ROOTED fixture (markdown-format.test.sh:544-555) against the guard at markdown-format.sh:230 ("$REPO_ROOT" == "$(dirname "$FILE")"):

  • NONREPO_ROOTED="$OUTER/plain/fixtureOverrideFires.md", and $OUTER/plain is never git init'd, so hook::repo_root returns its hint ($OUTER/plain) — the guard's equality holds, unlike the previous docs/-nested fixtures where it never did.
  • With CLAUDE_PROJECT_DIR="$OUTER" and git present but unable to resolve a toplevel from $OUTER/plain, the override fires and REPO_ROOT becomes $OUTER.
  • markdownlint_config_discoverable then walks $OUTER/plain$OUTER and finds $OUTER/.markdownlint-cli2.jsonc, which forces MD004: dash (unlike the dropped {} outer config, which had nothing to flag on a single-item list) — so * star item- star item is only reachable if the override actually fired.
  • Reverting the override (i.e. leaving REPO_ROOT at $OUTER/plain) makes the walk single-directory, finds nothing, and the hook skips — so this case is load-bearing, not just "still lints somehow."

This closes the finding from the two prior passes: the guard is now genuinely exercised, and the assertion can fail.

One non-blocking note

The commit also removes the negative twin, with an in-file comment explaining that three separate instruments (lint bytes, data.file telemetry, exit status) were tried and none could distinguish a correctly-inert override from a wrongly-firing one in that configuration — resting on the claim that markdownlint-cli2's own config resolution doesn't cross the repository boundary independent of the hook's REPO_ROOT gate. That's a reasonable call (a test that can't fail is worse than no test, which is exactly what the prior round's finding was about), and I have no static objection to the reasoning shown. I did not independently verify the markdownlint-cli2 behavior it rests on — I have no markdownlint-cli2 install available in this environment and no non-interactive shell/network access in this lane, so that half is author-claimed, not confirmed by me. Flagging it only as context, not as a finding.

Everything else

  • No changes to markdown-format.sh, README.md, CHANGELOG.md, or plugin.json since the last full pass on this PR (all previously reviewed with no open findings).
  • I did not execute the test suite in this pass — this lane has no non-interactive shell execution available, consistent with prior passes on this PR.
    — branch fix/markdown-format-stranded-1030

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 46s —— View job


Security review complete

  • Gather PR diff since last security-reviewed HEAD (f87ac6a) up to current HEAD (406fd36)
  • Review the delta for security implications
  • Re-confirm no regressions in previously-reviewed hunks
  • Post findings

Scope: the 5 files changed in this PR, at HEAD 406fd36, diffed against origin/main.

No security issues found.

The only file touched since the last security pass (f87ac6a) is markdown-format.test.sh. git diff c8e5112 406fd36 -- plugins/markdown-format/hooks/markdown-format.sh plugins/markdown-format/.claude-plugin/plugin.json plugins/markdown-format/README.md plugins/markdown-format/CHANGELOG.md is empty — markdown-format.sh (the file carrying every security-relevant hunk: the command -v git membership-skip gate, the CLAUDE_PROJECT_DIR walk-terminator override, and the env-clearing subshell probe) has been unchanged since c8e5112, which was reviewed in full on the prior pass with no findings and re-confirmed unchanged again at f87ac6a.

Commit 406fd36 ("test(markdown-format): make the override case actually reach the branch, drop the inert twin") replaces the GITON_NESTED/OUTER_FIXTURE cases with:

  • A repositioned OUTER/.markdownlint-cli2.jsonc outer config, now forcing MD004: dash instead of shipping empty ({}) — a test-fixture change only, no new subprocess construction from untrusted input.
  • A new positive fixture (NONREPO_ROOTED under $OUTER/plain/) that actually satisfies the REPO_ROOT == dirname(FILE) guard by placing the file in a non-repository directory, so the override's positive path is genuinely exercised.
  • Removal of the negative twin, with an inline comment explaining why no instrument in this suite can observably distinguish an inert override from a fired one in that specific configuration (markdownlint-cli2 does its own config discovery and won't cross the repo boundary regardless of what REPO_ROOT/CLAUDE_PROJECT_DIR resolve to).

All of this is test-only: paths are built from $WORK/$OUTER/$REPO fixture roots under the harness's own control, no variable is attacker-influenced, and no test-only shim ships in the plugin itself. Confirmed via git diff f87ac6a 406fd36 that the diff touches only test code — no new git/markdownlint-cli2/subshell invocation pattern, no permission or config changes.

Restating from the prior passes on this PR, since the reviewed logic itself is unchanged: the command -v git membership-skip gate only changes behavior when git is absent, bounded by hook::repo_root's single-directory fallback; the CLAUDE_PROJECT_DIR walk-terminator override only fires post hook::read_file_path's canonicalized membership check, so it can't admit a path outside the already-established trust boundary; and its re-probe correctly clears the five git-discovery env vars before asking git. The executable-config trust gate (resolve_trust_dir / RISK_CONFIGS) remains untouched by this PR throughout.
— branch fix/markdown-format-stranded-1030

@kyle-sexton
kyle-sexton merged commit 5f92d94 into main Aug 10, 2026
33 checks passed
@kyle-sexton
kyle-sexton deleted the fix/markdown-format-stranded-1030 branch August 10, 2026 04:14
kyle-sexton added a commit that referenced this pull request Aug 10, 2026
…of 0.11.1

The earlier form of this change was cut against the #2121 branch before it
merged, so it edited the then-unreleased 0.11.1 entry in place. 0.11.1 has since
shipped with the CLAUDE_PROJECT_DIR mechanism, and rewriting its entry would
misstate what that version did. Restores 0.11.1 verbatim and describes this
change as 0.11.2, with the manifest bumped to match — the plugin version is the
update cache key, so an unbumped plugin never delivers the change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Aug 10, 2026
…t REPO_ROOT read (#2128)

No linked issue

## Summary

Test-only follow-up to #2121. That PR shipped a comment asserting the
`REPO_ROOT` override's
*inertness* was **not behaviourally observable**, and left the negative
case unwritten on that basis.
The assertion was wrong. A fourth instrument exists, it works, and the
negative is now written.

The claim mattered beyond the missing case: a comment saying a thing
cannot be observed tells the
next maintainer to stop looking.

## Why three instruments failed

All three try to observe an **effect** of `REPO_ROOT`:

1. **Lint output.** Force the override to fire on a file at a real
repository root — replace the
probe with `false` — and the file is still not rewritten.
`markdownlint-cli2` performs its own
config discovery and does not cross the repository boundary, so widening
the hook's gate changes
   no observable byte.
2. **Telemetry `data.file`**, which is derived from `REPO_ROOT` and
looked like the obvious answer.
The forced-override run emits an **empty** value rather than a
relative-to-outer path — and empty
is also what a sink that never populated looks like, so the assertion
could not separate a
   regression from a flaky sink.
3. **Exit status** is 0 either way.

## The fourth instrument reads `REPO_ROOT` directly

The hook resolves a repo-local linter at
`"$REPO_ROOT/node_modules/.bin/markdownlint-cli2"`. Plant a
distinguishable shim at **both** candidate roots and whichever one runs
names the root the hook
actually computed. That is a read of the variable from outside the
process, not an inference.

```
POST:  negative (file dir IS a repo)  -> INNER    positive (file dir is NOT a repo) -> OUTER
```

**Control for the negative** — a hook whose probe is forced to `false`,
so the override always fires:

```
correct hook -> INNER      forced-override hook -> no marker
```

The assertion is **positive**: the marker must read `INNER`. A
wrongly-firing override produces
`OUTER` or no marker at all, and both fail it.

### Two mechanics that silently defeat this

Recorded in the test file, because each one makes the instrument look
like a dead end:

- **The `PATH` copy of `markdownlint-cli2` wins** over the repo-local
one, so the shim never runs
while the real binary is reachable. The case strips only the directories
carrying it, leaving `jq`
and `git` on `PATH` — remove those and the hook exits early for
unrelated reasons.
- **The shim cannot announce itself on stdout or stderr.** The hook
captures both into a variable, so
  anything printed is swallowed. It must write a **marker file**.

## Tests

```
ok: git present, dir is no repo: the override fires and CLAUDE_PROJECT_DIR terminates the walk
ok: git present: the override stays inert — the hook resolved REPO_ROOT to the git toplevel
```

The unusable-environment branch emits a **visible** `ok` rather than
passing over in silence — this
suite has no skip helper and sources none, and a silent omission is
exactly what
`scripts/check-silent-skips.sh` exists to catch.

## Credit and provenance

The instrument was found by the session that wrote the guardrails work
on #2100, after I concluded
the negative was unwritable. I reproduced it independently before
building on it, including the
forced-override control above.

Worth recording alongside it: while testing this, that session hit the
same defect the reviewer
found in #2121's first attempt — fixtures built under a Windows 8.3
shortname
(`C:/Users/KYLESE~1/…`) while `hook::repo_root` returns the long form,
so the guard
`"$REPO_ROOT" == "$(dirname "$FILE")"` compared two spellings of one
directory and was always false.
The branch never executed and the output looked plausible throughout.
Same lesson as the rest of
this sequence: **prove the fixture reached the path under test.**

## Related

- #2121 — where the override landed and where the incorrect comment
shipped
- #1938 — the stranded post-merge review-findings sweep

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Aug 10, 2026
… rewrite of 0.11.1

The earlier form of this change was cut against the #2121 branch before it
merged, so it edited the then-unreleased entry in place. That version has since
shipped, and rewriting a released entry would misstate what it did. Restores
0.11.1 verbatim and gives this change its own entry, with the manifest bumped to
match — the plugin version is the update cache key, so an unbumped plugin never
delivers the change.

The number is whatever sits above the highest released entry at rebase time;
main has taken several while this branch was in review, so it is deliberately
not restated here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Aug 11, 2026
…sted files reach the root config without git (#2130)

Follow-up to #2121. **Both gaps are live on `main` right now** — not
stale review findings. Reproduced independently: the two new tests, run
against `main` own unmodified hook, give **PASS=136 FAIL=2**. With the
change, **138/0**.

## The two defects

**1. `markdown-format.sh:119` calls `hook::repo_root` raw.** With `git`
and `jq` both absent, a nested file makes the opt-in pre-check read an
opted-in repo as opted-out, and the `jq` notice is swallowed. A
repository that did opt in is treated as if it had not, silently.

**2. The `REPO_ROOT` guard at `229-237` covers only the
`CLAUDE_PROJECT_DIR`-set case.** The membership scope it exists to fix
is gated on that variable being **unset**, and the no-git fixture runs
unset — so the configuration the fix was written for is still broken for
nested files. `hook::repo_root` falls back to the file own directory,
the root markdownlint config is never discovered, and the edit is
skipped with no diagnostic.

The second is the one #2121 review comment described as "leaving the
normal nested-docs case unfixed". That reading was correct and remains
correct at `main`.

## The change

Resolve the repository root from the **filesystem** rather than from a
variable: walk up for a `.git` entry, accepting a directory **or** a
file so linked worktrees and submodules resolve. Git own answer is
returned untouched whenever git produced one, and `CLAUDE_PROJECT_DIR`
is kept as a further fallback, so the case `main` already handles is
subsumed rather than replaced.

Four commits, ordered so the defect is demonstrated before it is fixed:

```
9cbb3c2  tests      (red against main)
4d2cd84  fix
b42a935  coverage
66a100d  changelog + version
```

## Verification

- Baseline `main` **135/0**; with the change **138/0**; the two new
tests **red** against `main` own hook (independently reproduced at
`e47964ca`).
- `main` newest positive override test passes unchanged under the
replacement — verified rather than assumed, after confirming no `.git`
sits on the temp-dir ancestor chain that would have made the walk answer
differently on this host.
- `shellcheck -x -S warning`, shell-portability, silent-skips,
markdownlint, and changelog-parity all clean.

## Stated rather than glossed — three things not confirmed

- **The POSIX-host spawn count was simulated**, by addressing the repo
in git own path spelling on a Windows host. It was never observed on a
real POSIX host.
- **A perf claim was wrong on first pass and is corrected here.** An
unconditional ~140ms Git Bash cost was expected; measurement showed
**zero** extra spawns on Git Bash, because `rev-parse --show-toplevel`
and `dirname` never produce the same path spelling there. The extra
probe fires only where the spellings agree — 2 to 3 spawns, root-level
files only.
- **One `PASS=133 FAIL=1` intermittent** was seen at an abandoned
intermediate commit. It was unnamed, did not reproduce in five runs at
the successor commit, and never recurred in any run backing these
numbers. Unconfirmed rather than dismissed.

## Provenance

Prepared as a cherry-pickable offer while #2121 was open; #2121 merged
at `5f92d946` without taking it, leaving no branch to cherry-pick onto,
so this is cut from `main` instead. The offer comment on #2121 remains
accurate for what it offered at the time.

Fixes #2134

## Conflict resolution against a moving `main`

`main` moved under this branch twice and the PR went `DIRTY`. The
version collision was resolved
twice, and the branch now carries the second resolution's numbers.

- **Conflict, both times: `plugins/markdown-format/CHANGELOG.md`.**
`main` took `0.11.2` (#2120's
shared `hook-utils.sh` NUL fix), then `0.11.3` (#2147). This branch's
entry moved up each time and
now sits at **`0.11.4`**, with `main`'s `0.11.3` and `0.11.2` kept below
it, order strictly
  descending.
- **`plugin.json` auto-merged to `main`'s number on both passes,
silently leaving no bump at all** —
no conflict marker, and only `check-changelog-parity.sh --check-bump`
catches it. Bumped to
`0.11.4` to match the changelog. This is the trap worth carrying
forward: a manifest version
collision does not conflict, it resolves to whichever side git saw last.
- `check-changelog-parity.sh --check-bump origin/main` clean at the
resolved tree.

**History note, stated rather than glossed.** This resolution was first
delivered as two merge
commits (`git merge origin/main`, never a rebase, since force-push is
blocked here). The branch was
subsequently **force-pushed** to a rebased, linear history carrying the
same resolved content and the
same `0.11.4` numbers, which discarded those merge commits. The shipped
branch is therefore a rebase,
not the merge described above; the resolution it carries is the same
one.

**Version coordination with #2135:** that PR also bumps
`markdown-format`, and after its own merges
of `main` it currently takes `0.11.4` as well. Whichever of the two
merges second must re-bump — the
manifests will auto-merge to the same number without conflicting,
exactly as described above.

## Related

- Fixes #2134 — the two no-git root-resolution defects this PR closes.
- Refs #2121 — the predecessor whose review comment identified the
nested-docs case; merged at
`5f92d946` without taking the offered follow-up, which is why this is
cut from `main`.
- Refs #2120 — merged into `main` mid-flight; its shared `hook-utils.sh`
change took the `0.11.2`
  slot this branch's changelog entry originally occupied.
- Refs #2135 — concurrent `markdown-format` version bump; see the
coordination note above.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant