Skip to content

fix(hook-utils): scope read_file_path to git worktrees when project dir unset - #2450

Merged
kyle-sexton merged 9 commits into
mainfrom
cursor/hook-utils-git-worktree-scope-63e1
Aug 12, 2026
Merged

fix(hook-utils): scope read_file_path to git worktrees when project dir unset#2450
kyle-sexton merged 9 commits into
mainfrom
cursor/hook-utils-git-worktree-scope-63e1

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Closes #1091

Summary

Scope read_file_path to git worktrees when the project directory is unset.

Related

N/A

@cursor

cursor Bot commented Aug 12, 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.

@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

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


Security Review

  • Gather context (PR diff, changed files)
  • Understand the request (security review only)
  • Review lib/hook-utils.sh logic changes
  • Review test file changes
  • Review duplicated plugin copies for consistency
  • Post findings

Scope: lib/hook-utils.sh / lib/hook-utils.test.sh and 16 byte-identical plugin copies (plugins/*/hooks/hook-utils.sh), all carrying SHA 967e010. No GitHub Actions workflow files are touched by this PR, so the pull_request_target / workflow_run / permissions: / unpinned-action lanes don't apply here.

What changed: hook::read_file_path (lib/hook-utils.sh:405-459) gains a new branch: when CLAUDE_PROJECT_DIR is unset, it now requires the target file to sit inside a git working tree (via the new hook::in_git_working_tree, lib/hook-utils.sh:393-403) before returning the path to callers (formatters/linters like typos-format.sh, bash-format.sh, markdown-format.sh, etc.). Previously this case had no scoping check at all, so any existing file was accepted. This is strictly more restrictive than before — it narrows, not widens, what these hooks will act on.

Findings: none.

Specifically checked and found sound:

  • Symlink handlinghook::in_git_working_tree is checked against hook::physical_path (realpath-resolved) rather than the raw path, so a symlinked directory can't be used to fake tree membership; the resolved location is what's actually tested (lib/hook-utils.sh#L456-L458). The -L "$file" && "$file_physical" == "$file" guard (L453-L455) correctly rejects the case where symlink resolution silently fails (both realpath and readlink -f fail and hook::physical_path falls back to echoing the input unchanged), rather than treating an unresolvable symlink as trustworthy.
  • Env-var spoofing of git discoveryhook::in_git_working_tree explicitly unsets GIT_DIR, GIT_WORK_TREE, GIT_COMMON_DIR, GIT_CEILING_DIRECTORIES, GIT_DISCOVERY_ACROSS_FILESYSTEM in a subshell before calling git -C ... rev-parse --show-toplevel (lib/hook-utils.sh:397-402), so an inherited GIT_DIR can't make an out-of-tree directory falsely resolve as in-tree.
  • Shell injection — all path values flow through quoted variable expansions ("$file", "$1", "$file_physical") into git -C, realpath, dirname, [[ ]] tests — no eval, no unquoted expansion, no command construction from untrusted input.
  • Consistency — all 16 plugin copies plus lib/hook-utils.sh are byte-identical (same blob SHA), so there's no drift where one plugin got a weaker/divergent check.
  • This is a local scoping heuristic for the assistant's own hook tooling (which already runs with the invoking user's full filesystem privileges), not an authentication/authorization boundary between mutually distrusting principals — so there's no privilege-escalation angle to a TOCTOU between the existence check and a caller's later write.

No CRITICAL, IMPORTANT, or SUGGESTION-level security issues identified in this PR's diff.

@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: baa7140ae6

ℹ️ 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 lib/hook-utils.sh
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

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


Code Review

  • Gather context (diff, CLAUDE.md, REVIEW.md)
  • Understand the request
  • Review changed files for correctness, error handling, test coverage, maintainability
  • Post findings as inline comments
  • Post summary

Reviewed the 18 changed files (lib/hook-utils.sh + its 17 synced plugin copies + lib/hook-utils.test.sh), per REVIEW.md's code-review lane scope (correctness, error handling, test coverage, maintainability — security is this repo's dedicated claude-security-review lane).

The core change itself is sound: hook::in_git_working_tree correctly unsets GIT_DIR/GIT_WORK_TREE/etc. before shelling out so an inherited env can't fake worktree membership, and the new elif branch in hook::read_file_path scopes to worktree membership only when CLAUDE_PROJECT_DIR is unset, using the same resolved-physical-path approach as the existing project-dir branch. All 17 plugin copies are byte-identical to lib/hook-utils.sh (verified with a diff loop), so the sync is not the problem — a real gap follows the sync.

🔴 Blocking — carrying plugins were not version-bumped, and CI is red because of it.
This PR changes lib/hook-utils.sh and syncs the copy into 17 plugins, but none of those plugins' .claude-plugin/plugin.json had its version bumped, and no CHANGELOG.md entries were added (the repo's established pattern for this, e.g. plugins/actionlint/CHANGELOG.mdpermalink). The repo enforces this by design — scripts/sync-hook-utils.sh --check-bump fails when the lib changed but a carrying plugin's version didn't, because "the plugin version is the update cache key, so an unbumped plugin never delivers the change to consumers." I confirmed this is not just theoretical: the live CI run for this PR (ci / hook-utils-sync job, step "Verify carrying plugins bumped when the lib changed") failed with STALE VERSION for all 17 carrying plugins — e.g. plugins/actionlint/.claude-plugin/plugin.json is still 0.8.7. Without a version bump, the fix in #1091 will not actually reach any plugin consumer even after merge.

  • Fix: bump the version in each of the 17 carrying plugins' .claude-plugin/plugin.json, and add a ### Changed / "Synced hook-utils.sh: ..." entry to each plugin's CHANGELOG.md matching the existing convention.
  • Fix this →

Test coverage gap (Suggestion, inline on lib/hook-utils.test.sh): the new Test 12e doesn't exercise the symlink defense the new git-branch code added, unlike the analogous CLAUDE_PROJECT_DIR-set path which has four dedicated symlink cases. Left as an inline comment with specifics.

Nit (inline): the new "Test 12e" sorts before pre-existing "Test 12d" in the file, breaking the otherwise-sequential test labeling.

One other CI failure on this PR (plugin-gate / plugins/guardrails/hooks/cli-flag-verify.test.sh, UNKNOWN_FLAG-related failures) does not appear related to this diff — that plugin only carries the mechanical hook-utils.sh sync here, not a change to its CLI-flag-verify logic — but is worth a look before merge since it's on the guardrails plugin this PR also touches.
· branch: cursor/hook-utils-git-worktree-scope-63e1

Comment thread lib/hook-utils.test.sh
Comment thread lib/hook-utils.test.sh
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown

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

@cursor
cursor Bot force-pushed the cursor/hook-utils-git-worktree-scope-63e1 branch 2 times, most recently from e9026c9 to 04ba21a Compare August 12, 2026 18:06
kyle-sexton added a commit that referenced this pull request Aug 12, 2026
…ckRollup running-check pitfall (#2513)

## Summary

Lands the two PR-lifecycle facts #1436 recorded from driving
#1393/#1316/#1318/#1322 to merge, in the document that was prescribing
the expensive path:

- **Stale-branch recovery now defaults to merge-forward.**
`monitor.md`'s mergeable pre-check (§3.1 step 1) and stale-branch
recovery rule (§3.2) both prescribed "force-push with lease" — but
auto-mode permission classifiers commonly deny force-push, and the
recorded consequence was a fresh branch + fresh PR + closing the old one
per rebase (the #1315#1377#1393 churn, with every review thread
re-opened on the successor). Merging the default branch *into* the PR
branch resolves staleness and pushes **fast-forward** — no force-push —
and under a squash-only default branch the merge commits collapse to one
commit on merge, so linear-history requirements stay satisfied. Verified
in the issue's own record: #1393 landed that way and #1318 was
merge-forwarded five times without needing a new branch. Rebase stays
available as the exception for projects requiring a linear PR branch
where force-push is actually permitted.
- **`statusCheckRollup` reports a running check as `conclusion: ""`
(empty string), not `null`.** The complement-shaped filter (`conclusion
!= null and != "SUCCESS"`) therefore counts every in-progress check as a
failure — the exact misreport in the issue (two "failing" checks that
were simply still running). The multi-PR scan section (§3.0.6, the one
place this skill reads `statusCheckRollup`) now documents the pitfall
with value-positive jq selectors for "failed" and "still running".

Version `0.53.11` → `0.53.14` (patch; `0.53.12`/`0.53.13` are claimed by
in-flight PRs #2450/#2453/#2483/#2510 and #2469 — skipping past them per
the #1746 collision pattern).

## Test plan

- `npx markdownlint-cli2@0.23.2` on both edited markdown files — 0
issues.
- Docs-only change to skill reference text; no scripts or hooks touched.
The jq forms added are the ones from the issue, verified against `gh pr
view --json statusCheckRollup` semantics.

## Related

Fixes #1436

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request Aug 12, 2026
…ckRollup running-check pitfall (#2513)

## Summary

Lands the two PR-lifecycle facts #1436 recorded from driving
#1393/#1316/#1318/#1322 to merge, in the document that was prescribing
the expensive path:

- **Stale-branch recovery now defaults to merge-forward.**
`monitor.md`'s mergeable pre-check (§3.1 step 1) and stale-branch
recovery rule (§3.2) both prescribed "force-push with lease" — but
auto-mode permission classifiers commonly deny force-push, and the
recorded consequence was a fresh branch + fresh PR + closing the old one
per rebase (the #1315#1377#1393 churn, with every review thread
re-opened on the successor). Merging the default branch *into* the PR
branch resolves staleness and pushes **fast-forward** — no force-push —
and under a squash-only default branch the merge commits collapse to one
commit on merge, so linear-history requirements stay satisfied. Verified
in the issue's own record: #1393 landed that way and #1318 was
merge-forwarded five times without needing a new branch. Rebase stays
available as the exception for projects requiring a linear PR branch
where force-push is actually permitted.
- **`statusCheckRollup` reports a running check as `conclusion: ""`
(empty string), not `null`.** The complement-shaped filter (`conclusion
!= null and != "SUCCESS"`) therefore counts every in-progress check as a
failure — the exact misreport in the issue (two "failing" checks that
were simply still running). The multi-PR scan section (§3.0.6, the one
place this skill reads `statusCheckRollup`) now documents the pitfall
with value-positive jq selectors for "failed" and "still running".

Version `0.53.11` → `0.53.14` (patch; `0.53.12`/`0.53.13` are claimed by
in-flight PRs #2450/#2453/#2483/#2510 and #2469 — skipping past them per
the #1746 collision pattern).

## Test plan

- `npx markdownlint-cli2@0.23.2` on both edited markdown files — 0
issues.
- Docs-only change to skill reference text; no scripts or hooks touched.
The jq forms added are the ones from the issue, verified against `gh pr
view --json statusCheckRollup` semantics.

## Related

Fixes #1436

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
@cursor
cursor Bot force-pushed the cursor/hook-utils-git-worktree-scope-63e1 branch from 04ba21a to cb48d95 Compare August 12, 2026 21:56
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude finished @cursor[bot]'s task in 1m 6s —— View job


Security Review

  • Gather context (PR diff vs origin/main, custom instructions)
  • Invoke /review:security-review skill logic
  • Check skip gate

Skip gate hit: condition 3 (this head already has a successful security review that still applies).

I compared the actual PR diff (origin/main...HEAD, 50 files / +609/-52) against what was reviewed in the earlier successful security-review comment on this PR (commit baa7140). The only security-relevant code — lib/hook-utils.sh (hook::in_git_working_tree and the new hook::read_file_path git-worktree branch) plus its 17 byte-identical synced plugin copies — is unchanged between baa7140 and the current head cb48d95. Everything added since that review is non-functional: 17 .claude-plugin/plugin.json version bumps and matching CHANGELOG.md entries (addressing the earlier reviewer feedback about carrying-plugin versioning), with no changes to lib/hook-utils.sh, lib/hook-utils.test.sh, or any plugin's hooks/hook-utils.sh.

Per the skip gate, no new findings to report — nothing else to add. The prior review's conclusion stands: no CRITICAL, IMPORTANT, or SUGGESTION-level security issues in this PR's diff.
· branch

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown

Last security-reviewed head: 01059624925ee3b758e28fbde3df3b82911e5b83. On the next push, the relevance gate compares only the commits since this SHA; delete this comment to force a full re-review.

@cursor
cursor Bot force-pushed the cursor/hook-utils-git-worktree-scope-63e1 branch from 8e8f910 to b356ab8 Compare August 12, 2026 22:12
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude finished @cursor[bot]'s task in 1m 26s —— View job


Security Review

  • Gather context (PR diff vs origin/main, prior review history)
  • Invoke /review:security-review skill logic
  • Check skip gate
  • Post findings

Skip gate hit: condition 3 (this head already has a successful security review that still applies).

The branch was rebased onto the latest origin/main since the last review (commit cb48d95 is no longer an ancestor of HEAD), and a new empty chore: retrigger CI commit was added on top — but I diffed the actual PR content two ways to confirm nothing security-relevant moved:

  • git diff origin/main...HEAD --stat at the current head (b356ab8) produces the same 50 files / +602/-51, matching the file list already reviewed.
  • git diff origin/main...HEAD -- lib/hook-utils.sh lib/hook-utils.test.sh shows the hook::in_git_working_tree helper and the new hook::read_file_path git-worktree branch are byte-for-byte identical to what was reviewed at baa7140 and re-confirmed unchanged at cb48d95 — only line numbers shifted (now lib/hook-utils.sh#L400-L457) due to the rebase.
  • All 17 plugin copies of hook-utils.sh remain byte-identical to lib/hook-utils.sh; the only diffs since the last review are non-functional (.claude-plugin/plugin.json version bumps, CHANGELOG.md entries, and base drift from the rebase onto a newer origin/main).

Per the skip gate, no new findings to report. The prior review's conclusion stands: no CRITICAL, IMPORTANT, or SUGGESTION-level security issues in this PR's diff — the new elif branch in hook::read_file_path (lib/hook-utils.sh#L446-L457) only narrows what's accepted when CLAUDE_PROJECT_DIR is unset (git-worktree scoping with symlink and env-spoofing defenses), and does not weaken any existing check.
· branch: cursor/hook-utils-git-worktree-scope-63e1

@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude finished @cursor[bot]'s task in 2m 30s —— View job


Code Review

  • Gather context (diff vs origin/main, CLAUDE.md, REVIEW.md)
  • Check skip gate
  • Review changed files for correctness, error handling, test coverage, maintainability
  • Post inline findings (if any)
  • Post summary

Skip gate hit: condition 4 (this head already has a successful review from this lane that still applies).

I diffed the current head (b356ab8) against the commit the last code-review lane pass reviewed (baa7140). The only files with review-relevant surface for this lane — lib/hook-utils.sh and lib/hook-utils.test.sh — are byte-identical between those two commits (confirmed via 640cc276d… blob SHA unchanged across both). Everything added since that pass (commits 0cc8d49, b356ab8) is the mechanical fix for that pass's own blocking finding: 17 .claude-plugin/plugin.json version bumps + matching CHANGELOG.md entries, plus an empty CI retrigger commit. No new code-review surface.

Status of the prior pass's findings, re-verified against the current diff:

  • 🔴 Blocking (resolved): carrying-plugin versions were not bumped when hook-utils.sh changed, so scripts/sync-hook-utils.sh --check-bump failed and the fix would never reach installed plugin consumers. Confirmed fixed — all 17 carrying plugins now have both a version bump (e.g. plugins/actionlint/.claude-plugin/plugin.json: 0.8.100.8.11) and a corresponding CHANGELOG.md entry (e.g. plugins/actionlint/CHANGELOG.md).
  • Suggestion (still open, already inline on the PR): Test 12e covers plain in-repo/out-of-tree acceptance for the new git-worktree branch but doesn't exercise the symlink defense that branch actually added (lib/hook-utils.sh#L452-L455), unlike the four dedicated symlink cases the CLAUDE_PROJECT_DIR-set path has. Not re-posting a duplicate inline comment since the original from this lane's earlier pass is still live on the PR thread.
  • Nit (still open, already inline on the PR): Test 12e is inserted before the pre-existing Test 12d, breaking the otherwise-sequential 12, 12b, 12c, 12d, ... labeling.

Not re-litigating these since the underlying lines haven't changed — the existing inline threads from the earlier pass on this commit lineage still apply and don't need duplicates.

One item worth a note: at the time of the earlier pass, plugin-gate (plugins/guardrails/hooks/cli-flag-verify.test.sh) was failing on an issue unrelated to this diff. CI for the current head (b356ab8, the retrigger commit) is still in progress as of this review — worth confirming that lane is green before merge.

No new findings from this lane. · branch

…ir unset (#1091)

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
@cursor
cursor Bot force-pushed the cursor/hook-utils-git-worktree-scope-63e1 branch from b356ab8 to 5f1dd20 Compare August 12, 2026 22:34
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude finished @cursor[bot]'s task in 1m 30s —— View job


Security Review

  • Gather context (PR diff vs origin/main, prior review history)
  • Invoke /review:security-review skill logic
  • Check skip gate
  • Post findings

Skip gate hit: condition 3 (this head already has a successful security review that still applies).

Current head is 5f1dd20. Verified two ways that nothing security-relevant moved since the last successful security review (posted against b356ab8):

  • git diff origin/main...HEAD --stat at 5f1dd20 produces the same 50 files (+557/-16) already reviewed — lib/hook-utils.sh, lib/hook-utils.test.sh, and 17 hooks/hook-utils.sh plugin copies, plus their .claude-plugin/plugin.json version bumps and CHANGELOG.md entries.
  • git diff b356ab8 HEAD -- lib/hook-utils.sh lib/hook-utils.test.sh and the same diff against every one of the 17 plugin hooks/hook-utils.sh copies all produce no output — those files are byte-identical to what was already reviewed. The only content that changed since b356ab8 is base drift from main moving forward (the branch was rebased/squashed to a single commit on top of current main), none of which is part of this PR's actual diff.

Per the skip gate, no new findings to report. The prior conclusion stands: no CRITICAL, IMPORTANT, or SUGGESTION-level security issues in this PR's diff — hook::in_git_working_tree unsets GIT_DIR/GIT_WORK_TREE/GIT_COMMON_DIR/GIT_CEILING_DIRECTORIES/GIT_DISCOVERY_ACROSS_FILESYSTEM before shelling out (so an inherited env can't fake worktree membership), and the new elif branch in hook::read_file_path (lib/hook-utils.sh#L446-L457) only narrows what's accepted when CLAUDE_PROJECT_DIR is unset, checking worktree membership against the realpath-resolved physical path — a symlink whose resolution fails or points elsewhere is rejected, not trusted.
· branch

@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude finished @cursor[bot]'s task in 3m 23s —— View job


Code Review

  • Gather context (diff vs origin/main, prior review history, CLAUDE.md/REVIEW.md)
  • Check skip gate
  • Review hook::read_file_path / hook::in_git_working_tree logic (lib/hook-utils.sh)
  • Review test coverage (lib/hook-utils.test.sh)
  • Verify prior blocking finding (carrying-plugin version bumps) was resolved
  • Post findings

Skip gate: partially applies. lib/hook-utils.sh and lib/hook-utils.test.sh are unchanged since this lane's earlier review at baa7140 (confirmed against the current head, 5f1dd20) — only the 17 carrying plugins' .claude-plugin/plugin.json versions and CHANGELOG.md entries changed since then. So I did not redo a full from-scratch review; I re-verified the prior conclusions and checked one path the earlier passes hadn't covered.

🔴 Prior blocking finding — resolved. The earlier code-review pass (and Codex) flagged that none of the 17 carrying plugins had bumped version or added a CHANGELOG.md entry, which meant #1091's fix would never reach installed consumers via the version-keyed update cache. That's now fixed: spot-checked plugins/actionlint/.claude-plugin/plugin.json (0.8.100.8.11) with a matching CHANGELOG.md entry ("scope read_file_path to git worktrees when project dir unset (#1091)"), and the PR's file list shows the same +1/-1 manifest bump + +6/-0 changelog addition pattern across all 17 plugins.

New finding (inline on lib/hook-utils.sh:446): the new worktree-scoping branch only runs elif command -v git >/dev/null 2>&1. When CLAUDE_PROJECT_DIR is unset and git isn't on PATH, neither branch in hook::read_file_path fires and the function falls through to unconditionally returning the file — i.e. the exact unscoped behavior #1091 exists to close, reintroduced silently with no test coverage (Test 12e's else at lib/hook-utils.test.sh:622 just marks itself SKIPPED rather than asserting anything). Posted as an inline suggestion with a fix-this link.

Still open from the prior review (unchanged content, so not re-posted as duplicates — see the existing inline threads on this PR):

  • Test 12e lacks symlink-defense coverage that the new git-branch code added (the -L "$file" && "$file_physical" == "$file" guard), unlike the four dedicated symlink cases on the CLAUDE_PROJECT_DIR-set path (Test 12).
  • Test 12e sorts before Test 12d in the file, breaking the otherwise-sequential 12, 12b, 12c, 12d, ... labeling — still unaddressed at the current head.

None of these are blocking on their own — they're edge-case robustness/coverage gaps, not correctness regressions in the primary path.
· branch: cursor/hook-utils-git-worktree-scope-63e1

Comment thread lib/hook-utils.sh
cursoragent and others added 2 commits August 12, 2026 22:49
Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
…_DIR

read_file_path now rejects out-of-tree scratch files when the project dir is
unset (#1091). Point each case at its own project root so the hook still runs.

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude finished @cursor[bot]'s task in 1m 8s —— View job


Security Review

  • Gather context (PR diff vs origin/main, prior review history)
  • Invoke /review:security-review skill logic
  • Check skip gate
  • Post findings

Skip gate hit: condition 3 (this head already has a successful security review that still applies).

Current head is b386082. Verified nothing security-relevant changed since the last successful security review (posted against 5f1dd20):

  • git diff 5f1dd20 HEAD -- lib/hook-utils.sh lib/hook-utils.test.sh produces no output — the hook::in_git_working_tree helper and the hook::read_file_path git-worktree branch, plus all 17 synced plugin copies, are byte-identical to what was already reviewed.
  • The only diff since 5f1dd20 is plugins/guardrails/hooks/cli-flag-verify.test.sh (commit b386082, "scope cli-flag-verify fixtures under CLAUDE_PROJECT_DIR"): it adds CLAUDE_PROJECT_DIR="$case_dir" (or equivalent per-test tmp dir) to several existing test invocations so those fixtures resolve correctly under the new worktree-scoping check. This is test-only fixture plumbing — no production code, no new input handling, no change to trust boundaries or attack surface.
  • No other files changed against origin/main beyond what was already reviewed (51 files / +564/-21, matching the PR's stated diff stat).

Per the skip gate, no new findings to report. The prior conclusion stands: no CRITICAL, IMPORTANT, or SUGGESTION-level security issues in this PR's diff — hook::in_git_working_tree unsets GIT_DIR/GIT_WORK_TREE/GIT_COMMON_DIR/GIT_CEILING_DIRECTORIES/GIT_DISCOVERY_ACROSS_FILESYSTEM before shelling out (so an inherited env can't fake worktree membership), and the elif branch in hook::read_file_path (lib/hook-utils.sh#L446-L457) only narrows what's accepted when CLAUDE_PROJECT_DIR is unset, checking worktree membership against the realpath-resolved physical path — a symlink whose resolution fails or points elsewhere is rejected, not trusted.

(Note: an earlier code-review pass on this branch — a separate lane, not security — flagged a robustness gap where the worktree check falls through to unscoped behavior if git isn't on PATH. That's a fail-open robustness concern for the code-review lane, not a new exploitable security finding, since it doesn't weaken behavior relative to pre-#1091 in that edge case.)
· branch

Without git on PATH, read_file_path no longer falls through to the unscoped
pre-#1091 behavior. Test 12f covers the rejection path.

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude finished @cursor[bot]'s task in 1m 12s —— View job


Security Review

  • Gather context (PR diff vs origin/main, prior review history)
  • Invoke /review:security-review skill logic
  • Check skip gate
  • Post findings

Skip gate: does not apply — new security-relevant code since the last review. The last successful security review on this PR covered up to commit b386082. The current head, cde3921, adds one new commit, cde3921 ("fail closed when git is absent and project dir unset"), which touches lib/hook-utils.sh / lib/hook-utils.test.sh plus the 17 synced plugin copies — so this needed a fresh look rather than a skip.

What changed: this commit directly resolves the fail-open gap flagged by the earlier code-review pass on this PR (and by Codex): previously, when CLAUDE_PROJECT_DIR was unset and git was not on PATH, hook::read_file_path fell through both the project-dir branch and the elif command -v git branch straight to the unconditional printf '%s' "$file", silently reverting to the unscoped pre-#1091 behavior. The fix adds an explicit else arm (lib/hook-utils.sh#L458-L461) that return 1s in that case — failing closed instead of failing open. New Test 12f (lib/hook-utils.test.sh#L626-L643) exercises this by pointing PATH at an empty directory (no git binary) with CLAUDE_PROJECT_DIR unset and asserting the file is rejected.

Findings: none. This is a strict tightening of the existing behavior — it removes the one remaining fail-open edge case in hook::read_file_path rather than introducing a new code path or trust boundary:

  • The else branch has no attacker-controlled input of its own; it's an unconditional return 1 reached only when both prior branches' preconditions are false.
  • All 17 plugin copies received the identical 4-line lib/hook-utils.sh diff (verified against the commit's per-file change stats — each shows the same +4/-0), so there's no drift where one plugin retained the fail-open behavior.
  • The new test uses a scratch mktemp -d for the empty PATH dir and cleans it up (rmdir/rm -f), no leftover state or injection surface in the test harness itself.
  • No change to hook::in_git_working_tree's env-scrubbing or the symlink guard reviewed in earlier passes on this PR — those remain byte-identical to what was already reviewed.

No CRITICAL, IMPORTANT, or SUGGESTION-level security issues identified in this PR's diff.
· branch

read_file_path now rejects files when git is unavailable and CLAUDE_PROJECT_DIR
is unset. Update the git-absence contract cases to match.

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude finished @cursor[bot]'s task in 5m 26s —— View job


Code Review

  • Gather context (diff vs origin/main, prior review history)
  • Check skip gate
  • Review new commits since last code-review pass (b386082, cde3921, 3ecec01)
  • Post inline findings
  • Post summary

Skip gate: partially applies. This lane's last full pass reviewed head 5f1dd20 and flagged (as an inline suggestion) that hook::read_file_path fell through to the old unscoped behavior when git isn't on PATH. Since then, three new commits landed: b386082 (guardrails test fixture scoping — already reviewed by the security lane, no code-review surface), cde3921 (fixes that exact prior finding by adding a fail-closed else branch), and 3ecec01 (updates markdown-format.test.sh expectations to match). I reviewed the two functional commits fresh rather than re-running the whole-PR pass.

cde3921 — the fail-closed fix itself is correct. lib/hook-utils.sh:458-461 adds else return 1 so that when CLAUDE_PROJECT_DIR is unset and git isn't resolvable, hook::read_file_path now rejects the file instead of silently falling through to the pre-#1091 unscoped printf '%s' "$file". All 17 plugin copies stay byte-identical to lib/hook-utils.sh (spot-checked plugins/actionlint/hooks/hook-utils.sh, diff is identical).

🟠 New finding (posted inline on lib/hook-utils.test.sh:626-641): the new "Test 12f", which the commit message says covers this rejection path, doesn't actually reach the branch it's testing. It sets PATH="$EMPTY_GIT_BIN" as a prefix on printf inside a pipeline (printf ... | hook::read_file_path) — that assignment only scopes to printf's own process, not to hook::read_file_path on the other side of the pipe, which still sees the real, unrestricted PATH. So command -v git still finds git, the new else branch is never reached, and the assertion passes for an unrelated reason (the scratch file, created via plain mktemp, isn't inside any git working tree either way). The test would pass identically if the cde3921 fix were reverted — full details and a fix pointer are in the inline comment (it references the correct pattern already used in plugins/markdown-format/hooks/markdown-format.test.sh's BASH_ENV shim).

3ecec01 — reviewed, correct. The markdown-format.test.sh expectation flips (in-repo .md now expected unmodified/* star item instead of linted/- star item, nested fixtures, and the symlink case) correctly match the new fail-closed contract, and that file's git-absence simulation (NO_GIT_ENV BASH_ENV shim overriding command/git) is sound — it doesn't have the pipeline-prefix problem Test 12f has, since it invokes the hook as a real subprocess (bash "$HOOK") with BASH_ENV exported into that subprocess's environment rather than prefixing a single pipeline stage.

Non-blocking observation: cde3921 changes lib/hook-utils.sh (and all 17 synced copies) again, but doesn't touch any plugin's .claude-plugin/plugin.json or CHANGELOG.md — e.g. plugins/actionlint/CHANGELOG.md still only describes the original worktree-scoping fix under [0.8.11], not the fail-closed behavior added afterward under the same unreleased version. This isn't the same "stale version, fix never ships" bug flagged earlier in this PR's history — 0.8.11 still differs from origin/main's version, so scripts/sync-hook-utils.sh --check-bump should still pass, and since this is all pre-merge the final 0.8.11 will carry the complete diff either way. It's a changelog-completeness gap worth a line, not a blocker.

No other findings in the new commits.
· branch: cursor/hook-utils-git-worktree-scope-63e1

Comment thread lib/hook-utils.test.sh Outdated
Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude finished @cursor[bot]'s task in 5m 21s —— View job


Code Review

  • Gather context (diff vs origin/main, prior review history, CLAUDE.md/REVIEW.md)
  • Check skip gate
  • Review changed files for correctness, error handling, test coverage, maintainability
  • Post inline findings
  • Post summary

Skip gate: partially applies. The last code-review pass on this PR reviewed up to commit 5f1dd20/cde3921 (lib/hook-utils.sh + lib/hook-utils.test.sh — unchanged since, confirmed via git log cde3921..HEAD -- lib/hook-utils.sh lib/hook-utils.test.sh returning nothing). So I did not re-review those two files from scratch; I focused on what's new at the current head (b57a760): commit 3ecec01 (plugins/markdown-format/hooks/markdown-format.test.sh) and the merge/reconciliation commit b57a760 itself (17 .claude-plugin/plugin.json + CHANGELOG.md files), neither of which any prior pass on this PR had examined.

🔴 Blocking (new, inline on plugins/eol-normalizer/CHANGELOG.md:13): plugins/eol-normalizer/.claude-plugin/plugin.json was never bumped, even though eol-normalizer carries a synced copy of hooks/hook-utils.sh (changed in this PR) and got a matching CHANGELOG.md entry. This is the same STALE VERSION failure mode scripts/sync-hook-utils.sh --check-bump was built to catch — the exact 🔴 blocking finding this PR already fixed once, for its other 15 carrying plugins. Confirmed plugins/eol-normalizer/.claude-plugin/plugin.json's version (0.6.11) is identical to origin/main's copy — no bump exists anywhere in this branch's history for this plugin, likely dropped during the chore: merge main and reconcile version bumps after parallel merges commit (b57a760), which touched other eol-normalizer files but not its manifest. Without the bump, the fix won't reach installed eol-normalizer consumers via the version-keyed update cache.

Suggestion (new, inline on plugins/markdown-format/hooks/markdown-format.test.sh:631): commit 3ecec01 correctly flipped the in-repo-symlink assertion to match the new fail-closed-when-git-absent behavior, but left the explanatory "CONTROL" comment block above it (lines 608-622) describing the old, now-contradicted assertion (! -L / "asserted on the link being gone"). Beyond being stale, the test has quietly lost the discriminating power the comment claims for it — since every no-git case now skips unconditionally, this case can no longer catch a broken containment check the way it used to.

Still open from earlier passes on this PR (unchanged content in lib/hook-utils.sh / lib/hook-utils.test.sh since the last review, so not re-posted as duplicates — see the existing inline threads):

Separately reviewed and found sound (no issues): the CLAUDE_PROJECT_DIR="$case_dir" additions to plugins/guardrails/hooks/cli-flag-verify.test.sh correctly scope every fixture that exercises the hook under the new worktree-membership check, and the rest of markdown-format.test.sh's fail-closed contract updates (in-repo, nested, and the three escaping-symlink cases) are consistent with the new behavior.
· branch: cursor/hook-utils-git-worktree-scope-63e1

@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude finished @cursor[bot]'s task in 1m 11s —— View job


Security Review

  • Gather context (PR diff vs origin/main, prior review history)
  • Invoke /review:security-review skill logic
  • Check skip gate
  • Post findings

Skip gate hit: condition 3 (this head already has a successful security review that still applies).

Current head is b57a760. Verified nothing security-relevant changed since the last successful security review (posted against cde3921):

  • git diff origin/main...HEAD --stat at b57a760 produces the same 51 files (+660/-38) already reviewed — lib/hook-utils.sh, lib/hook-utils.test.sh, 17 hooks/hook-utils.sh plugin copies, their .claude-plugin/plugin.json version bumps / CHANGELOG.md entries, plus plugins/guardrails/hooks/cli-flag-verify.test.sh and plugins/markdown-format/hooks/markdown-format.test.sh fixture updates.
  • git diff cde3921 HEAD -- lib/hook-utils.sh lib/hook-utils.test.sh produces no output — the hook::in_git_working_tree helper, the hook::read_file_path git-worktree branch (including the fail-closed else return 1 arm), and all 17 synced plugin copies are byte-identical to what was already reviewed.
  • The one file that did change since cde3921plugins/markdown-format/hooks/markdown-format.test.sh (commit 3ecec01) — is test-only: it updates fixture assertions to expect the fail-closed skip behavior (* star item unmodified) instead of the old fail-open linted expectation, aligning the test with the production fix already reviewed. No production code changed.
  • The remaining commit since cde3921 (b87717f, rate-limit-guard statusline perf work) and the merge commit b57a760 reconciling it are unrelated to this PR's diff — that content already lives on origin/main, so it doesn't appear in git diff origin/main...HEAD and carries no security surface for this PR.

Per the skip gate, no new findings to report. The prior conclusion stands: no CRITICAL, IMPORTANT, or SUGGESTION-level security issues in this PR's diff — hook::in_git_working_tree unsets GIT_DIR/GIT_WORK_TREE/GIT_COMMON_DIR/GIT_CEILING_DIRECTORIES/GIT_DISCOVERY_ACROSS_FILESYSTEM before shelling out (so an inherited env can't fake worktree membership), and hook::read_file_path's worktree-scoping branch (lib/hook-utils.sh#L446-L461) checks membership against the realpath-resolved physical path (rejecting unresolvable/escaping symlinks) and now fails closed (return 1) rather than falling through to the unscoped pre-#1091 behavior when git is unavailable.
· branch

Comment thread plugins/eol-normalizer/CHANGELOG.md Outdated
Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
Comment thread plugins/markdown-format/hooks/markdown-format.test.sh
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude finished @cursor[bot]'s task in 59s —— View job


Security Review

  • Gather context (PR diff vs origin/main, prior review history)
  • Invoke /review:security-review skill logic
  • Check skip gate
  • Post findings

Skip gate hit: condition 3 (this head already has a successful security review that still applies).

Current head is ebae4cc. Verified nothing security-relevant changed since the last successful security review (posted against cde3921, and re-confirmed unchanged through b57a760):

  • git diff b57a760 ebae4cc --stat shows only 2 files: plugins/eol-normalizer/.claude-plugin/plugin.json (0.6.110.6.12) and plugins/eol-normalizer/CHANGELOG.md (moves the existing "scope read_file_path to git worktrees" entry under a new [0.6.12] heading). This is the mechanical fix for the STALE VERSION gap flagged in an earlier review pass on this PR (eol-normalizer's manifest hadn't been bumped despite its hooks/hook-utils.sh copy changing).
  • git diff origin/main...HEAD --stat at ebae4cc produces the same 52 files (+663/-39) already reviewed — lib/hook-utils.sh, lib/hook-utils.test.sh, 17 hooks/hook-utils.sh plugin copies, their .claude-plugin/plugin.json version bumps / CHANGELOG.md entries, plus the guardrails/markdown-format test fixture updates from earlier commits.
  • No production code (lib/hook-utils.sh, any plugin's hooks/hook-utils.sh, or lib/hook-utils.test.sh) changed since cde3921/b57a760 — the diff since then is entirely .claude-plugin/plugin.json version fields and CHANGELOG.md prose.

Per the skip gate, no new findings to report. The prior conclusion stands: no CRITICAL, IMPORTANT, or SUGGESTION-level security issues in this PR's diff — hook::in_git_working_tree unsets GIT_DIR/GIT_WORK_TREE/GIT_COMMON_DIR/GIT_CEILING_DIRECTORIES/GIT_DISCOVERY_ACROSS_FILESYSTEM before shelling out (so an inherited env can't fake worktree membership), and hook::read_file_path's worktree-scoping branch (lib/hook-utils.sh#L446-L461) checks membership against the realpath-resolved physical path (rejecting unresolvable/escaping symlinks) and fails closed (return 1) rather than falling through to the unscoped pre-#1091 behavior when git is unavailable.

(Note: the still-open Test 12f coverage gap flagged in an earlier review pass — the PATH= prefix on printf in a pipeline not actually restricting hook::read_file_path's PATH — is a test-coverage/code-review-lane concern, not a security finding on its own; it's already tracked inline on this PR.)
· branch

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude finished @cursor[bot]'s task in 1m 1s —— View job


Security Review

  • Gather context (PR diff vs origin/main, prior review history)
  • Invoke /review:security-review skill logic
  • Check skip gate
  • Post findings

Skip gate hit: condition 3 (this head already has a successful security review that still applies), reinforced by condition 2 for what's new.

Current head is 291724a. The last successful security-review pass on this PR covered up through ebae4cc. Verified what changed since then:

  • git diff ebae4cc..291724a -- lib/hook-utils.sh lib/hook-utils.test.sh '**/hooks/hook-utils.sh' '**/cli-flag-verify.test.sh' '**/markdown-format.test.sh' produces no output — every security-relevant file already reviewed (the hook::in_git_working_tree helper, hook::read_file_path's worktree-scoping branch and its fail-closed else, all 17 synced plugin copies, and the related test fixtures) is byte-identical to what was reviewed at ebae4cc.
  • git diff ebae4cc..291724a --stat shows only 7 files changed: plugins/claude-ops/.claude-plugin/plugin.json (version bump 0.31.80.31.9) and six claude-ops skill/doc markdown files (SKILL.md, context/converge.md, context/gotchas.md, context/scope-semantics.md, context/sync.md). This content is a merge of main (fix(claude-ops): widen the settings-write facts to every enabledPlugins call #2527) plus the resulting version bump — it documents existing claude plugin CLI settings-write behavior in prose; it contains no shell/code changes, no GitHub Actions workflow changes, and no new input-handling or trust-boundary surface.
  • git diff origin/main...HEAD --stat at 291724a reproduces the same 52 files / +663/-39 already reviewed in prior passes.

Per the skip gate, no new findings to report. The prior conclusion stands: no CRITICAL, IMPORTANT, or SUGGESTION-level security issues in this PR's diff — hook::in_git_working_tree unsets GIT_DIR/GIT_WORK_TREE/GIT_COMMON_DIR/GIT_CEILING_DIRECTORIES/GIT_DISCOVERY_ACROSS_FILESYSTEM before shelling out (so an inherited env can't fake worktree membership), and hook::read_file_path's worktree-scoping branch (lib/hook-utils.sh#L446-L461) checks membership against the realpath-resolved physical path (rejecting unresolvable/escaping symlinks) and fails closed (return 1) rather than falling through to the unscoped pre-#1091 behavior when git is unavailable.

