fix(markdown-format): skip out-of-tree Markdown when CLAUDE_PROJECT_DIR unset (#972)#1030
fix(markdown-format): skip out-of-tree Markdown when CLAUDE_PROJECT_DIR unset (#972)#1030kyle-sexton wants to merge 4 commits into
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, |

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=66 FAIL=1.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). The single FAIL istelemetry/slow-sink, a pre-existing Windows spawn-latency flake unrelated tothis change: it fails on unmodified
origin/mainon this machine too, andworse (8396ms vs ~3000ms here; both under the sink's 3000ms sleep, so no fd1
leak — it merely exceeds the tight 2000ms threshold under load). CI runs on
ubuntu-24.04where the headroom passes.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.2 with entry).shellcheckonmarkdown-format.sh+markdown-format.test.sh→ clean;markdownlint-cli2on the CHANGELOG → 0 errors.Closes #972
Related
Held as draft because the issue flagged the git-worktree-fallback default as
"veto before merge" — a maintainer should confirm the approach before this lands.
The earlier version collision with #903 (
fix(guardrails): shared git parser) is resolved: #903 merged to main and cascade-bumpedmarkdown-formatto0.6.1; this branch mergedorigin/mainin and re-bumpedto
0.6.2, keeping #903's0.6.1CHANGELOG entry and placing theout-of-tree fix under
0.6.2. The net diff (GitHub "Files changed") is the fourmarkdown-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. 🤖