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
2 changes: 1 addition & 1 deletion docs/CATALOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ plugin manifests and kept in sync by CI — never hand-edit it; the category voc

## Security

- [`guardrails`](../plugins/guardrails) — Twelve safety guards that block secret/credential writes, hardcoded machine-specific paths, git hook-bypass attempts, irreversible git operations (force-push, reset --hard, worktree-wide checkout/restore discards), Bash file-write workarounds that circumvent Write/Edit hooks, commit subjects and gh pr create titles that violate the repo's tracked team convention (when one is declared in .claude/source-control.md), (advisory) hallucinated CLI flags, (advisory) /plugin:skill references that do not resolve, (advisory) markdown citing a repo path the repo's own history shows was removed, (advisory) un-throttled Workflow fan-out that risks burst 529s, and (advisory) direct git commit/gh pr create calls bypassing this marketplace's own commit/pull-request skills — each independently toggleable.
- [`guardrails`](../plugins/guardrails) — Twelve safety guards that block secret/credential writes, hardcoded machine-specific paths, git hook-bypass attempts, irreversible git operations (force-push, reset --hard, worktree-wide checkout/restore discards), Bash file-write workarounds that circumvent Write/Edit hooks, multi-line `git commit -m` messages (an actual-newline `-m` mangles across shells; single-line `-m` passes), commit subjects and gh pr create titles that violate the repo's tracked team convention (when one is declared in .claude/source-control.md), (advisory) hallucinated CLI flags, (advisory) /plugin:skill references that do not resolve, (advisory) markdown citing a repo path the repo's own history shows was removed, (advisory, opt-in) un-throttled Workflow fan-out that risks burst 529s, and (advisory, opt-in) direct gh pr create calls bypassing this marketplace's own pull-request skill — each independently toggleable.

## Workflow