(Note: the still-open Test 12f coverage gap flagged in an earlier review pass — the PATH= prefix on printf in a pipeline not actually restricting hook::read_file_path's PATH — is a test-coverage/code-review-lane concern, not a security finding on its own; it remains tracked inline on this PR.)
· branch

Fix test 12f so PATH hides git for the read_file_path consumer (not only
the pipeline producer) while keeping jq available, and assert rc=1 on an
in-repo file. Rewrite the markdown-format git-absent in-repo symlink
commentary to match fail-closed expectations.

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude finished @cursor[bot]'s task in 55s —— View job


Security Review

  • Gather context (PR diff vs origin/main, prior review history)
  • Invoke /review:security-review skill logic
  • Check skip gate
  • Post findings

Skip gate hit: condition 3 (this head already has a successful security review that still applies).

Current head is 0105962. The last successful security-review pass on this PR covered up through 291724a. Verified what changed since then:

  • git diff 291724a..HEAD --stat shows exactly 2 files, both test-only: lib/hook-utils.test.sh and plugins/markdown-format/hooks/markdown-format.test.sh.
  • git diff 291724a..HEAD -- lib/hook-utils.sh '**/hooks/hook-utils.sh' produces no outputhook::in_git_working_tree, hook::read_file_path's worktree-scoping branch, and all 17 synced plugin copies are byte-identical to what was already reviewed. No production/runtime code changed.
  • The lib/hook-utils.test.sh change fixes exactly the Test 12f gap flagged in an earlier code-review pass on this PR: it now builds a private PATH (NO_GIT_BIN12F, containing only jq/realpath/readlink, deliberately omitting git) and applies that PATH= prefix to both sides of the pipe (PATH=... printf ... | PATH=... hook::read_file_path), so the consumer process actually loses git this time, and asserts both the exit code (rc_no_git -eq 1) and empty output. It also switches the scratch file to an in-repo path (README.md) specifically so that if git leaked back onto PATH, the worktree branch would wrongly admit it and the test would catch that. This is a test-fixture correctness improvement with no production surface — no injection risk (uses mktemp -d/ln -s to a fixed private dir, cleaned up with rm -rf, no unsanitized input in constructed paths).
  • The markdown-format.test.sh change only rewrites a stale explanatory comment block (previously flagged as misleading in an earlier review pass) to match the already-reviewed fail-closed behavior; the assertion logic itself is unchanged in this diff.

Per the skip gate, no new findings to report. The prior conclusion stands: no CRITICAL, IMPORTANT, or SUGGESTION-level security issues in this PR's diff — hook::in_git_working_tree unsets GIT_DIR/GIT_WORK_TREE/GIT_COMMON_DIR/GIT_CEILING_DIRECTORIES/GIT_DISCOVERY_ACROSS_FILESYSTEM before shelling out (so an inherited env can't fake worktree membership), and hook::read_file_path's worktree-scoping branch (lib/hook-utils.sh#L446-L461) checks membership against the realpath-resolved physical path (rejecting unresolvable/escaping symlinks) and fails closed (return 1) rather than falling through to the unscoped pre-#1091 behavior when git is unavailable.
· branch

