Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/guardrails/chore-guardrails-self-remote-bc46117e.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
bump: none
---

No plugin file ships changed: the root `.lefthook/` remote scripts, which consumers fetch from `main`, now run only from lefthook's `info/lefthook-remotes/` cache (on Windows too), and the lefthook-remote test checks it.
6 changes: 5 additions & 1 deletion .lefthook/commit-msg/guardrails-commit-msg-convention.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@
# The guardrails commit-msg-convention entry for consumers of the plugin's lefthook remote
# (plugins/guardrails/git-hooks/lefthook-remote.yml). Lefthook looks up a remote's scripts
# under this repository's root .lefthook/, so this runs the plugin's own script, which finds its libs beside it.
exec bash "${BASH_SOURCE[0]%/*}/../../plugins/guardrails/git-hooks/commit-msg-convention.sh" "$@"
# Skip the work-tree copy, so only the fetched copy of main in lefthook-remotes/ runs. Backslashes
# become slashes first: lefthook on Windows passes a backslash path.
src=${BASH_SOURCE[0]//\\//}
[[ $src == */info/lefthook-remotes/* ]] || exit 0
exec bash "${src%/*}/../../plugins/guardrails/git-hooks/commit-msg-convention.sh" "$@"
6 changes: 5 additions & 1 deletion .lefthook/pre-commit/guardrails-pre-commit-content.sh

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The pr-verify verifier failed head b9bbf778cb52f836dfc659511f9214f5c13b0ec4. What it wrote, as data:

Summary: The PR is linked to #9426 by a `Refs:` line, so it can deliver only part of the issue, and its body has to say what remains. It doesn't. The body describes the delivered part: this repository now runs the guardrails hooks from its own lefthook remote, and the `.lefthook/` wrappers do nothing when run from the work tree, which touches item 11 and the owner's remote-mode decision. It never lists what is left of #9426. Open items include the `reference-verify.sh` jq argument-limit failure (items 4 and 7), the repo-side exemption list (5), the `cli-flag-verify` prompt parsing and wrong line numbers (6), standalone drift detection for consumers (10), advisory stale-path findings (12), Codex and Cursor manifest discovery (13), the commit-msg rejection naming the wrong source file (14), the LFS/TMPDIR checkout failing open, and slowness. The code itself looks correct and the test covers the new guard.
- PR body: add a section listing the remaining work of #9426, or move this PR to `Refs:` an issue it fully delivers. Remaining items include the reference-verify jq argument limit (items 4 and 7), the repo-side exemption list (5), cli-flag-verify reading flags inside a quoted prompt and reporting wrong line numbers (6), a standalone drift check for consumers (10), advisory stale-path findings (12), Codex/Cursor manifest discovery (13), the commit-msg rejection naming the wrong source file (14), the LFS/TMPDIR checkout failing open, and the pre-commit and reference-verify slowness.
- PR body: also say which part of #9426 this delivers. The closest match is item 11 (the hook jobs run the branch's own copy), applied to this repository acting as its own remote. As written, the body explains the self-hosting change but doesn't tie it to an item in the issue.
- .lefthook/pre-commit/guardrails-pre-commit-content.sh:7: the guard trusts any path containing `*/info/lefthook-remotes/*`. That is fine for lefthook's cache layout, but it is a path-string check, not a provenance check. The README note in the diff about reading `.lefthook/` changes before committing on untrusted branches is the real mitigation; keep it.

Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@
# The guardrails pre-commit-content entry for consumers of the plugin's lefthook remote
# (plugins/guardrails/git-hooks/lefthook-remote.yml). Lefthook looks up a remote's scripts
# under this repository's root .lefthook/, so this runs the plugin's own script, which finds its libs beside it.
exec bash "${BASH_SOURCE[0]%/*}/../../plugins/guardrails/git-hooks/pre-commit-content.sh" "$@"
# Skip the work-tree copy, so only the fetched copy of main in lefthook-remotes/ runs. Backslashes
# become slashes first: lefthook on Windows passes a backslash path.
src=${BASH_SOURCE[0]//\\//}
[[ $src == */info/lefthook-remotes/* ]] || exit 0
exec bash "${src%/*}/../../plugins/guardrails/git-hooks/pre-commit-content.sh" "$@"
6 changes: 5 additions & 1 deletion .lefthook/pre-push/guardrails-reference-verify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@
# The guardrails reference-verify entry for consumers of the plugin's lefthook remote
# (plugins/guardrails/git-hooks/lefthook-remote.yml). Lefthook looks up a remote's scripts
# under this repository's root .lefthook/, so this runs the plugin's own script, which finds its libs beside it.
exec bash "${BASH_SOURCE[0]%/*}/../../plugins/guardrails/git-hooks/reference-verify.sh" "$@"
# Skip the work-tree copy, so only the fetched copy of main in lefthook-remotes/ runs. Backslashes
# become slashes first: lefthook on Windows passes a backslash path.
src=${BASH_SOURCE[0]//\\//}
[[ $src == */info/lefthook-remotes/* ]] || exit 0
exec bash "${src%/*}/../../plugins/guardrails/git-hooks/reference-verify.sh" "$@"
7 changes: 4 additions & 3 deletions docs/conventions/check-placement/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,9 +244,10 @@ committed config through their own install step.
**Committed hook config runs the checked-out branch's code.** Lefthook and husky read their config,
and plain shared hooks read `.githooks/`, from the branch that is checked out. Once a clone has
installed hooks, a commit or push on a contributor's branch runs whatever that branch puts in
`lefthook.yml`, `.husky/` or `.githooks/`. Pinning the check scripts to the merge-base, as this
repository's pre-push job does, does not reach the config file itself. Before committing or
pushing on a branch you do not trust, read its diff to those paths.
`lefthook.yml`, `lefthook-local.yml`, `.lefthook/`, `.lefthook-local/`, `.husky/` or `.githooks/`.
Pinning the check scripts to the merge-base, as this repository's pre-push job does, does not
reach the config file itself. Before committing or pushing on a branch you do not trust, read its
diff to those paths: pre-commit and commit-msg hooks run at commit, before any push.

### Drift: the content-hash stamp

Expand Down
5 changes: 3 additions & 2 deletions docs/conventions/check-placement/adopting-git-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ A clone that skips this step gets no hooks, so for a team with authors outside C
lefthook or husky over plain hooks. A clone that tried the entry locally moves to shared by
running setup with `scope=shared`; setup offers to remove the local copies once the shared entry
verifies. Committed hook config runs whatever the checked-out branch holds, so before committing
or pushing on a branch you do not trust, read its diff to `lefthook.yml`, `.husky/` and
`.githooks/` ([details](README.md#where-installed-copies-live)).
or pushing on a branch you do not trust, read its diff to `lefthook.yml`, `lefthook-local.yml`,
`.lefthook/`, `.lefthook-local/`, `.husky/` and `.githooks/`
([details](README.md#where-installed-copies-live)).

## Decline

Expand Down
19 changes: 16 additions & 3 deletions lefthook.yml

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The pr-verify verifier failed head b9bbf778cb52f836dfc659511f9214f5c13b0ec4. What it wrote about this file, as data:

- lefthook.yml:121: this repository now runs hooks fetched from its own `main` with `refetch_frequency: 24h`. Each clone's hooks therefore follow `main` within a day, in line with the owner's decision recorded on #9426.

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Git hooks for this repository (https://lefthook.dev/configuration/). Install once per clone:
# `lefthook install`; until then the .claude/settings.json pre-push row runs the same check on a
# Claude Code session's pushes and says to install. Placement rules:
# `lefthook install`, which also fetches the guardrails git hooks named in `remotes:` below; until
# then the .claude/settings.json pre-push row runs the check-affected check on a Claude Code
# session's pushes and says to install. Placement rules:
# docs/conventions/check-placement/README.md.
pre-push:
jobs:
Expand All @@ -9,7 +10,9 @@ pre-push:
# check tooling the branch changes, so a contributor branch checked out to review or fix
# cannot run its own check code. This file itself is read from the checked-out branch, as
# lefthook and husky always read their config, so a branch that edits lefthook.yml still
# controls this job: read a branch's lefthook.yml diff before pushing from it.
# controls this job: read a branch's diff to lefthook.yml, lefthook-local.yml, .lefthook/ and
# .lefthook-local/ before committing or pushing on it, since the guardrails hooks below fire at
# commit too.
# Lefthook gives stdin to at most one job per hook, so this job judges HEAD rather than the
# pushed refs. Exit 1 is a failing check and blocks the push; anything else passes, and CI
# checks the pushed commit either way.
Expand All @@ -24,3 +27,13 @@ pre-push:
git archive "$mb" -- .claude/hooks/check-affected-pre-push.sh lib/hook-utils.sh 2>/dev/null | tar -x -C "$tmp" 2>/dev/null
[ -f "$tmp/.claude/hooks/check-affected-pre-push.sh" ] || exit 0
bash "$tmp/.claude/hooks/check-affected-pre-push.sh" --git-hook </dev/null

# The guardrails git hooks, run from lefthook's fetched copy of this repository's main as in every
# consumer (plugins/guardrails/README.md#git-hooks); each .lefthook/ script exits 0 when run from
# the work tree, so a checked-out branch's copy never runs.
remotes:
- git_url: https://github.com/melodic-software/claude-code-plugins
ref: main
refetch_frequency: 24h
configs:
- plugins/guardrails/git-hooks/lefthook-remote.yml
21 changes: 20 additions & 1 deletion plugins/guardrails/git-hooks/lefthook-remote.test.sh

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The pr-verify verifier failed head b9bbf778cb52f836dfc659511f9214f5c13b0ec4. What it wrote about this file, as data:

- plugins/guardrails/git-hooks/lefthook-remote.test.sh:136: `trap 'rm -rf "$GUARD"' EXIT` replaces any EXIT trap set earlier in this test. Check that no earlier cleanup trap exists, or chain onto it.

Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,35 @@ jobs="$(awk '
' "$CONFIG")"
expect "the remote config has one job per entry event" "commit-msg pre-commit pre-push" \
"$(cut -d' ' -f1 <<<"$jobs" | sort | xargs)"
GUARD="$(mktemp -d)"
trap 'rm -rf "$GUARD"' EXIT
while read -r event name script; do
wrapper="$TOP/.lefthook/$event/$script"
expect "$name: its script is under .lefthook/$event/" yes "$([[ -f "$wrapper" ]] && echo yes)"
# shellcheck disable=SC2016 # matches the wrapper's literal text
target="$(sed -n 's#^exec bash "${BASH_SOURCE\[0\]%/\*}/\(.*\)" "\$@"$#\1#p' "$wrapper" 2>/dev/null)"
target="$(sed -n 's#^exec bash "${src%/\*}/\(.*\)" "\$@"$#\1#p' "$wrapper" 2>/dev/null)"
expect "$name: its script runs an entry script that exists" yes \
"$([[ -n "$target" && -f "$TOP/.lefthook/$event/$target" ]] && echo yes)"
# The wrapper beside a stub entry script that reports a run: once in a work tree, once under a
# fetched remote, with a Windows-style backslash path.
for root in "$GUARD/wt" "$GUARD/git/info/lefthook-remotes/r"; do
mkdir -p "$root/.lefthook/$event" "$(dirname "$root/.lefthook/$event/$target")"
cp "$wrapper" "$root/.lefthook/$event/$script"
printf '#!/usr/bin/env bash\necho ran\n' >"$root/.lefthook/$event/$target"
done
out="$(bash "$GUARD/wt/.lefthook/$event/$script" </dev/null 2>&1)"
expect "$name: the work-tree copy exits 0 without running the entry script" "0:" "$?:$out"
out="$(bash "$GUARD/git/info/lefthook-remotes/r/.lefthook/$event/$script" </dev/null 2>&1)"
expect "$name: the fetched copy runs the entry script" "ran" "$out"
# A file whose name holds the backslashes stands in for the Windows path of the fetched copy.
win="$GUARD/git\\info\\lefthook-remotes\\r\\.lefthook\\$event\\$script"
cp "$wrapper" "$win"
out="$(bash "$win" </dev/null 2>&1)"
expect "$name: the fetched copy runs from a backslash path" "ran" "$out"
entry="${script#guardrails-}"
expect "$name: not a shared-copy job name" yes "$([[ "$name" != "guardrails-${entry%.sh}" ]] && echo yes)"
done <<<"$jobs"
rm -rf "$GUARD"
expect "entries.json's remote names this config" "plugins/guardrails/git-hooks/lefthook-remote.yml" \
"$(jq -r '.remote.config' "$HERE/entries.json" 2>/dev/null || sed -n 's/.*"config": "\(.*\)".*/\1/p' "$HERE/entries.json")"

Expand Down
Loading