fix(markdown-format): skip out-of-tree Markdown when CLAUDE_PROJECT_DIR unset (#972) - #1030
Conversation
…E_PROJECT_DIR unset (#972) hook::read_file_path gated project-membership scoping on CLAUDE_PROJECT_DIR being set. When unset (e.g. an autonomous session whose cwd is the home directory, not a repo) the membership block was skipped entirely and the hook processed the file wherever it lived — most visibly linting a lane's temp comment-body .md outside any repository with repo-doc rules (MD041/MD013). Fall back to git-working-tree containment when CLAUDE_PROJECT_DIR is unset: a file under no git working tree is skipped, while a repo file edited in such a session is still processed. Set-CLAUDE_PROJECT_DIR behavior is unchanged, and the extra git rev-parse runs only on the unset (elif) path. The fix lives in the shared lib/hook-utils.sh, so the change is synced to all 12 carrying plugins and each is version-bumped with a CHANGELOG entry (rule 6d + the sync/changelog CI gates). Ten call hook::read_file_path (real scoping fix); claude-ops and desktop-notification carry the lib only (no behavior change). Regression coverage: lib/hook-utils.test.sh Test 12b (unset + in-tree accepted, unset + out-of-tree skipped) and a markdown-format.test.sh black-box case. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 10s —— View job PR Review
|
|
Claude finished @kyle-sexton's task in 2m 16s —— View job Security Review — PR #1030
FindingsNo security issues found. The PR makes exactly one functional change: adding an What was reviewedCore logic change ( elif ! git -C "$(dirname "$file")" rev-parse --show-toplevel >/dev/null 2>&1; then
return 1
fiInjection surface — Scope effect: The change narrows the hook's reach when New subprocess cost: The Synced copies: All 12 plugin copies of plugin.json version bumps: Purely semver integer increments (e.g. GitHub Actions workflows: No workflow files changed in this PR. No Test additions: |
|
🔒 merge-drive lane claiming this PR for on-branch fix work this cycle (amendment-round: 16, worker tier). Will fix clear branch-owned CI/bot findings, defer non-DEFER-FORBIDDEN items with reply+resolve, push, then re-check the merge gate. |
…IR unset (#972) When CLAUDE_PROJECT_DIR is unset (an autonomous session whose cwd is not a repo), hook::read_file_path applies no membership scoping, so the hook linted the .md wherever it lived — most visibly a lane's temp comment-body composed outside any repository (for gh --body-file), firing repo-doc rules (MD041, MD013) that do not apply. Pure advisory noise on every such write. Add a markdown-format-local fallback: when CLAUDE_PROJECT_DIR is unset, skip a file that is not under any git working tree. A scratch/temp file in no git tree is skipped; a repo .md edited in such a session is still linted; set- CLAUDE_PROJECT_DIR behavior is unchanged. The scoping is deliberately local to this hook rather than in the shared lib/hook-utils.sh guard: repo-doc linting is repository-scoped, but sibling hooks on the same guard (e.g. guardrails/cli-flag-verify) must fire regardless of repository membership, so widening the shared guard would regress them. Regression: markdown-format.test.sh asserts an out-of-tree scratchpad .md is skipped (exit 0, no findings, file unmodified); the in-tree-still-linted case is covered by every existing $REPO fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…markdown-format-project-dir-fallback
|
Claude finished @kyle-sexton's task in 2m 10s —— View job PR Review
Actual diff vs Core fix — correctness ✅
if [[ -z "${CLAUDE_PROJECT_DIR:-}" ]] &&
! git -C "$(dirname "$FILE")" rev-parse --show-toplevel >/dev/null 2>&1; then
exit 0
fiThe guard is correctly placed — after One minor efficiency note (not a bug): when The placement decision (local to Test coverage ✅
Three things done well:
One minor style nit: Version bump and CHANGELOG ✅
Security ✅
CLAUDE.md alignment ✅
SummaryFix is minimal, correct, well-tested, and properly scoped. No blocking issues. Ready to merge once the |
|
Claude finished @kyle-sexton's task in 2m 25s —— View job Security Review — PR #1030
No security issues found. The PR makes one functional change: a 3-line early-exit guard in Core fix — injection surface
if [[ -z "${CLAUDE_PROJECT_DIR:-}" ]] &&
! git -C "$(dirname "$FILE")" rev-parse --show-toplevel >/dev/null 2>&1; then
exit 0
fi
Quoting audit: Output handling: Scope effect: The change is strictly more restrictive — the hook now skips more files than before. Before this PR, the Test additions
GitHub Actions / supply-chainNo workflow files are changed. No new external dependencies, action pins, or remote MCP servers are introduced. The |
Resolve the markdown-format version collision introduced when #903 (shared git parser fix) landed on main and cascade-bumped markdown-format to 0.6.1 — the same bump this branch made. Re-bump to 0.6.2 (one past main) and split the CHANGELOG so #903's 0.6.1 entry and this branch's out-of-tree fix (now 0.6.2) each stand alone. No shared-lib change from this branch; main's hook-utils.sh (including #903's --config-env parser change) is taken as-is.
|
Claude finished @kyle-sexton's task in 1m 29s —— View job PR Review
Sequencing resolved ✅The PR's Core fix — correctness ✅
if [[ -z "${CLAUDE_PROJECT_DIR:-}" ]] &&
! git -C "$(dirname "$FILE")" rev-parse --show-toplevel >/dev/null 2>&1; then
exit 0
fiPlacement is correct. The guard runs after line 54 (
Injection surface is clean.
One minor efficiency note (not a bug): When The comment (lines 60–67) is well-reasoned and correctly explains why the fix is local to Test coverage ✅
Three things done well:
Cleanup is correct (line 261): One minor style note (non-blocking): Version bump and CHANGELOG ✅
Security ✅
CLAUDE.md alignment ✅
SummaryThe fix is minimal, correct, and well-tested. The sequencing issue with #903 is resolved in the current branch state. No blocking issues. Ready to merge pending the |
|
Claude finished @kyle-sexton's task in 1m 6s —— View job Security Review — PR #1030
No security issues found. This PR makes one functional change — a 3-line early-exit guard local to Core fix — injection audit
if [[ -z "${CLAUDE_PROJECT_DIR:-}" ]] &&
! git -C "$(dirname "$FILE")" rev-parse --show-toplevel >/dev/null 2>&1; then
exit 0
fi
Quoting: Output handling: Both stdout and stderr are discarded via Scope effect: The change is strictly more restrictive — the hook now exits 0 for more files than before. The Edge case — bare filename: If Test additions
GitHub Actions / supply chainNo workflow files changed. No new external dependencies, remote MCP servers, action pins, |
Resolves the markdown-format CHANGELOG collision: main shipped 0.6.2 (test-only differential fd1-leak detector), so the out-of-tree scoping fix moves to 0.6.3 and the manifest is re-bumped accordingly. Both entries are kept.
|
Claude finished @kyle-sexton's task in 2m 8s —— View job PR Review
Scope4 files changed, all in Core fix — correctness ✅
if [[ -z "${CLAUDE_PROJECT_DIR:-}" ]] &&
! git -C "$(dirname "$FILE")" rev-parse --show-toplevel >/dev/null 2>&1; then
exit 0
fiPlacement is correct. The guard runs after line 54 ( The Quoting is clean. Minor efficiency note (not a bug): When Comment block (lines 60–67) is well-written. The rationale — that this is intentionally local to Test coverage ✅
Three things done well:
One minor style note (non-blocking, pre-existing): Version and CHANGELOG ✅
One clarification note (no action needed): The PR description says "re-bumped to The CHANGELOG entry accurately describes the symptom, cause, and the "behavior when Security ✅
CLAUDE.md alignment ✅
SummaryFix is minimal, correct, well-tested, and properly scoped. No blocking issues. The only item that may look surprising is the version showing |
|
Claude finished @kyle-sexton's task in 1m 53s —— View job Security Review
No security issues found. This PR makes one functional change — a 3-line early-exit guard in Core fix — injection audit
if [[ -z "${CLAUDE_PROJECT_DIR:-}" ]] &&
! git -C "$(dirname "$FILE")" rev-parse --show-toplevel >/dev/null 2>&1; then
exit 0
fi
Quoting: Output handling: Both stdout and stderr discarded via Scope effect: Strictly more restrictive — the Path traversal consideration: If Test additions — injection/safety audit
The pre-assertion guard (line 251: GitHub Actions / supply chainNo workflow files changed. No new external dependencies, remote MCP servers, action pins, |
|
Claude finished @kyle-sexton's task in 2m 47s —— View job PR Review
Scope4 files changed, all in Core fix — correctness ✅
if [[ -z "${CLAUDE_PROJECT_DIR:-}" ]] &&
! git -C "$(dirname "$FILE")" rev-parse --show-toplevel >/dev/null 2>&1; then
exit 0
fiPlacement is correct. The guard runs after line 54 ( Short-circuit is correct.
Minor efficiency note (non-blocking): When Comment block (lines 60–67) is well-written. The rationale — that the fix is intentionally local to REVIEW.md — always-check criteria ✅Trust boundaries and injection ( The remaining always-check items (object-level authorization, tenant-scoped access, secrets in source, atomicity, audit logging) do not apply to this change. Test coverage ✅
Four things done well:
The in-tree counterpart (unset Version and CHANGELOG ✅
Note on PR description: The body still refers to "re-bumped to CLAUDE.md alignment ✅
🟡 Nit —
|
|
Claude finished @kyle-sexton's task in 1m 50s —— View job Security Review — PR #1030
No security issues found. This PR makes one functional change — a 3-line early-exit guard in Core fix — injection audit
if [[ -z "${CLAUDE_PROJECT_DIR:-}" ]] &&
! git -C "$(dirname "$FILE")" rev-parse --show-toplevel >/dev/null 2>&1; then
exit 0
fi
Quoting: Bash variable expansion in Output handling: Both stdout and stderr discarded via
Scope effect: Strictly more restrictive — the Path traversal: If Test additions — injection/safety audit
The pre-assertion guard (line 251: GitHub Actions / supply chainNo workflow files changed. No new external dependencies, remote MCP servers, action pins, |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d1882518ba
ℹ️ 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".
…hip test The CLAUDE_PROJECT_DIR-unset fallback tested the lexical parent of the edited path, so an in-repository symlink whose target lives outside any git working tree passed the guard and markdownlint-cli2 --fix rewrote that external target under the repository's rules. Derive the directory from hook::physical_path first, matching how hook::read_file_path decides project membership. Adds a regression case: an in-repo symlink to an out-of-tree .md is skipped and its target left unmodified, guarded to skip on hosts that cannot create real symlinks.
|
Claude finished @kyle-sexton's task in 1m 49s —— View job PR Review
Scope of this review passThe latest commit ( Symlink fix — correctness ✅! git -C "$(dirname "$(hook::physical_path "$FILE")")" rev-parse --show-toplevel >/dev/null 2>&1; thenThe guard now resolves the physical path before deriving the directory for the
hook::physical_path() {
local resolved
if resolved=$(realpath -- "$1" 2>/dev/null) || resolved=$(readlink -f -- "$1" 2>/dev/null); then
if [[ -n "$resolved" ]]; then
printf '%s' "$resolved"
return
fi
fi
printf '%s' "$1"
}
Validity guarantee at call site: Quoting chain: Symlink regression test — correctness ✅
The test:
This is precisely the right regression test for the reported vector. The four assertions (two exit-code/output checks, two content-unchanged checks) leave no path for a silent partial fix. One minor style note (non-blocking, pre-existing): CHANGELOG ✅The
This makes the security guarantee visible to consumers upgrading past Security ✅The Codex-reported P2 finding is fully addressed. Before this commit, a Claude Code session with No new trust surfaces, no new subprocesses beyond the existing CLAUDE.md alignment ✅
SummaryThe symlink fix is minimal (one changed expression in the guard), uses the pre-existing |
|
Claude finished @kyle-sexton's task in 2m 59s —— View job Security Review — PR #1030
No exploitable security issues found. This review covers the 4 files changed in this PR at HEAD Codex P2 (symlink escape) — addressed ✅The Codex review comment flagged that an in-repo symlink to an out-of-tree target would bypass the membership guard because the check ran against the lexical parent (the in-repo directory). Commit if [[ -z "${CLAUDE_PROJECT_DIR:-}" ]] &&
! git -C "$(dirname "$(hook::physical_path "$FILE")")" rev-parse --show-toplevel >/dev/null 2>&1; then
exit 0
fiFor a symlink
The external file is not rewritten. The fix is correct. Injection audit ✅
Quoting. Output discarding. Both stdout and stderr of Scope effect. The change is strictly more restrictive — the
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3cbdffea54
ℹ️ 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".
…available hook::physical_path degrades to the unchanged lexical path when neither realpath nor readlink -f resolves. The out-of-tree membership test then asked git about the in-repository symlink's own parent, admitted it, and let markdownlint-cli2 --fix rewrite the out-of-tree target under repo rules. An unresolved symlink is now skipped. The check is on the outcome -- the path came back unchanged for a path that IS a symlink -- rather than on resolver availability, so it also covers a resolver that exists but fails.
|
Claude encountered an error after 2s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 2s —— View job I'll analyze this and get back to you. |
|
Warning Automated review did not complete — this is an infrastructure failure, not a review verdict. Treat any Claude comment on this PR (including a placeholder like "I'll analyze this and get back to you") as incomplete, not "no findings."
Re-running the job, or pushing a new commit, will retry the review. |
|
Warning Automated security review did not complete — this is an infrastructure failure, not a review verdict. Treat any Claude comment on this PR (including a placeholder like "I'll analyze this and get back to you") as incomplete, not "no findings."
Re-running the job, or pushing a new commit, will retry the review. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 660b52cc94
ℹ️ 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".
…ambient git env GIT_DIR and GIT_WORK_TREE override Git's repository discovery outright (https://git-scm.com/docs/git), so `git -C <out-of-tree dir> rev-parse --show-toplevel` answered with the inherited repository and admitted an external file. The membership probe now clears the repository-selection and discovery variables so the verdict comes from the file's physical directory alone.
|
Claude encountered an error after 2s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 2s —— View job I'll analyze this and get back to you. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a1491f966e
ℹ️ 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".
| ( | ||
| unset GIT_DIR GIT_WORK_TREE GIT_COMMON_DIR GIT_CEILING_DIRECTORIES \ | ||
| GIT_DISCOVERY_ACROSS_FILESYSTEM | ||
| git -C "$1" rev-parse --show-toplevel |
There was a problem hiding this comment.
Handle missing Git before treating the file as out-of-tree
When CLAUDE_PROJECT_DIR is unset on a POSIX host without git on PATH, this command fails exactly like a negative membership result, so every Markdown edit is silently skipped even when the file is in a repository and jq and markdownlint-cli2 are available. Previously hook::repo_root tolerated Git being unavailable by falling back to the file directory, and neither the README requirements nor the setup check requires Git; either preserve that behavior or make Git an explicit, visibly checked prerequisite rather than interpreting command-not-found as out-of-tree.
Useful? React with 👍 / 👎.
…ree" (#2121) 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: ```bash - 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 - #1030 — where the finding was filed - #1938 — the stranded post-merge review-findings sweep this came out of --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

Summary
markdown-format's PostToolUse hook linted.mdfiles outside any repository(a loop lane's scratchpad/temp comment-body composed for
gh issue comment --body-file) with repo-doc rules that do not apply — most visibly MD041(first-line-h1) and MD013 (line-length). Pure advisory noise on every such write.
Cause: when
CLAUDE_PROJECT_DIRis unset (an autonomous session whose cwd is nota repo), the shared
hook::read_file_pathguard applies no membership scoping, sothe hook processed the file wherever it lived.
Fix
Add a markdown-format-local fallback in
markdown-format.sh, right after theextension gate: when
CLAUDE_PROJECT_DIRis unset, skip a file that is not underany git working tree.
A scratch/temp file in no git tree is skipped; a repo
.mdedited in such asession is still linted; set-
CLAUDE_PROJECT_DIRbehavior is unchanged.--show-toplevelsucceeds only inside a working tree — the same predicatehook::repo_rootalready uses — and the extragit rev-parseruns only on theunset path.
Why local, not in the shared guard
The obvious-looking fix — teach the shared
hook::read_file_pathinlib/hook-utils.shto fall back to git-tree membership — is wrong, and itstest suite proves it:
hook::read_file_pathis consumed by 10 hooks, andguardrails/cli-flag-verifyis a location-independent guardrail — it catcheshallucinated CLI flags in written content regardless of repository membership
(a bad
ghflag in a scratchpad comment-body is precisely its job, and preciselythe file this hook should not lint). Widening the shared guard made
cli-flag-verify.test.shfail 9 assertions (the hook began skipping itsout-of-tree fixtures). The two hooks want opposite unset-case membership
policies, so the repo-scoping policy belongs in
markdown-format, not the sharedlibrary. This keeps the change to one plugin (matching the issue's scope and
rule 6d's single-plugin version bump) and touches no shared code.
Verification
Ran locally on Windows Git Bash (git 2.x, jq present), branch merged up to date
with current
origin/main:plugins/markdown-format/hooks/markdown-format.test.sh: PASS=67 FAIL=0.New case passes: an out-of-tree scratchpad
.mdis skipped (exit 0, nofindings, file left unmodified). The in-tree-still-linted acceptance case is
covered by every existing
$REPOfixture (they live in a git working tree andalready run with
CLAUDE_PROJECT_DIRunset). Thetelemetry/slow-sinkcasethat previously failed on this Windows host is now green: main's 0.6.2 made
that detector differential rather than a fixed wall-clock bound, which this
branch picks up in the merge.
plugins/guardrails/hooks/cli-flag-verify.test.sh: PASS=48 FAIL=0 —confirms the guardrail is untouched (this is the regression the shared-lib
approach caused; the local fix avoids it).
lib/hook-utils.test.sh: PASS=83 FAIL=0 (post-merge, includes fix(guardrails): distinguish --config-env from -c/--config in shared git parser #903's tests).scripts/sync-hook-utils.sh --check→ 12 copies match;--check-bump origin/main→ "Lib unchanged; no version bumps required" (no shared-lib touch).scripts/check-changelog-parity.sh --check-bump origin/main→ OK(
markdown-format0.6.3 with entry).shellcheckonmarkdown-format.sh+markdown-format.test.sh→ clean;markdownlint-cli2on the CHANGELOG → 0 errors.Closes #972
Related
Draft hold released. Issue #972 records the git-working-tree fallback as a
defaulted decision with an open veto window ("maintainer-vetoable"), not a
required approval. The window has been open since 2026-07-22; no veto was
entered on the issue or this PR, the work-class was operator-ratified on
2026-07-23, and the implementation matches the defaulted branch and all three
acceptance criteria verbatim. Marked ready on that basis.
The version collisions are resolved: #903 cascade-bumped
markdown-formatto
0.6.1, then main shipped0.6.2(test-only differential fd1-leakdetector). This branch merged
origin/mainin and placed the out-of-tree fixunder
0.6.3, keeping both prior entries intact. The net diff (GitHub"Files changed") is the four
markdown-formatfiles; no shared code is touched.History note: earlier commits on this branch attempted a shared-lib approach
(edit
lib/hook-utils.sh+ sync 12 copies + bump all 12). That was revertedafter
cli-flag-verify.test.shproved the guardrail divergence described above.The superseded cascade commit remains reachable in the "Commits" tab only via an
ours-merge and contributes nothing to the tree; this repository issquash-merge only (
allow_rebase_merge/allow_merge_commitboth false),so the intermediate commits collapse to the net four-file change on merge and
the superseded cascade can never be replayed.
Deferred follow-up (not in scope for #972): the 9 sibling formatter hooks
(
bash-format,biome-format,eol-normalizer,go-format,powershell-format,ruff-format,typos-format,actionlint) share the samelatent out-of-tree noise. Fixing them as a class wants an opt-in shared
scoping mechanism (formatters opt in; the guardrail stays location-agnostic) —
worth a separate issue with that trigger recorded.
Origin: converted from the fleet-sweep #657 line (markdown-format comment-body
lint noise).
Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com
Work-class: C3 (bug-fix-shaped) — attended triage 2026-07-23, operator-ratified. 🤖