Expand Down
16 changes: 8 additions & 8 deletions plugins/guardrails/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "guardrails",
"version": "0.19.5",
"description": "Twelve safety guards that block secret/credential writes, hardcoded machine-specific paths, git hook-bypass attempts, irreversible git operations (force-push, reset --hard, worktree-wide checkout/restore discards), Bash file-write workarounds that circumvent Write/Edit hooks, commit subjects and gh pr create titles that violate the repo's tracked team convention (when one is declared in .claude/source-control.md), (advisory) hallucinated CLI flags, (advisory) /plugin:skill references that do not resolve, (advisory) markdown citing a repo path the repo's own history shows was removed, (advisory) un-throttled Workflow fan-out that risks burst 529s, and (advisory) direct git commit/gh pr create calls bypassing this marketplace's own commit/pull-request skills — each independently toggleable.",
"version": "0.20.0",
Comment thread
kyle-sexton marked this conversation as resolved.
"description": "Twelve safety guards that block secret/credential writes, hardcoded machine-specific paths, git hook-bypass attempts, irreversible git operations (force-push, reset --hard, worktree-wide checkout/restore discards), Bash file-write workarounds that circumvent Write/Edit hooks, multi-line `git commit -m` messages (an actual-newline `-m` mangles across shells; single-line `-m` passes), commit subjects and gh pr create titles that violate the repo's tracked team convention (when one is declared in .claude/source-control.md), (advisory) hallucinated CLI flags, (advisory) /plugin:skill references that do not resolve, (advisory) markdown citing a repo path the repo's own history shows was removed, (advisory, opt-in) un-throttled Workflow fan-out that risks burst 529s, and (advisory, opt-in) direct gh pr create calls bypassing this marketplace's own pull-request skill — each independently toggleable.",
"author": {
"name": "Melodic Software",
"email": "info@melodicsoftware.com"
Expand Down Expand Up @@ -53,7 +53,7 @@
"block_noncanonical_commit_enabled": {
"type": "boolean",
"title": "block-noncanonical-commit guard",
"description": "Block `git commit` that does not pipe its message via `-F -`; --amend, -C/-c, --fixup/--squash, -F <path>, and an in-progress merge/rebase are exempt",
"description": "Block `git commit -m` when the message actually contains a newline (multi-line `-m` mangles across shells — pipe it via `-F -` instead; single-line `-m` passes); --amend, -C/-c, --fixup/--squash, -F <path>, and an in-progress merge/rebase are exempt",
"default": true
},
"block_convention_gate_enabled": {
Expand Down Expand Up @@ -83,14 +83,14 @@
"workflow_resilience_check_enabled": {
"type": "boolean",
"title": "workflow-resilience-check guard",
"description": "Advise on un-throttled Workflow fan-out (never blocks)",
"default": true
"description": "Advise on un-throttled Workflow fan-out (never blocks). Default off since 0.20.0: a behavioral-class prose injector, config-disabled per the instruction-economy evidence gate (#2021) — set true to opt back in",
"default": false
},
"flag_commit_pr_skill_bypass_enabled": {
"type": "boolean",
"title": "flag-commit-pr-skill-bypass guard",
"description": "Advise when direct git commit / gh pr create bypasses the source-control skills (never blocks)",
"default": true
"description": "Advise when a direct gh pr create bypasses the source-control pull-request skill (never blocks). Default off since 0.20.0: a behavioral-class prose injector, config-disabled per the instruction-economy evidence gate (#2021) — set true to opt back in",
"default": false
},
"cli_flag_verify_bins": {
"type": "string",
Expand All @@ -113,7 +113,7 @@
"block_noncanonical_commit_allow": {
"type": "string",
"title": "block-noncanonical-commit allow-list",
"description": "Comma-separated form tokens to allow (currently: message-flag, which permits a bare `-m`)",
"description": "Comma-separated form tokens to allow (currently: message-flag, which permits `-m` even when the message contains a newline)",
"default": ""
},
"block_no_verify_hook_manager_prefixes": {
Expand Down
72 changes: 72 additions & 0 deletions plugins/guardrails/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,78 @@
All notable changes to the `guardrails` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.20.0]

### Changed

- **The two behavioral-class advisory injectors now default OFF: `flag-commit-pr-skill-bypass` and
`workflow-resilience-check`.** Issue #2021's hook-surface classification found these are the
plugin's only two clean behavioral-class context injectors — fixed prose that consults no external
ground truth (`flag-commit-pr-skill-bypass` emits a static nudge toward `/pull-request create`;
`workflow-resilience-check` runs two greps and emits a fixed ~120-word checklist asserting nothing
the model cannot derive). Per `docs/PLUGIN-PHILOSOPHY.md` "Instruction economy", a hook that
corrects model behavior is an ablation candidate, and the evidence-gated order is **config-disable
first where a kill switch exists** — so the scripts and their wiring stay, and a consumer opts back
in by setting the existing `flag_commit_pr_skill_bypass_enabled` / `workflow_resilience_check_enabled`
userConfig option to `true`. Deletion, if ever, is a separate change gated on ablation evidence.

**Mechanism, stated because the shared helper's fallback points the other way:**
`hook::check_enabled` reads the `CLAUDE_PLUGIN_OPTION_<NAME>_ENABLED` process mirror with an
UNSET-means-true fallback, which would silently re-enable a default-off hook anywhere the harness
does not materialize userConfig defaults into the environment. Both hooks therefore switch to an
explicit opt-in test (`[[ "${VAR:-false}" == "true" ]] || exit 0` — the same shape session-flow's
default-off `observer-arm` uses), and the `plugin.json` defaults flip to `false` so the
configuration dialog and `${user_config.*}` agree. Per the current plugins reference
(<https://code.claude.com/docs/en/plugins-reference>, fetched 2026-08-08), `default` is the
"Value used when the user provides nothing" and options are "exported to hook processes as
`CLAUDE_PLUGIN_OPTION_<KEY>`"; the script-side default is what makes the OFF posture hold when
that export is absent. Each test suite now exports the switch ON for its behavior cases and pins
the unset-switch no-op as its own case.

- **`block-noncanonical-commit` narrowed to the actual hazard: only a `-m` message that REALLY
contains a newline blocks.** The guard used to deny every `git commit` that was not the `-F -`
stdin form — `git commit -m "fix: typo"` included — which #2021 classified hybrid: the multi-line
`-m` cross-shell mangling is a policy-grade hazard, but the blanket width policed style. Now:

- a single-line `-m` passes; a `-m`/`--message` value carrying an actual newline blocks, in every
spelling the argv scan sees — separated (`-m <msg>`), attached (`-m"<msg>"`), `--message=<msg>`,
every accepted unique abbreviation of `--message` (any prefix from `--m` up to one letter
short of the full spelling, separated or `=`-attached — git's parse-options accepts any
unique long-option prefix and `--message` is git commit's only `m`-initial long option;
verified on git 2.55), and a short-option cluster ending in `m` (`-am <msg>`);
- bare `git commit` / `git commit -a` (no message source; the old block) now pass — no `-m`, no
mangling hazard;
- repeated single-line `-m` flags pass: git itself joins them as paragraphs, no shell newline is
involved;
- the exemptions are unchanged (`--amend`, `-C`/`-c`, `--fixup`/`--squash`, `-F`, in-progress
sequencer), as are the fail-closed structural refusals (`--config-env` alias shape,
alias-traversal budget, unparsable PowerShell);
- on the PowerShell tool a here-string `-m` value still blocks: the classifier blanks the body to
a placeholder, so its content — multi-line by construction of the form — cannot be inspected,
and the guard fails closed on it. A single-line literal PowerShell `-m` passes;
- the `block_noncanonical_commit_allow` token `message-flag` now means "permit `-m` even with a
newline"; the kill switch is unchanged.

**Accepted residual, fail-OPEN and documented in the hook header:** a message attached to a
short-option cluster (`-am"multi<NL>line"`) is not recognized — which cluster letters take values
is per-option knowledge the scan does not model — consistent with the guard's friction-not-sandbox
posture. The test suite is respelled in both directions: every alias/wrapper/traversal fixture
that asserted a block now carries a real-newline `-m` payload (so it still pins the machinery it
was written for), and new cases pin the allowed single-line forms.

- **`hooks.json`: the two structurally separate PreToolUse groups carrying the identical
`Bash|PowerShell` matcher are merged into one six-hook group.** Pure wiring cleanup flagged by
#2021 — behavior is identical: per the current hooks reference
(<https://code.claude.com/docs/en/hooks>, fetched 2026-08-08), all matching hooks run in parallel,
and same-matcher groups are separate entries that each fire independently, so one group of six and
two groups of four-plus-two schedule the same work.

### Fixed

- **Three stale hook headers said "Triggered on Bash tool calls" while wired `Bash|PowerShell`:**
`block-hook-bypass.sh`, `block-noncanonical-commit.sh`, and `flag-commit-pr-skill-bypass.sh` now
say Bash and PowerShell (cosmetic; the wiring itself was already correct).

## [0.19.5]

### Fixed
Expand Down
15 changes: 9 additions & 6 deletions plugins/guardrails/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ Each guard is independently toggleable, so you run exactly the subset you want.
| **block-dangerous-git** | PreToolUse · Bash | **Blocks** (exit 2) | Irreversible git operations: `push --force`/`-f` plus the equivalent leading-`+` refspec and `--mirror` forms, and the unsafe `--force-with-lease` spellings, in the two kinds git itself treats differently. **No expected value** (bare `--force-with-lease` or `=<refname>`) leases against the remote-tracking ref, which git documents as "trivially defeated" by a background fetch — blocked unless `--force-if-includes` is present, which git documents as the mitigation for exactly this form. **A movable `=<refname>:<expect>`** — `origin/main`, `HEAD`, a tag, an *abbreviated* object id, or hex of the wrong width for this repository's hash format, all of which git resolves at push time, and gitrevisions resolves a short hex word as a ref before trying it as an object-id prefix — is blocked unconditionally, because git declares `--force-if-includes` a no-op alongside an explicit `:<expect>`. A lease passes only when `<expect>` is immutable: a **literal** object id of the pushed repository's own hash width (detection never evaluates substitutions, so resolve it with `git rev-parse` as a separate step and pass the result) (40 hex under SHA-1, 64 under SHA-256, read from `git rev-parse --show-object-format` with the command's own `-C`/`--git-dir`/`--work-tree`/`--namespace` replayed onto it; undeterminable fails closed) or the empty string asserting the ref must not exist. The other width is a ref name there, not an object id — git ignores a ref whose name is full-width hex for its own format, but resolves one of the other width like any name. git scopes a pin to its own ref, so a bare fallback alongside a pinned entry still governs every other ref being updated; where the same ref carries several lease entries, git consults the first, and so does this guard. A trailing `--no-force-with-lease` cancels every previous lease, and a push dry-run disarms the check. Also blocked: `reset --hard`, `clean` with a force flag (any dry-run flag disarms), worktree-wide `checkout`/`restore` pathspecs (`.`, `:/`, `:(top…)` — path-scoped forms and `restore --staged .` pass), and forced `checkout -f` / `switch --discard-changes`. Accepted unique-prefix abbreviations of the blocked long options match too. `branch -D` is deliberately not blocked (reflog-recoverable; sanctioned skill flows issue it). Per-repo/per-user allow-list via the `block_dangerous_git_allow` userConfig option (comma list, any subset of `push-force,push-lease-unsafe,reset-hard,clean-force,checkout-dot,restore-dot,checkout-force`). |
| **block-hook-bypass** | PreToolUse · Bash | **Blocks** (exit 2) | Bash file-write workarounds that circumvent the Write/Edit hook gates — `cat > file`, `echo … > file`, and `python3 -c` with file-write indicators. Executable-token detection ignores quoted prose/commit text that merely mentions the pattern. |
| **cli-flag-verify** | PostToolUse · Write \| Edit | **Advisory** (exit 0) | Hallucinated CLI flags — a `--flag` written as a command that does not exist in the binary's actual `--help` output. Surfaces via `additionalContext`, never blocks. |
| **workflow-resilience-check** | PreToolUse · Workflow | **Advisory** (exit 0) | Un-throttled Workflow fan-out — a script calling `parallel()` / `pipeline()` with no wave-cap throttle (`inWaves` / `inWavesPipeline`) and no retry wrapper (`agentRetry`), which risks a burst 529 under wide Opus fan-out. Surfaces a resilience checklist via `additionalContext`, never blocks. |
| **block-noncanonical-commit** | PreToolUse · Bash | **Blocks** (exit 2) | `git commit` that does not pipe its message via `-F -` / `--file -` — `-m` flattens newlines unpredictably across shells. Exempt: `--amend`, `-C`/`-c`/`--reuse-message`/`--reedit-message`, `--fixup`/`--squash`, `-F <path>`, and any commit taken while a merge/rebase/cherry-pick/revert is in progress. Resolves `bash -lc` wrappers and git aliases (inline `-c` and persisted config alike). |
| **workflow-resilience-check** | PreToolUse · Workflow | **Advisory** (exit 0) | Un-throttled Workflow fan-out — a script calling `parallel()` / `pipeline()` with no wave-cap throttle (`inWaves` / `inWavesPipeline`) and no retry wrapper (`agentRetry`), which risks a burst 529 under wide Opus fan-out. Surfaces a resilience checklist via `additionalContext`, never blocks. **Opt-in — default off since 0.20.0** (behavioral-class injector config-disabled per #2021; set `workflow_resilience_check_enabled=true` to enable). |
| **block-noncanonical-commit** | PreToolUse · Bash | **Blocks** (exit 2) | `git commit -m` whose message actually contains a newline — a multi-line `-m` flattens newlines unpredictably across shells; pipe it via `-F -` / `--file -` instead (narrowed in 0.20.0 per #2021: single-line `-m`, bare `git commit`, and repeated single-line `-m` paragraphs all pass). On the PowerShell tool a here-string `-m` value blocks too — its content is uninspectable and multi-line by construction of the form. Exempt: `--amend`, `-C`/`-c`/`--reuse-message`/`--reedit-message`, `--fixup`/`--squash`, `-F <path>`, and any commit taken while a merge/rebase/cherry-pick/revert is in progress. Resolves `bash -lc` wrappers and git aliases (inline `-c` and persisted config alike). |
| **block-convention-violation** | PreToolUse · Bash | **Blocks** (exit 2) | A commit subject or `gh pr create --title` that violates the team-tracked convention pattern declared in `.claude/source-control.md`. No tracked pattern means no enforcement. Same exemptions as `block-noncanonical-commit`. |
| **flag-commit-pr-skill-bypass** | PreToolUse · Bash | **Advisory** (exit 0) | Any `gh pr create`, bypassing this marketplace's own `/pull-request create` skill. Only fires when the consuming project's own `.claude/settings.json` enables the `source-control` plugin — silent otherwise. Surfaces via `additionalContext`, never blocks. |
| **flag-commit-pr-skill-bypass** | PreToolUse · Bash | **Advisory** (exit 0) | Any `gh pr create`, bypassing this marketplace's own `/pull-request create` skill. Only fires when the consuming project's own `.claude/settings.json` enables the `source-control` plugin — silent otherwise. Surfaces via `additionalContext`, never blocks. **Opt-in — default off since 0.20.0** (behavioral-class injector config-disabled per #2021; set `flag_commit_pr_skill_bypass_enabled=true` to enable). |
| **skill-reference-verify** | PostToolUse · Write \| Edit | **Advisory** (exit 0) | A `` `/plugin:skill` `` reference in markdown that does not resolve. Only fires inside a marketplace repo, and only for a plugin that repo's own manifests own — a reference to another marketplace is left alone. Resolves through manifest and frontmatter `name`, so a renamed directory still matches. Surfaces via `additionalContext`, never blocks. |
| **stale-path-verify** | PostToolUse · Write \| Edit | **Advisory** (exit 0) | A repo-relative path cited in a markdown inline code span that this repo's own history shows was **deleted** and that is gone from the working tree. The gate is provenance, not absence: the exact path must appear in `git log HEAD --no-renames --diff-filter=D --name-only`, so a path belonging to a consuming project's tree, an example, or a plan is never adjudicated. Names the surviving file when exactly one tracked path now carries that basename. Link destinations are out of scope. Surfaces via `additionalContext`, never blocks. |

Expand Down Expand Up @@ -107,9 +107,12 @@ out of scope until such a signal exists.

## Per-hook kill switches

Each guard is toggled by its own `userConfig` boolean (default **on**; set to
`false` for a clean no-op). This per-hook control is the bundle's core
contract — disable one guard without touching the others.
Each guard is toggled by its own `userConfig` boolean (default **on**, except
the two behavioral-class advisories `workflow-resilience-check` and
`flag-commit-pr-skill-bypass`, default **off** since 0.20.0 per #2021 — set to
`true` to opt in; set any switch to `false` for a clean no-op). This per-hook
control is the bundle's core contract — disable one guard without touching the
others.

| Guard | Option |
| ----- | ------ |
Expand Down
Loading
Loading