@cursor

cursor Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review threads addressed in 0105962:

  1. Test 12f now builds a private bin (jq/realpath/readlink, no git), applies PATH to the hook::read_file_path consumer, uses in-repo README.md, asserts rc=1 + empty stdout.
  2. eol-normalizer version bump was already present (0.6.12) — resolved as outdated.
  3. markdown-format CONTROL commentary rewritten for fail-closed (link survives).

All three review threads resolved. Awaiting CI then merge.

cursor Bot pushed a commit that referenced this pull request Aug 12, 2026
PR #2450 already claims 0.31.9 for the hook-utils sync; keep #2522's
prose fix on the next patch.

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
@kyle-sexton
kyle-sexton merged commit 5e83b3c into main Aug 12, 2026
38 checks passed
@kyle-sexton
kyle-sexton deleted the cursor/hook-utils-git-worktree-scope-63e1 branch August 12, 2026 23:54
kyle-sexton added a commit that referenced this pull request Aug 13, 2026
…2522) (#2530)

Fixes #2522

## Summary

The `install_new` userConfig paragraph in
`plugins/claude-ops/skills/plugins/SKILL.md` embedded
`${user_config.install_new}` inside explanatory prose about the unset
state. Claude Code substitutes every occurrence, so when the key is set
(e.g. to `all`) the paragraph falsely claims it still renders as the
literal placeholder — right after the **Configured value** line already
shows the real value.

This PR rewrites the explanation so only the **Configured value** line
keeps the live `${user_config.install_new}` token. Surrounding sentences
describe the placeholder by shape (`${user_config.…}`) or by reference
to that line, staying readable whether the key is set or unset.

Bumps `claude-ops` to `0.31.10` (0.31.9 reserved for in-flight
hook-utils sync on #2450).

## Test plan

- [x] No automated tests cover this prose contract.
- [x] Manual review: unset render — **Configured value** line shows the
placeholder token; explanatory prose does not embed a second live
`${user_config.install_new}`.
- [x] Manual review: set render (e.g. `all`) — **Configured value** line
shows `all`; explanatory prose still correctly describes unset behavior.

## Related

- #2522

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.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.

formatter hooks: 9 siblings share the out-of-tree noise gap #972 fixed for markdown-format only

2 participants