Repository navigation
feat(guardrails): serve the git hooks to lefthook repositories as a remote - #9504
Conversation
…emote Shared scope with lefthook now commits one lefthook `remotes:` entry instead of copies under .githooks/guardrails/: lefthook fetches plugins/guardrails/git-hooks/lefthook-remote.yml and its scripts from this repository's main and refetches them every 24h, so every consumer runs the current hooks with no refresh pull request and no lint exclusion. - lib/git-hooks/install.mjs (and its 10 generated copies): an optional `remote` block in entries.json; `plan --scope remote` returns the snippet, lefthook-install, migrate (from: shared) and remote-needs-lefthook decisions; verify reports ok only when the config carries the remote, the event hook is lefthook's and the remote is fetched. Plugins without the block are unchanged. - .lefthook/<event>/guardrails-<entry>.sh: lefthook looks up a remote's scripts under the remote repository's root .lefthook/; each runs the plugin's own entry script. - Remote job names are guardrails-remote-<entry>: lefthook merges a job of the same name across configs, and the shared-copy job's run: plus the remote's script: fail as one job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lefthook looks up a remote config's scripts under the consumer's own source_dir inside the fetched copy, so a consumer that sets one gets "script does not exist" on every commit (probed with lefthook 2.2.2). `plan --scope remote` now returns a remote-source-dir decision there and setup falls back to shared copies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Consumers now fetch the guardrails hooks from refs/heads/guardrails-release rather than main, so they run released code only (owner decision on #9504). - release-move-guardrails-ref.yml: on a push to main that touches the guardrails manifest, or a manual run from main, fast-forwards guardrails-release to main's newest guardrails version change, creating it on the first run. It refuses any other ref, a target off main and a move that is not a fast-forward, and pushes with the job's GITHUB_TOKEN (contents: write on that job only), which starts no workflow. - .github/scripts/guardrails-release-ref/move.sh decides the move; its test covers each case. - verify reads the cache directory lefthook 2.2.2 names, which writes each "-" of the ref as "--" (claude-code-plugins-guardrails--release), and still accepts the unescaped name an earlier lefthook used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…release A broken lefthook-remote.yml on guardrails-release would block every consumer's commits, and CI ran only the static half of its test. - .github/scripts/guardrails-release-ref/install-lefthook.sh installs the fleet's lefthook 2.2.2, checked against the release's published SHA-256. - release-move-guardrails-ref.yml runs lefthook-remote.test.sh against the target commit with that lefthook before it pushes, so a failing target stops the run. - test-bash installs it on the leg that runs lefthook-remote.test.sh (a new `lefthook` need in plan-test-lanes.sh) and sets GUARDRAILS_REQUIRE_LEFTHOOK=1, so the end-to-end cases fail there rather than skip. - README: the recovery command for a clone whose fetched remote no longer parses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a shadowing tag Review fixes for the remote install mode: - verify and plan read the remotes: entry whose git_url is exactly the plugin's; another ref is a lefthook-config decision (foundRef) and a missing verdict that says to set the ref, and another host or a comment no longer counts as configured. - Leftover shared jobs are found under a quoted name or a commands: key, and a remote plan also lists local copies to migrate. - The move workflow refuses while a guardrails-release tag exists, since consumers fetch the short name and git prefers the tag. - The setup skill's scope question says shared lefthook runs the released hooks fetched daily. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… in TOML or JSON The exact-match reader only understood YAML items that start with git_url, so a TOML or JSON lefthook config, or reordered keys, read as unconfigured. It now reads each remotes entry whole, in all three formats. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5529a1c3bc
ℹ️ 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".
|
claude-security-review reviewed Incremental security review of 4381c64 against 7132cf3: no security issues found. The change is a single character class edit. In
critical-findings: 0 |
|
claude-review reviewed Incremental review of 4381c64 against 7132cf3: no findings. The change widens the
The new test The same one-line change appears in the 10 carrier copies (actionlint, bash-format, biome-format, go-format, guardrails, instruction-placement, markdown-format, powershell-format, ruff-format, typos-format). Each hunk in the diff is identical to the canonical one, so the shared copies stay in step. I did not run No |
…nd point at lefthook's local-override docs - remotePlan read source_dir with a YAML-only expression, so a TOML or JSON config with its own source_dir was offered the remote, whose scripts it cannot find. - The setup skill no longer restates lefthook's local-override schema; it points at the lefthook-local and skip pages with an as-of date and recheck trigger. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A configs: [a, b] list or a {git_url: ..., ref: ..., configs: [...]} item
read as no remote, so a correctly configured repository never verified and
plan offered a second entry. Comment lines inside an item are skipped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Reply to the claude-security-review finding on 5529a1c (consumers run code from an unprotected mutable branch):
The fix is a ruleset in github-iac (branch create, update and delete; tag creation; a dedicated App bypass), tracked in #9531. That is an org trust-boundary change, so this PR does not make it. The open question for the owner is whether this PR merges before #9531 lands or waits for it. The branch comes into existence when the guardrails release PR merges, and |
…ase branch Owner decision: consumers' lefthook remote fetches main, which changes only through reviewed pull requests under main's rulesets with the remote's end-to-end test in PR CI. entries.json `remote.ref` is now `main` (cache dir claude-code-plugins-main), and the guardrails-release branch, its move workflow, move.sh and its test are removed. The pinned lefthook installer moves to .github/scripts/pr-require-checks/, still used by test-bash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Follow-up on the claude-security-review finding (consumers run code from an unprotected mutable branch), after the owner's decision:
No branch is left to protect, so the follow-up #9531 is closed. |
…consumer Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a renamed lefthook cache - A key named in a trailing comment (`# pinned, ref: develop`) no longer shadows the entry's real key. - verify falls back to any <repo name>-* directory under lefthook-remotes that holds the config, so a lefthook that renames its cache still verifies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A column-0 comment inside the YAML remotes: block ended the block, so later entries went unparsed and plan offered a duplicate remote. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lugins lefthook remote (#148) No related issue: rolls out the guardrails lefthook remote (claude-code-plugins#9504) ## Summary Runs the guardrails plugin's three git hooks in this repository from one lefthook `remotes:` entry, so every clone gets the same hooks from `lefthook install` and they update from `claude-code-plugins` main every 24 hours, with no vendored copies: - `commit-msg-convention` (commit-msg): the subject against the synced commit convention (`docs/conventions/source-control/commit-convention.yml`). - `pre-commit-content` (pre-commit): secrets and machine-specific paths on added lines. - `reference-verify` (pre-push): CLI flags, `/plugin:skill` references and deleted paths on pushed lines. No plugin component changes, so the plugin documentation preflight in `AGENTS.md` does not apply; this is repository tooling only. ## Fix - `lefthook.yml` (new; the repository had none): one `remotes:` entry, `git_url: https://github.com/melodic-software/claude-code-plugins`, `ref: main`, `refetch_frequency: 24h`, `configs: [plugins/guardrails/git-hooks/lefthook-remote.yml]`, from `install.mjs plan --scope remote`. - Nothing else differs from `main`: the earlier vendored `.githooks/guardrails/` copies, their lefthook jobs, the README layout lines and the `.claude/source-control.md` `subject_pattern` are gone (the synced commit convention supplies the pattern). - Installer `no-ci-rerun` decision: no CI workflow here runs `jq`/`reference-verify`, so a commit pushed without the hook is not re-checked in CI. ## Verification `install.mjs verify --repo <worktree>` after `lefthook install` (rc 0): ``` ok commit-msg-convention ok pre-commit-content ok reference-verify ``` A second `plan --scope remote` returns no `lefthook-config` or `migrate` decision. This PR's commit ran the remote `commit-msg` and `pre-commit` hooks and the push ran `guardrails-remote-reference-verify`. Observation runs against this repository's history in report-only mode, before blocking (the remote serves the same hook scripts, so this evidence holds): | Hook | Observation | Findings | Classification | |---|---|---|---| | `reference-verify` | pre-push replay of `origin/main~137..origin/main` (whole history, 138 commits), 35 changed `.md`/`.sh`/`.ps1` files, 3 s | 0 | none; largest scanned file is 46,507 bytes, under the 128 KiB argument limit | | `pre-commit-content` | each of the last 100 non-merge commits replayed into the index (1 to 15 staged files each, that commit's own), 14 s total | 1 commit (09e5a79, #119) | narrowly justified exception: `validate-marketplace.test.mjs:773` feeds `C:\Users\alice\plugin` to the validator as test input it must reject. Current content is clean; a future edit of that line would block. Repo-side exemption list tracked as item 5 of [claude-code-plugins#9426](melodic-software/claude-code-plugins#9426) | | `commit-msg-convention` | subject pattern tested against all 138 subjects on main | 1 non-matching: `Initial commit` | narrowly justified exception: the repository's root commit, historical only | ## Related - [claude-code-plugins#9504](melodic-software/claude-code-plugins#9504): the lefthook remote config this entry points at. - [claude-code-plugins#9426](melodic-software/claude-code-plugins#9426): the rollout and the installer defects it tracks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…lugins lefthook remote (#126) No related issue: rolls out the guardrails lefthook remote (claude-code-plugins#9504) ## Summary Runs the guardrails plugin's three git hooks in this repository from one lefthook `remotes:` entry, so every clone gets the same hooks from `lefthook install` and they update from `claude-code-plugins` main every 24 hours, with no vendored copies: - `commit-msg-convention` (commit-msg): the subject against the synced commit convention (`docs/conventions/source-control/commit-convention.yml`). - `pre-commit-content` (pre-commit): secrets and machine-specific paths on added lines. - `reference-verify` (pre-push): CLI flags, `/plugin:skill` references and deleted paths on pushed lines. These are repository git hooks, not a plugin port or Cursor plugin hooks, so the trust-review rule for ports that add MCP servers or hooks does not apply. ## Fix - `lefthook.yml` (new): one `remotes:` entry, `git_url: https://github.com/melodic-software/claude-code-plugins`, `ref: main`, `refetch_frequency: 24h`, `configs: [plugins/guardrails/git-hooks/lefthook-remote.yml]`, from `install.mjs plan --scope remote`. - Nothing else differs from `main`: the earlier vendored `.githooks/guardrails/` copies, their lefthook jobs, the README layout lines and the new `.claude/source-control.md` are gone (the synced commit convention supplies the pattern). ## Verification `install.mjs verify --repo <worktree>` after `lefthook install` (rc 0): ``` ok commit-msg-convention ok pre-commit-content ok reference-verify ``` A second `plan --scope remote` returns no decisions. This PR's commit ran `guardrails-remote-pre-commit-content` and `guardrails-remote-commit-msg-convention`, and its push ran `guardrails-remote-reference-verify`, all passing. Observation runs against this repository's history in report-only mode (the remote serves the same hook scripts, so this evidence holds): | Hook | Observation | Findings | Classification | |---|---|---|---| | `reference-verify` | pre-push replay of `origin/main~121..origin/main` (the repo has 122 commits), 52 changed files, 3 s | 0 | none | | `pre-commit-content` | each of the last 100 non-merge commits replayed into the index (every replay staged 1 or more files, only that commit's), 8 s total | 1 commit (3ef6f19, #23): 4 lines | narrowly justified exception: `scripts/sync-local.sh` and `plugins/plugin-ops/skills/sync-local/SKILL.md` name the `D:\`/`C:\` `repos\github.com\melodic-software\cursor-plugins` checkouts in the deliberate, fixed fallback list `sync-local` tries when no `-Source` is given (README, Install / update). Current content still has them, so a future edit of those lines would block. Whether that fallback list fits the plugin design boundary (no machine paths) is a separate question for the owner | | `commit-msg-convention` | subject pattern against all 122 subjects on main | 2 non-matching | pre-convention history: `Add Cloud Agent environment for cursor-plugins (#18)` and `Initial commit` | ## Related - [claude-code-plugins#9504](melodic-software/claude-code-plugins#9504): the lefthook remote config this entry points at. - [claude-code-plugins#9426](melodic-software/claude-code-plugins#9426): the rollout and the installer defects it tracks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…lugins lefthook remote (#865) No related issue: rolls out the guardrails lefthook remote (melodic-software/claude-code-plugins#9504) ## Summary The guardrails git hooks now run from one lefthook `remotes:` entry tracking `melodic-software/claude-code-plugins` `main` (refetched daily) instead of copies vendored under `.githooks/guardrails/` by #852. Every clone gets the same hooks from committed config, with nothing per machine and no copies to drift. ## Fix - `lefthook.yml`: add the `remotes:` entry; drop the three `guardrails-<entry>` jobs and the `.githooks/guardrails/**` pre-commit exclude. - Delete `.githooks/guardrails/` (16 files). - `pr-require-checks.yml`: drop the `.githooks/guardrails` ShellCheck and machine-path excludes. - `components/commit-convention/commit-convention.test.sh`: fetch the guardrails plugin from claude-code-plugins `main` (sparse, depth 1) for the resolver and commit-msg hook, the same ref consumers run. The suite already needed network for the check-contract action. ## Verification - `install.mjs verify`: `ok` for commit-msg-convention, pre-commit-content, reference-verify; `plan --scope remote` returns no decisions. - This commit ran `guardrails-remote-pre-commit-content` and `guardrails-remote-commit-msg-convention` from the fetched remote; both passed. - `bash harness/shell/run-tests.sh components/commit-convention/commit-convention.test.sh`: 1 passed. ## Related - melodic-software/claude-code-plugins#9504 (remote mode) - melodic-software/claude-code-plugins#9426 (comment) (owner decision: `ref: main`) - #852 (the copies this removes) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…lugins lefthook remote (#108) No related issue: rolls out the guardrails lefthook remote (claude-code-plugins#9504) ## Summary Runs the guardrails plugin's three git-hook entries in every clone of this repository from one committed lefthook `remotes:` entry, so each machine gets the same hooks from `lefthook install` and picks up upstream fixes automatically: - `commit-msg-convention` (commit-msg): the subject against the rule in `docs/conventions/source-control/commit-convention.yml`. - `pre-commit-content` (pre-commit): secrets and machine-specific paths on added lines. - `reference-verify` (pre-push): CLI flags, `/plugin:skill` references and deleted paths on pushed lines. ## Fix - `lefthook.yml` (new; the repository had no hook manager): one `remotes:` entry, `git_url: https://github.com/melodic-software/claude-code-plugins`, `ref: main`, `refetch_frequency: 24h`, config `plugins/guardrails/git-hooks/lefthook-remote.yml`. - That is the whole diff against `main`. The earlier revision of this PR vendored copies under `.githooks/guardrails/` and added a `.claude/source-control.md` carrying `subject_pattern`; both are gone. The subject rule now comes from the standards-synced `commit-convention.yml`. - No other changes kept. ## Verification - `install.mjs verify --repo <worktree>` (rc 0): ``` ok commit-msg-convention ok pre-commit-content ok reference-verify ``` - `install.mjs plan --scope remote` afterwards: no decisions left. - `lefthook install`: `sync hooks: ✔️(pre-push, commit-msg, pre-commit)`. This PR's commit ran the remote `pre-commit-content` (0.04 s) and `commit-msg-convention` (0.04 s), and its push ran `reference-verify`. Observation runs from the earlier revision, still valid because the remote runs the same hook scripts: | Hook | Observation | Findings | Classification | |---|---|---|---| | `reference-verify` | pre-push replay of `origin/main~100..origin/main` (102 commits in all), 20 changed files, 5 of them `.md`/`.sh`, 0.5 s | 0 | none | | `pre-commit-content` | each of the last 100 non-merge commits replayed into the index (179 staged paths in all, no replay empty), 7 s total | 0 | none | | `commit-msg-convention` | subject rule tested against all 102 subjects on main | 1 non-matching: `Initial commit` | narrowly justified exception: the repository's root commit, written before the convention; no later subject misses | ## Related - [claude-code-plugins#9504](melodic-software/claude-code-plugins#9504): the lefthook remote config this points at. - [claude-code-plugins#9426](melodic-software/claude-code-plugins#9426): rollout tracking and known hook defects. - [standards#852](melodic-software/standards#852): the pilot. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…lugins lefthook remote (#493) No related issue: rolls out the guardrails lefthook remote (claude-code-plugins#9504) ## Summary Runs the guardrails plugin's three git-hook entries in every clone of this repository from one committed lefthook `remotes:` entry, so each machine gets the same hooks from `lefthook install` and picks up upstream fixes automatically: - `commit-msg-convention` (commit-msg): the subject against the rule in `docs/conventions/source-control/commit-convention.yml`. - `pre-commit-content` (pre-commit): secrets and machine-specific paths on added lines. - `reference-verify` (pre-push): CLI flags, `/plugin:skill` references and deleted paths on pushed lines. ## Fix - `lefthook.yml` (new; the repository had no hook manager): one `remotes:` entry, `git_url: https://github.com/melodic-software/claude-code-plugins`, `ref: main`, `refetch_frequency: 24h`, config `plugins/guardrails/git-hooks/lefthook-remote.yml`. - That is the whole diff against `main`. The earlier revision of this PR vendored copies under `.githooks/guardrails/` and needed ShellCheck, shfmt and machine-path exclusions for them in `.github/workflows/pr-require-checks.yml`, plus a `subject_pattern` in `.claude/source-control.md`; all of that is gone. The subject rule now comes from the standards-synced `commit-convention.yml`. - No other changes kept. ## Verification - `install.mjs verify --repo <worktree>` (rc 0): ``` ok commit-msg-convention ok pre-commit-content ok reference-verify ``` - `install.mjs plan --scope remote` afterwards: no decisions left. - `lefthook install`: `sync hooks: ✔️(commit-msg, pre-push, pre-commit)`. This PR's commits ran the remote `pre-commit-content` and `commit-msg-convention`, and its push ran `reference-verify`. Observation runs from the earlier revision, still valid because the remote runs the same hook scripts: | Hook | Observation | Findings | Classification | |---|---|---|---| | `reference-verify` | pre-push replay of `origin/main~200..origin/main`, 28 changed `.md`/`.sh`/`.ps1` files, 6 s | 0 | none; no file in the range exceeds the argument-size limit (largest `README.md`, 31,947 bytes) | | `pre-commit-content` | each of the last 100 non-merge commits replayed into the index (1 to 65 staged files each; every count matches that commit's own change list, renames counted once), 10 s total | 0 | none. A planted `/home/<name>/...` probe was caught (rc 1), so the replay exercises the check | | `commit-msg-convention` | subject rule tested against the last 500 subjects on main (all 308) | 1 non-matching: `Initial commit` | narrowly justified exception: GitHub's repository-creation commit, never made through a hook | ## Related - [claude-code-plugins#9504](melodic-software/claude-code-plugins#9504): the lefthook remote config this points at. - [claude-code-plugins#9426](melodic-software/claude-code-plugins#9426): rollout tracking and known hook defects. - [standards#852](melodic-software/standards#852): the pilot. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…lugins lefthook remote (#211) No related issue: rolls out the guardrails lefthook remote (claude-code-plugins#9504) ## Summary Runs the guardrails plugin's three git-hook entries in every clone of this repository from one committed lefthook `remotes:` entry, so each machine gets the same hooks from `lefthook install` and picks up upstream fixes automatically: - `commit-msg-convention` (commit-msg): the subject against the rule in `docs/conventions/source-control/commit-convention.yml`. - `pre-commit-content` (pre-commit): secrets and machine-specific paths on added lines. - `reference-verify` (pre-push): CLI flags, `/plugin:skill` references and deleted paths on pushed lines. ## Fix - `lefthook.yml` (new; the repository had no hook manager): one `remotes:` entry, `git_url: https://github.com/melodic-software/claude-code-plugins`, `ref: main`, `refetch_frequency: 24h`, config `plugins/guardrails/git-hooks/lefthook-remote.yml`. - That is the whole diff against `main`. The earlier revision of this PR vendored copies under `.githooks/guardrails/` and needed: - ShellCheck exclusions for them in `.github/workflows/pr-require-checks.yml` and `.cursor/check.sh`. - A README note describing them. - A regex `subject_pattern` in `.claude/source-control.md`. All of that is gone. The subject rule now comes from the standards-synced `commit-convention.yml`. - No other changes kept. ## Verification - `install.mjs verify --repo <worktree>` (rc 0): ``` ok commit-msg-convention ok pre-commit-content ok reference-verify ``` - `install.mjs plan --scope remote` afterwards: no configuration decisions left, only the advisory `no-ci-rerun`: no CI workflow re-runs reference-verify's jq check. - `lefthook install`: `sync hooks: ✔️(pre-commit, commit-msg, pre-push)`. This PR's commit ran the remote `pre-commit-content` (0.11 s) and `commit-msg-convention` (0.03 s), and its push ran `reference-verify`. Observation runs from the earlier revision, still valid because the remote runs the same hook scripts: | Hook | Observation | Findings | Classification | |---|---|---|---| | `reference-verify` | pre-push replay of `origin/main~182..origin/main` (whole history after the root commit; 24 changed `.md`/`.sh`/`.ps1` files), 2 s | 0 | none | | `pre-commit-content` | each of the last 100 non-merge commits replayed into the index (1 to 18 staged files per commit, none empty), 8 s total | 0 | none | | `commit-msg-convention` | subject rule tested against all 183 subjects on main | 1 non-matching: `Initial commit` (root commit) | narrowly justified exception: the repository's creation commit, never re-checked | ## Related - [claude-code-plugins#9504](melodic-software/claude-code-plugins#9504): the lefthook remote config this points at. - [claude-code-plugins#9426](melodic-software/claude-code-plugins#9426): rollout tracking and known hook defects. - [standards#852](melodic-software/standards#852): the pilot. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
No related issue: follow-up to a review finding on #211 (claude-code-plugins#9504 rollout) ## Summary Lists `lefthook.yml` in README's "What's here" inventory, which the tracked-file inventory rule (`AGENTS.md`, Code Review Rules) requires for every added file. ## Fix - `README.md`: new **Git hooks** bullet. `lefthook.yml` holds one `remotes:` entry running the guardrails hooks from `claude-code-plugins`, and the hook scripts are owned there. ## Verification - This commit ran the remote `pre-commit-content` and `commit-msg-convention` hooks (both pass); the push ran `reference-verify`. - Addresses the [Codex finding on #211](#211 (comment)). ## Related - [#211](#211): added `lefthook.yml`. - [claude-code-plugins#9504](melodic-software/claude-code-plugins#9504): the remote config. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…lugins lefthook remote (#331) No related issue: rolls out the guardrails lefthook remote (claude-code-plugins#9504) ## Summary Runs the guardrails plugin's three git hooks in this repository from one lefthook `remotes:` entry, so every clone gets the same hooks from committed config and picks up upstream changes within 24 h: - `commit-msg-convention` (commit-msg): the subject against the repository's commit convention (`docs/conventions/source-control/commit-convention.yml`). - `pre-commit-content` (pre-commit): secrets and machine-specific paths on added lines. - `reference-verify` (pre-push): CLI flags, `/plugin:skill` references and deleted paths on pushed lines. ## Fix - `lefthook.yml` (new; the repository had no root lefthook config): only a `remotes:` entry for `https://github.com/melodic-software/claude-code-plugins`, `ref: main`, `refetch_frequency: 24h`, config `plugins/guardrails/git-hooks/lefthook-remote.yml`. The synced `.lefthook/base.yml` and `.lefthook/dotnet.yml` fragments stay unwired, as before. - Compared with the earlier revision of this PR, removed: the vendored `.githooks/guardrails/` copies, the three local `guardrails-*` jobs, and the new `.claude/source-control.md` with its `subject_pattern` (main now carries `commit-convention.yml`). - No other change. ## Verification `install.mjs verify --repo <worktree>` (rc 0): ```text ok commit-msg-convention ok pre-commit-content ok reference-verify ``` - `install.mjs plan --scope remote` afterwards: no `lefthook-config` or `migrate` decision left. This PR's commit ran `guardrails-remote-pre-commit-content` (0.06 s) and `guardrails-remote-commit-msg-convention` (0.04 s); its push ran `guardrails-remote-reference-verify` (0.07 s). - `install.mjs plan` reports the advisory `no-ci-rerun` for `reference-verify` (`jq`): no CI workflow here runs it, so a skipped push is not re-checked in CI. Observation runs from the earlier revision still hold, since the remote runs the same hook scripts: | Hook | Observation | Findings | Classification | |---|---|---|---| | `reference-verify` | pre-push replay of `origin/main~200..origin/main`, 39 changed `.md`/`.sh`/`.ps1` files, 14 s | 0 | none; largest file in range `CHANGELOG.md` at 79,256 bytes, under the 128 KiB argument limit | | `pre-commit-content` | each of the last 100 non-merge commits replayed into the index (1 to 29 staged files each), 20 s total | 0 | none | | `commit-msg-convention` | `subject_pattern` tested against all 234 subjects on main | 1 non-matching: `Initial commit` (the root commit) | narrowly justified exception: the repository's first commit, never repeated | ## Related - [claude-code-plugins#9504](melodic-software/claude-code-plugins#9504): the lefthook remote config this points at. - [claude-code-plugins#9426](melodic-software/claude-code-plugins#9426): the rollout and its recorded design decision. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Refs: #9426
Summary
/guardrails:setup apply install-git-hooks scope=sharedin a lefthook repository, or one with no hook manager yet, now commits one lefthookremotes:entry instead of copies under.githooks/guardrails/. Lefthook fetchesplugins/guardrails/git-hooks/lefthook-remote.ymland its scripts from this repository'smainbranch and refetches them every 24h. Every consumer then runs the guardrails hooks as merged tomain, with no refresh pull requests and no lint exclusions. This is the owner's decision. Husky and plain hooks keep the shared copies.What a consumer commits in
lefthook.yml(frominstall.mjs plan --scope remote):Footprint.
install.mjsis a shared copy: its canonical source islib/git-hooks/install.mjs, registered inscripts/shared-copies.txt:496-505. Changing guardrails' copy therefore means changing the canonical and regenerating all 10 carriers.--check-bumpthen needs a fragment for each carrier, written withscripts/new-changelog-fragment.sh --carriers-of. The session-start drift notice runs that installer throughlib/prerequisites.mjs, which is also shared (scripts/shared-copies.txt:234), so no guardrails-only module could quiet it. The new mode is opt-in through anentries.jsonremoteblock, so the other nine plugins behave exactly as before.Ref:
main(owner decision). Consumers track this repository'smain, which changes only through reviewed pull requests under main's existing rulesets.lefthook-remote.test.shruns end to end in PR CI:test-bashinstalls the pinned lefthook (2.2.2, SHA-256 checked,.github/scripts/pr-require-checks/install-lefthook.sh) on the leg that runs it, andGUARDRAILS_REQUIRE_LEFTHOOK=1makes a missing lefthook fail instead of skip. There is no release branch and no move workflow.Cache path.
<git common dir>/info/lefthook-remotes/claude-code-plugins-main/(lefthook's<repo name>-<ref>,RemoteDirectoryNamein internal/git/remote.go).#9426 coverage
Fix
lib/git-hooks/install.mjsand its 10 regenerated copies:remoteblock (git_url,ref,config), and it is validated.plan --scope remotewrites nothing. It returns these decisions:lefthook-configwith the snippet above;lefthook-installuntil every entry event's hook is lefthook's and the remote is fetched;migrate(from: shared), naming.githooks/guardrails/and the oldguardrails-<entry>jobs;remote-needs-lefthookfor husky or plain hooks;remote-source-dirwhen the consumer's lefthook config sets its ownsource_dir;declinedand the tool decisions, as before.verifyreportsokonly when three things hold: the config has aremotes:entry with exactly the plugin'sgit_url,refand config in block or flow YAML, TOML or JSON (another ref ismissingwith "set ref: main", andplanreturnslefthook-configwithfoundRef), the event hook is lefthook's, and<git-path info>/lefthook-remotes/claude-code-plugins-main/<config>exists. Otherwise it reportsmissingwith "run lefthook install".copy --scope remoteis a usage error.plugins/guardrails/git-hooks/lefthook-remote.ymlholds the three jobs, namedguardrails-remote-<entry>.entries.jsongains theremoteblock..lefthook/<event>/guardrails-<entry>.shat the repository root: three one-line scripts, each running the plugin's own entry script. The scripts have to live there. When a remote config sets a top-levelsource_dir, it overrides the consumer's ownsource_dirand breaks the consumer's own script jobs (evidence below). With nosource_dir, lefthook looks up a remote's scripts under the remote repository's root.lefthook/.skills/setup/SKILL.md:scope=sharedrunsplan --scope remotefirst. A clone-only opt-out skips theguardrails-remote-<name>job in the clone's lefthook local config, pointing at lefthook's docs for the syntax (tested withskip: true: lefthook prints "(skip) by condition").reference/git-hook-decisions.mdand the README also cover the new mode.lib/git-hooks/install.test.mjs. The first 7 were checked against the old installer by restoringHEAD'sinstall.mjs, and all 7 failed. Thesource_dircase fails there too, because the old installer has no--scope remote. Review added 7 more (another ref, another host or a comment, quoted andcommands:job names, local copies in a remote plan, key order with flow-style YAML, TOML and JSON configs, andsource_dirin TOML and JSON); each fails on the installer before its fix.plugins/guardrails/git-hooks/lefthook-remote.test.sh. It runs 11 static checks everywhere. When lefthook is on PATH it adds end-to-end cases against afile://stand-in: the three entries block from the fetched copy, the cache isclaude-code-plugins-main, and a new commit on main reaches the consumer onlefthook install.scripts/plan-test-lanes.test.shfor thelefthookneed.minor, and the 9 other carrierspatch.Verification
Step 1 probe with lefthook 2.2.2 (
lefthook version), run in throwaway repositories (obs-lefthook-remote-*beside the worktree). The stand-in is a local bare repository holding this branch'splugins/guardrailsand.lefthook/. The consumer'slefthook.ymlholds the exact snippet above, andgit config url.file://<stand-in>.insteadOf https://github.com/melodic-software/claude-code-pluginspoints that URL at the stand-in. Findings:git -C <repo>/.git/info/lefthook-remotes clone --quiet --origin origin --depth 1 --branch main <url> claude-code-plugins-main, thenfetch --depth 1 origin -- mainandcheckout FETCH_HEAD. That is a shallow clone at<git common dir>/info/lefthook-remotes/<repo name>-<ref>/. A linked worktree uses the same one (git rev-parse --git-path infonames the main.git/info).script:job whose file is in the remote. The probe printedpwd=<consumer root> src=<consumer>/.git/info/lefthook-remotes/.../probe.sh args=.git/COMMIT_EDITMSG, so the script runs from the fetched copy with the consumer as its working directory and git's arguments. Arun:command also runs in the consumer root, but nothing gives it the remote's path.source_dirin a remotelefthook dumpshowed one top-levelsource_dir: plugins/guardrails/git-hooks/lefthook/, and the consumer's ownscript: mine.shjob failed withscript does not exist: mine.sh. Without it, scripts resolve under the remote's root.lefthook/, and both the consumer's.lefthook/pre-commit/mine.shand the remote's scripts ran.commit-msg: an off-pattern subject blocked (Rewrite the subject to match the pattern..., rc=1) andABC-1: initialpassed.pre-commit:notes.md:2: secret or credential pattern: GitHub PAT, rc=1.pre-push(use_stdin: true, on a branch with no upstream, pushingmain):readme.md:1: stale-path-verify: cited path(s) in readme.md were deleted from this repo, rc=1. Each entry found its libraries in the plugin layout and read the consumer's index, message and.claude/source-control.md.guardrails-commit-msg-conventionrun:job plus a remote job of that name merged into one job holding bothrun:andscript:, which failed:either run, script, or group must be provided for a job. The remote jobs are therefore namedguardrails-remote-<entry>. With different names, a half-migrated repository runs both.Couldn't sync https://github.com/melodic-software/claude-code-plugins main. Will continue with fallback version: main.and ran the cached jobs; the commit went through.lefthook installprintedCouldn't sync from ... Will continue anyway: exit status 128andsync hooks: ✔️but installed onlyprepare-commit-msg, so commits were unchecked. Restoring the network did not repair it; a secondlefthook installdid.verifynow reportsmissingin this state.refand refetchlefthook installalways fetches, for a branch and for a force-moved tag alike. A hook run within 24h does not fetch. A hook run after the cache's.gitmtimes were aged 25h did fetch, for the branch and the tag alike. The ref docs warn against droppingrefonce used. Per the refetch_frequency docs, "Failure to fetch does not cause an error, but just a warning message."source_dirsource_dir: .hooks/, lefthook looks for the remote's scripts under.hooks/in the fetched copy:guardrails-remote-pre-commit-content: script does not exist: guardrails-pre-commit-content.sh, and the commit fails.plan --scope remotenow returnsremote-source-dirfor this case. None of standards'components/lefthook-*/lefthook.ymlsetssource_dir.mainat eaac7fa)plan --scope remotereturnedlefthook-config,lefthook-install, andmigrate(from: shared, the three.githooks/guardrails/scripts, the threeguardrails-<entry>jobs). After applying them (snippet added,.githooks/guardrails/and the three jobs removed, the pre-commitexcludeline dropped),lefthook installsyncedcommit-msg, pre-commit, pre-pushandverifyprintedokthree times.lefthook dumpshowed the remote jobs merged alongside standards'commands:and hook-levelexclude. A non-conventional subject blocked; a conventional one committed, with editorconfig, typos, gitleaks andguardrails-remote-pre-commit-contentpassing.Error: yaml: ...(rc=1), andlefthook installfailed the same way before it refetched. To recover, runrm -rf "$(git rev-parse --git-path info)/lefthook-remotes/claude-code-plugins-main" && lefthook installonce the fix is on main (also in the README) (the empty-cache reinstall above is the evidence).Suites:
node --test lib/git-hooks/install.test.mjs: 60 pass.bash plugins/guardrails/git-hooks/lefthook-remote.test.shwithGUARDRAILS_REQUIRE_LEFTHOOK=1and lefthook 2.2.2: 19 pass. With lefthook stripped from PATH: a SKIP, or a FAIL whenGUARDRAILS_REQUIRE_LEFTHOOK=1.plan-test-lanes.test.sh43 pass. actionlint is clean onpr-require-checks.yml.plugins/*/git-hooks/*.test.shandplugins/guardrails/hooks/git-hooks-notice.test.shpass.scripts/sync-shared-copies.sh --checkreports 520 copies match.--check-bump origin/mainpasses.scripts/check-changelog-fragments.sh --checkpasses.Limitations:
lefthook-remote.ymlonmainwould block every consumer's commits until it is fixed and refetched. Before merge,test-bashinstalls the pinned lefthook on the leg that runslefthook-remote.test.sh(a newlefthookneed inscripts/plan-test-lanes.sh) and setsGUARDRAILS_REQUIRE_LEFTHOOK=1, so that suite fails there instead of skipping.verifylooks for lefthook 2.2.2's observed cache path,<repo name>-<ref>, and falls back to any<repo name>-*directory holding the config, so a lefthook release that renames it still verifies.source_dircannot use the remote (remote-source-dir).mainwithin 24h of a merge; a change to the hooks reaches them only through a reviewed pull request whose CI ran the end-to-end test.Related
🤖 Generated with Claude Code