Skip to content

feat(source-control): add machine-readable commit-convention enforcement seam#925

Merged
kyle-sexton merged 1 commit into
mainfrom
feat/913-convention-enforcement-seam
Jul 22, 2026
Merged

feat(source-control): add machine-readable commit-convention enforcement seam#925
kyle-sexton merged 1 commit into
mainfrom
feat/913-convention-enforcement-seam

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Seams-first foundation (#912) for hard-gating commit subjects and PR titles. Gives a zero-dependency guardrails hook a machine-readable path to the effective enforcement pattern without changing the consumer-facing markdown surface (which just landed in #660).

What lands

Locked contract (binds #914/#919)

  1. Dialect = POSIX ERE; normalizes (?:->(, \d->[0-9], \w/\s; rejects un-expressible PCRE (lookaround/backref/\b) -> no enforcement + visible note.
  2. Unresolved -> no enforcement (never the bundled CC default -- CC is not lane-1-eligible).
  3. Policy-floor via team-only reads -- enforcement reads the tracked .claude/source-control.md only; gitignored/user-global overlays are drafting-only and can never weaken a gate.

Verification

  • bash lib/resolve-convention-pattern.test.sh -> 19/19 pass
  • shellcheck --rcfile=.shellcheckrc clean on all three scripts
  • scripts/sync-resolve-convention-pattern.sh --check passes

Related

Closes #913

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 12s —— View job


PR Review: feat(source-control): add machine-readable commit-convention enforcement seam

  • Gather context and read changed files
  • Review lib/resolve-convention-pattern.sh (core library)
  • Review lib/resolve-convention-pattern.test.sh (test suite)
  • Review scripts/sync-resolve-convention-pattern.sh (sync scaffold)
  • Review .github/workflows/ci.yml changes
  • Review documentation changes
  • Compile and post final review

Overall assessment

Well-structured seam. The core logic is correct, the architecture (team-tracked-only enforcement, POSIX-ERE normalization, unresolved → no-op) is sound, and the 19-case test suite covers the important contract variants. Four minor issues below — none blocking, but two deserve fixes before the dependents (#914, #919) source this.


Findings

1. Missing test: pr_title_pattern deferral when subject_pattern = "Conventional Commits" — test coverage gap

File: lib/resolve-convention-pattern.test.sh

The deferral test at line 105 uses a custom regex. The path where pr_title_pattern defers to a CC-keyword subject_pattern (triggering keyword expansion via raw_value) is not tested:

## subject_pattern
Conventional Commits

## pr_title_pattern
Same as `subject_pattern`.

In this case the deferral branch calls raw_value "subject_pattern" which should expand to $CC_ERE. The logic is correct, but the test gap means CC expansion and deferral are only tested independently. Since #914 and #919 will source this file, a test that exercises both together would be a meaningful addition. Fix this →


2. Raw GitHub URL for rendered README — DX issue

File: plugins/source-control/reference/config-resolution.md, line 94

[commit-convention enforcement seam](https://raw.githubusercontent.com/melodic-software/claude-code-plugins/main/docs/conventions/commit-convention/README.md)

raw.githubusercontent.com serves the Markdown source, not the rendered page. In GitHub's rendered view this works as a hyperlink, but following it presents raw text. A relative path renders correctly and doesn't hardcode the main branch:

[commit-convention enforcement seam](../../../docs/conventions/commit-convention/README.md)

(File is at plugins/source-control/reference/config-resolution.md; target is docs/conventions/commit-convention/README.md, so three levels up.) Fix this →


3. Unnecessary shellcheck disable=SC2016 — misleading comment

File: lib/resolve-convention-pattern.sh, line 51

# shellcheck disable=SC2016  # backticks are a literal part of the deferral marker, not a substitution
readonly PR_DEFERRAL='Same as `subject_pattern`.'

SC2016 warns about backticks inside double-quoted strings. PR_DEFERRAL uses single quotes, so ShellCheck would not flag it and the disable directive has no effect. The comment is harmless but misleads a reader into thinking suppression was needed. Fix this →


4. --check-bump manifest path inference is fragile if copy ever leaves hooks/

File: scripts/sync-resolve-convention-pattern.sh, line 63

manifest="${copy%%/hooks/*}/.claude-plugin/plugin.json"

This trims the longest suffix matching /hooks/*. For a copy at plugins/guardrails/hooks/resolve-convention-pattern.sh it correctly yields plugins/guardrails/.claude-plugin/plugin.json. But if a future consumer places a copy outside a hooks/ subdirectory the pattern won't match, and manifest becomes a path like plugins/foo/bar/resolve-convention-pattern.sh/.claude-plugin/plugin.jsonjq then silently exits non-zero on the missing file, base_version is empty, and the if [[ -z "$base_version" ]] guard at line 61 skips the stale check rather than erroring. This parallels the approach in sync-parse-concern-value.sh so the risk is accepted; worth a comment noting the assumption. Fix this →


5. newrepo helper comment promises a note that doesn't exist — minor

File: lib/resolve-convention-pattern.test.sh, line 36

# Extra positional args after the first write additional overlay files:
#   newrepo "<team body>" [local-body] [does NOT create user-global — see note]

There is no "note" in the file explaining why user-global is absent. The reason (enforcement reads team-tracked only, so testing a user-global layer is purposeless) is clear from context, but the dangling — see note will confuse a future editor. Fix this →


What's solid

  • Policy-floor design is correct: the team-file-only read, unresolved→no-enforcement, never-CC-fallback guarantees are all enforced at the right layer and well-tested.
  • ERE normalization is match-equivalent: (?:( changes capture group numbering but not matches; \d/\w/\s expansions are faithful POSIX equivalents. The pre-normalization rejection of lookaround/backreference/\b prevents silent mis-enforcement.
  • is_non_ere ordering: rejection runs before normalization, preventing a case where a PCRE construct could be mangled into something ERE-valid but semantically wrong.
  • Empty-array idiom: ${copies[@]+"${copies[@]}"} in the sync script handles the intentionally empty copies array correctly in all bash versions (no unbound variable from set -u).
  • CI job structure: resolve-convention-pattern-sync mirrors the existing parse-concern-value-sync/hook-utils-sync jobs exactly, is wired into ci-status.needs, and the fetch-depth: 0 for bump verification follows the same pattern.
  • Test isolation: mktemp -d per test plus a trap cleanup — no inter-test interference possible.

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 0s —— View job


Security Review — PR #925

  • Read PR diff and changed files
  • Review shell scripts for injection surfaces
  • Review CI workflow changes
  • Review documentation changes
  • Post findings

Summary

No critical or important security findings. The PR follows correct security patterns throughout: SHA-pinned actions, persist-credentials: false, pull_request (not pull_request_target), proper env-var indirection for GitHub context values, and user-controlled content flows only through awk/sed data paths — never through eval or dynamic command construction.


Findings

SUGGESTION — Incomplete PCRE detection in is_non_ere (confidence: medium)

File: lib/resolve-convention-pattern.sh

is_non_ere rejects lookaround ((?=, (?!, (?<), named groups ((?P<), backreferences (\[0-9]), and \b/\B. It does not reject several other PCRE-only constructs that are also invalid POSIX ERE:

  • (?i:…) — PCRE inline flag / case-insensitive non-capturing group
  • (?>…) — PCRE atomic group
  • (?#…) — PCRE comment
  • (?i), (?m), (?s) — PCRE mode modifiers

Scenario: A consumer sets subject_pattern: (?i:feat|fix): .+ expecting case-insensitive enforcement. is_non_ere returns false (not detected), normalize_ere does not transform (?i: (it only matches (?:), and the resolver emits (?i:feat|fix): .+ as if it were valid ERE. When a consuming hook passes this to [[ commit_msg =~ ]] or grep -E, bash returns exit status 2 (invalid regex) or grep errors — the hook may then silently pass all commits (fail-open), bypassing intended enforcement without any diagnostic output to the consumer.

This is weaker than a true bypass (it can only produce fail-open, never false enforcement), but a consumer who opts into enforcement sees it silently no-op. The resolver's stderr diagnostic (which correctly signals non-ERE patterns) is never emitted in this case.

Suggested fix: Extend is_non_ere with the missing PCRE-only patterns:

[[ "$p" == *'(?i'* || "$p" == *'(?m'* || "$p" == *'(?s'* || "$p" == *'(?x'* ]] && return 0  # inline flags
[[ "$p" == *'(?>'* || "$p" == *'(?#'* ]] && return 0  # atomic group, comment

Fix this →


Confirmed clean

Pattern Verdict
pull_request (not pull_request_target) ✓ safe trigger
${{ github.base_ref }} via env: indirection, not direct run: interpolation ✓ no script injection
All action refs SHA-pinned
persist-credentials: false on checkout
repo_root + key never eval'd or used as a command
User-controlled file content flows through awk/sed data paths only
normalize_ere pipes to sed stdin (not as a sed script)
copies array hardcoded in sync script (not user-controllable)
origin/ prefix on $BASE_REF in git commands prevents option injection
set -uo pipefail throughout

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

ℹ️ 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/resolve-convention-pattern.sh Outdated
Comment thread lib/resolve-convention-pattern.sh Outdated
@kyle-sexton
kyle-sexton force-pushed the feat/913-convention-enforcement-seam branch from e271002 to 934e787 Compare July 22, 2026 01:03
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 58s —— View job


PR Review: feat(source-control): add machine-readable commit-convention enforcement seam

  • Read diff and changed files
  • Review lib/resolve-convention-pattern.sh (core library)
  • Review lib/resolve-convention-pattern.test.sh (test suite)
  • Review scripts/sync-resolve-convention-pattern.sh (sync scaffold)
  • Review CI and documentation changes
  • Check against CLAUDE.md guidelines
  • Compile final review

Overall

The architecture is sound and the implementation is careful where it matters most: team-tracked-layer-only enforcement, unresolved → no-op, the is_non_ere-before-normalize ordering, and the ${copies[@]+"${copies[@]}"} empty-array guard. The policy-floor contract is correctly enforced at the right layer and the CI job mirrors the established pattern exactly.

Two findings below deserve fixes before #914/#919 vendor this. The remaining three are minor polish.


Findings

1. \d/\w/\s inside a bracket expression produces a semantically wrong ERE — correctness bug

File: lib/resolve-convention-pattern.sh:114–117

normalize_ere substitutes \w[[:alnum:]_] globally, including inside existing bracket expressions. A consumer pattern like [\w-]+ becomes [[[:alnum:]_]-]+. In POSIX ERE, the outer [ opens the bracket expression, the inner [ that leads [:alnum:] is treated as a literal [, so the normalized class additionally matches the character [ — incorrect relative to the PCRE original.

The same applies to \d inside brackets: [\d-][[0-9]-] matches [, 0–9, - instead of just 0–9 and -. And to \s.

The practical blast radius for commit-subject patterns is low (most consumers won't write [\w-]), but the seam contract says the normalization is "match-equivalent" and this case isn't. The fix is to only substitute shorthand escapes that appear outside bracket expressions, which requires bracket-context tracking. A simpler, correct approach for the common case is to expand \w to [[:alnum:]_] but when the pattern already wraps it in […], the outer bracket should not double up. One workable path:

# Normalize standalone \d, \w, \s only when NOT already inside a bracket expression.
# sed cannot track bracket context; use awk or a two-pass approach.

If the full fix is deferred to a follow-up, the contract doc and resolver header should note this limitation explicitly so consumers aren't surprised.

Fix this →


2. Raw GitHub URL in config-resolution.md — DX issue

File: plugins/source-control/reference/config-resolution.md:94

[commit-convention enforcement seam](https://raw.githubusercontent.com/melodic-software/claude-code-plugins/main/docs/conventions/commit-convention/README.md)

raw.githubusercontent.com serves Markdown source, not the rendered view. Following the link shows raw text. It also hardcodes main. A relative path renders correctly in GitHub and survives branch renames:

[commit-convention enforcement seam](../../../docs/conventions/commit-convention/README.md)

(The file is at plugins/source-control/reference/config-resolution.md; three ../ levels up reaches docs/conventions/commit-convention/README.md.)

Fix this →


3. is_non_ere misses PCRE inline flags and atomic groups — incomplete detection

File: lib/resolve-convention-pattern.sh:103–108

Several PCRE-only constructs are not caught and pass through to normalize_ere, which cannot transform them:

Construct Example Risk
Inline flags (?i:feat|fix) Not detected; (?i:(i: after (?:( normalization — silently wrong
Mode modifiers (?i)^feat: .+ Not detected; emitted as-is, invalid ERE
Atomic group (?>feat|fix) Not detected; emitted as-is, invalid ERE
PCRE comment (?#type)^feat Not detected; emitted as-is

The (?i: case is the most dangerous because normalize_ere transforms it: (?i:feat|fix)(i:feat|fix). This is valid ERE syntax (a non-special group with a literal i: prefix), so it compiles without error but enforces the wrong pattern — i:feat would be required rather than case-insensitive feat. This is worse than fail-open; it's mis-enforcement.

Suggested additions to is_non_ere:

# PCRE inline flags — (?i, (?m, (?s, (?x — no ERE equivalent
[[ "$p" == *'(?i'* || "$p" == *'(?m'* || "$p" == *'(?s'* || "$p" == *'(?x'* ]] && return 0
# Atomic group, PCRE comment
[[ "$p" == *'(?>'* || "$p" == *'(?#'* ]] && return 0

Fix this →


4. Unnecessary shellcheck disable=SC2016 — misleading

File: lib/resolve-convention-pattern.sh:51

SC2016 warns about backticks inside double-quoted strings. PR_DEFERRAL uses single quotes; SC2016 would never fire here. The disable directive has no effect and misleads readers.

Fix this →


5. Dangling — see note in newrepo helper comment — minor

File: lib/resolve-convention-pattern.test.sh:36–37

#   newrepo "<team body>" [local-body] [does NOT create user-global — see note]

There is no "note" in the file. The reason is clear from context (enforcement is team-tracked-only so testing a user-global overlay is purposeless), but the dangling forward-reference will confuse a future editor. Inline the explanation:

#   newrepo "<team body>" [local-body]  (no user-global: enforcement reads team-tracked layer only)

Fix this →


PR description accuracy note

The PR claims "19/19" tests pass. grep -c 'assert_eq\|assert_exit' returns 21 because two of those matches are the function definitions themselves — the actual test count is 19 as stated. No issue, just confirming the count is correct.


What's solid

  • Policy-floor is correctly enforced at the layer boundary: h2_value reads TEAM_FILE only; local and user-global overlays are never consulted by the resolver. The test at line 100–102 explicitly verifies this invariant.
  • is_non_ere runs before normalize_ere: prevents a PCRE construct from being mangled into accidentally-valid ERE that enforces the wrong thing. The ordering is load-bearing.
  • CC keyword → one canonical ERE: raw_value owns the expansion in one place; no per-hook re-definition possible.
  • Unresolved → exit 1, stdout empty: the [[ -n "$value" ]] || exit 1 guard at line 144 is clean and covers both absent-file and absent-key paths.
  • CI job is correctly wired: SHA-pinned checkout, persist-credentials: false, BASE_REF via env var (no script-injection), fetch-depth: 0 for bump checking, correct placement in ci-status.needs.
  • Empty-array idiom: ${copies[@]+"${copies[@]}"} throughout — correct under set -u when the array is intentionally empty.
  • sync-resolve-convention-pattern.sh structure: The intentionally-empty copies list with a commented placeholder for feat(guardrails): CC-layer content gate — commit subject + PR title #914 is the right scaffold: no premature copies to drift before the consumers exist.

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

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


Security Review — PR #925

  • Read PR diff and changed files
  • Review shell scripts for injection surfaces
  • Review CI workflow changes
  • Review documentation changes
  • Post findings

Summary

No CRITICAL or IMPORTANT findings. The architecture is sound: ${{ github.base_ref }} is passed through env: indirection (never interpolated directly in run:), all action refs are SHA-pinned, persist-credentials: false is set, the trigger is pull_request (not pull_request_target), user-controlled file content flows only through awk/sed data paths, and copies is hardcoded. Three SUGGESTION-level findings follow, all of which affect enforcement fail-open behavior in downstream consumers (#914, #919) rather than this repo itself.


Findings

SUGGESTION — Incomplete PCRE detection in is_non_ere still unaddressed | confidence: medium

File: lib/resolve-convention-pattern.sh L103–L109

This finding was raised in the previous security review (commit e271002) and remains unchanged. is_non_ere does not detect PCRE-only constructs that are also invalid POSIX ERE:

  • (?i:…) / (?i), (?m), (?s), (?x) — inline flags and flag groups
  • (?>…) — atomic group
  • (?#…) — PCRE comment

Scenario: Consumer sets subject_pattern: (?i:feat|fix): .+ expecting case-insensitive enforcement. is_non_ere returns false; normalize_ere does not transform (?i: (it only handles (?:); the resolver exits 0 and emits (?i:feat|fix): .+. A consuming hook passes this to [[ =~ ]] or grep -E; both return exit status 2 (invalid regex). If the hook treats non-zero as a pass condition, every commit passes silently without the expected diagnostic.

Fix this →


SUGGESTION — normalize_ere substitutions are context-unaware inside bracket expressions | confidence: medium

File: lib/resolve-convention-pattern.sh L113–L118

p="$(printf '%s' "$p" | sed -e 's/\\d/[0-9]/g' -e 's/\\w/[[:alnum:]_]/g' -e 's/\\s/[[:space:]]/g')"

The substitutions are global and do not track whether the match site is already inside a bracket expression […]. For a common team pattern like ^[\w-]+$:

  • \w[[:alnum:]_], yielding ^[[[:alnum:]_]-]+$

POSIX ERE treats [[…]] as a bracket expression containing a POSIX character class; behavior is implementation-defined when the inner class collides with operator characters. GNU grep -E accepts this but parses it differently from the intended [\w-]; Bash [[ =~ ]] on macOS (BSD regex) may reject it with exit status 2. The failure mode is the same as finding #1: depending on the hook's error handling the gate either blocks everything or passes everything, silently.

Fix this →


SUGGESTION — Normalized output is emitted without ERE compile-time validation | confidence: medium

File: lib/resolve-convention-pattern.sh L151

printf '%s\n' "$(normalize_ere "$value")"
exit 0

A pattern that passes is_non_ere and reaches normalize_ere is emitted with exit 0 ("enforce it") without being tested against an actual ERE compiler. Any construct with no ERE equivalent that escaped the string-match detection in is_non_ere (e.g., unbalanced brackets like ^[unclosed, an unrecognized escape like \p{Lu}) produces an output that the resolver declares enforceable but that grep -E / [[ =~ ]] rejects at runtime.

The contract says: exit 0 → the caller must enforce. A caller that writes:

if [[ $( resolve-convention-pattern.sh . subject_pattern ) ]]; then ... fi

and doesn't separately test whether grep -E "$pattern" compiles will fail open or closed unpredictably.

Suggested mitigation: before the final printf, probe the emitted pattern with echo | grep -qE "$p" 2>/dev/null || { diag ... >&2; exit 1; }. This would be a deterministic compile check that completes in zero overhead.

Fix this →


Confirmed clean

Pattern Verdict
pull_request trigger (not pull_request_target)
${{ github.base_ref }} via env: indirection, not direct run: interpolation
"origin/$BASE_REF" in git diff/git show — prefixed, no option injection
All action refs SHA-pinned (actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0)
persist-credentials: false on checkout
copies array hardcoded in sync script (not user-controllable)
key validated to subject_pattern|pr_title_pattern before awk -v key="$k"
normalize_ere pipes to sed stdin (not as a sed script)
h2_value uses [[ -f "$file" ]] || return 0 before awk
set -uo pipefail throughout; set -euo pipefail in sync script
No secrets, tokens, or credentials referenced

@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: 934e787807

ℹ️ 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/resolve-convention-pattern.sh Outdated
@kyle-sexton
kyle-sexton force-pushed the feat/913-convention-enforcement-seam branch from 934e787 to 337c83f Compare July 22, 2026 02:23
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

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


PR Review: feat(source-control): add machine-readable commit-convention enforcement seam

  • Identify what changed since prior reviews (PR sync; rebase on a67ccba, no code changes)
  • Review lib/resolve-convention-pattern.sh against current HEAD
  • Review lib/resolve-convention-pattern.test.sh for coverage gaps
  • Cross-check all prior findings against current state
  • Compile and post final review

Context

This review is triggered by a PR synchronize event (the branch was rebased onto main at a67ccba). The content of the nine changed files is unchanged from the prior review at 934e787807. None of the findings from the previous two code reviews and two security reviews have been addressed in the current HEAD.

Summary: two blocking issues (correctness), one important issue (safety contract), and four minor issues.


Blocking — fix before #914 / #919 vendor this

1. is_non_ere does not detect PCRE inline flags → exit 0 with invalid ERE

File: lib/resolve-convention-pattern.sh:103–109

The function only checks for lookaround ((?=, (?!, (?<, (?P<), backreferences, and \b/\B. It misses:

Construct Example Outcome
Inline flag group (?i:feat|fix) Not detected; normalize_ere sees (?i:(?: so leaves it intact; resolver exits 0 with invalid ERE
Mode modifier (?i)^feat: .+ Same — not detected, emitted as-is
Atomic group (?>feat|fix) Same
PCRE comment (?#type)^feat Same

The result is the resolver exits 0 (signalling "enforce this"), but the emitted pattern is invalid ERE. A consuming hook that does [[ $commit_msg =~ $pattern ]] or grep -E "$pattern" gets exit status 2 — an unhandled error path that may silently fail-open on all commits, bypassing the gate with no diagnostic.

Fix: Extend is_non_ere before the final return 1:

# PCRE inline flags and atomic group/comment — no ERE equivalent
[[ "$p" == *'(?i'* || "$p" == *'(?m'* || "$p" == *'(?s'* || "$p" == *'(?x'* ]] && return 0
[[ "$p" == *'(?>'* || "$p" == *'(?#'* ]] && return 0

Fix this →


2. normalize_ere produces wrong ERE for shorthand escapes inside bracket expressions

File: lib/resolve-convention-pattern.sh:116

p="$(printf '%s' "$p" | sed -e 's/\\d/[0-9]/g' -e 's/\\w/[[:alnum:]_]/g' -e 's/\\s/[[:space:]]/g')"

The substitutions are global and have no bracket-expression awareness. A common team pattern like ^[\w-]+$ becomes ^[[[:alnum:]_]-]+$. In POSIX ERE, the outer [ opens the bracket expression and the inner [[:alnum:]_] is parsed as a POSIX class inside it — but the leading [ of [[:alnum:]_] is treated as a literal [ character added to the class, making the pattern match [, a-z, A-Z, 0-9, _, and - instead of just the intended word characters and hyphen. This is a silent semantic difference: the resolver exits 0 with a structurally valid but semantically wrong ERE.

The same issue applies to \d inside brackets ([\d-][[0-9]-]) and \s.

The fix requires either bracket-context tracking (complex) or a simpler approach: if \w, \d, or \s appears within a bracket expression […], reject it as non-enforceable and emit a diagnostic. For the common case where these escapes appear outside brackets, the current normalization is correct.

Fix this →


Important

3. No ERE compile-time validation before emitting the pattern

File: lib/resolve-convention-pattern.sh:151

The resolver's documented contract is: exit 0 → the pattern is a valid ERE ready for grep -E / [[ =~ ]]. But a consumer pattern that passes is_non_ere (e.g., unbalanced bracket ^[unclosed, unrecognized escape \p{Lu}, or a pattern stranded by the bracket-context issue in finding #2) will be emitted with exit 0 without any compile probe. Downstream hooks will then receive exit status 2 from the regex engine — behavior that isn't covered by the current test suite.

A zero-overhead guard before the final printf:

printf '' | grep -qE "$normalized" 2>/dev/null || {
  printf 'resolve-convention-pattern: %s normalized to an invalid ERE; enforcement disabled.\n' "$key" >&2
  exit 1
}

This makes "exit 0 = valid ERE" a guarantee rather than a best-effort claim.

Fix this →


Minor

4. Raw GitHub URL in config-resolution.md — serves Markdown source, not rendered page

File: plugins/source-control/reference/config-resolution.md:94

[commit-convention enforcement seam](https://raw.githubusercontent.com/melodic-software/claude-code-plugins/main/docs/conventions/commit-convention/README.md)

raw.githubusercontent.com serves Markdown source text, not the rendered view. It also hardcodes main. A relative path works correctly in GitHub's rendered view and survives branch renames:

[commit-convention enforcement seam](../../../docs/conventions/commit-convention/README.md)

(The file is at plugins/source-control/reference/config-resolution.md; three ../ levels up reaches docs/conventions/commit-convention/README.md.)

Fix this →


5. Unnecessary shellcheck disable=SC2016 — misleads future readers

File: lib/resolve-convention-pattern.sh:51

SC2016 warns about backticks inside double-quoted strings. PR_DEFERRAL uses single quotes ('...'), so ShellCheck never triggers SC2016 here. The disable directive has no effect and misleads a reader into thinking suppression was required.

Fix this →


6. Dangling — see note in test helper comment

File: lib/resolve-convention-pattern.test.sh:36

#   newrepo "<team body>" [local-body] [does NOT create user-global — see note]

There is no "note" in this file. Inline the reason:

#   newrepo "<team body>" [local-body]  (no user-global: enforcement reads team-tracked layer only)

Fix this →


7. Missing test: CC keyword + pr_title_pattern deferral exercised together

File: lib/resolve-convention-pattern.test.sh:104–106

The existing deferral test uses a custom ERE for subject_pattern. The path where pr_title_pattern defers to a Conventional Commits keyword subject_pattern (exercising CC expansion through the deferral branch) is not covered. Since this is the combination most likely to appear in real consumer configs, a dedicated case would close the gap before #914/#919 vendor the resolver.

Fix this →


What's solid

  • Policy-floor is correctly enforced at the layer boundary. h2_value reads TEAM_FILE only; the test at line 100–102 explicitly verifies local overlays can never enforce.
  • is_non_ere runs before normalize_ere. The ordering prevents lookaround / backreference / \b from being mangled into accidentally-valid ERE.
  • CC keyword → one canonical ERE in one place. raw_value owns the expansion; no per-hook re-definition possible.
  • Unresolved → exit 1, empty stdout. The [[ -n "$value" ]] || exit 1 guard covers both absent-file and absent-key.
  • ${copies[@]+"${copies[@]}"} empty-array idiom throughout the sync script — correct under set -u.
  • CI job structure mirrors parse-concern-value-sync exactly: SHA-pinned checkout, persist-credentials: false, BASE_REF via env var, fetch-depth: 0, correctly wired into ci-status.needs.

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 18s —— View job


Security Review — PR #925

  • Gather context and read PR diff
  • Review lib/resolve-convention-pattern.sh (core library)
  • Review lib/resolve-convention-pattern.test.sh (test suite)
  • Review scripts/sync-resolve-convention-pattern.sh (sync scaffold)
  • Review .github/workflows/ci.yml changes
  • Check for new issues vs. prior reviews
  • Post final security findings

Summary

No CRITICAL findings. Two IMPORTANT issues and two SUGGESTION-level issues. Both IMPORTANT findings affect enforcement correctness — one is new to this review, one was raised in prior reviews and remains unaddressed. Both can produce silent mis-enforcement (an incorrect pattern emitted with exit 0 and no diagnostic), which is more dangerous than the fail-open case.


Findings

IMPORTANT — Negated PCRE shorthand classes (\D, \W, \S) silently produce wrong enforcement | confidence: high

File: lib/resolve-convention-pattern.sh L103–L118

is_non_ere does not detect \D, \W, or \S (the uppercase/negation counterparts of the three normalized shorthands), and normalize_ere only substitutes the lowercase forms:

sed -e 's/\\d/[0-9]/g' -e 's/\\w/[[:alnum:]_]/g' -e 's/\\s/[[:space:]]/g'

Scenario: A team sets:

## subject_pattern
^\D+$

(PCRE intent: reject subjects containing any digits.) is_non_ere returns 1 (not detected), normalize_ere leaves \D unchanged, and the resolver emits ^\D+$ with exit 0, declaring it enforceable. Both grep -E '^\D+$' and bash [[ =~ '^\D+$' ]] treat the unrecognized \D as the literal character D, so the enforced gate silently accepts any commit subject that isn't a string of Ds — the opposite of the intended rule, with no diagnostic to the consumer. Same applies to \W (enforces literal W) and \S (enforces literal S).

The fix: add detection in is_non_ere for \D, \W, \S:

[[ "$p" == *'\D'* || "$p" == *'\W'* || "$p" == *'\S'* ]] && return 0

Or normalize them to their POSIX negation equivalents ([^0-9], [^[:alnum:]_], [^[:space:]]) in normalize_ere, following the same approach used for the lowercase forms.

Fix this →


IMPORTANT — Undetected PCRE-only (? constructs emitted as valid ERE → fail-open without diagnostic | confidence: high

File: lib/resolve-convention-pattern.sh L103–L108

is_non_ere detects lookaround ((?=, (?!, (?<) and named groups ((?P<), but not:

Construct Example ERE behavior
Inline flag group (?i:feat|fix) Not caught; emitted as-is; invalid ERE → consuming hook errors
Mode modifier (?i)^feat: .+ Not caught; emitted as-is; invalid ERE
Atomic group (?>feat|fix) Not caught; emitted as-is; invalid ERE
PCRE comment (?#type)^feat Not caught; emitted as-is; invalid ERE

Scenario (worst case): Team sets subject_pattern: (?i:feat|fix): .+ expecting case-insensitive enforcement. is_non_ere returns 1 (not a non-ERE construct). normalize_ere does not transform (?i: — the (?:( substitution at line 115 looks for exactly (?: (three chars (, ?, :) and (?i: doesn't match. The resolver emits (?i:feat|fix): .+ with exit 0. A consuming hook hands this to grep -E or [[ =~ ]]; both reject it with exit status 2 (invalid ERE). If the hook treats any non-zero as pass, every commit clears the gate silently.

Add to is_non_ere:

# PCRE inline flags, atomic group, comment — no POSIX ERE equivalent
[[ "$p" == *'(?i'* || "$p" == *'(?m'* || "$p" == *'(?s'* || "$p" == *'(?x'* ]] && return 0
[[ "$p" == *'(?>'* || "$p" == *'(?#'* ]] && return 0

(This finding was raised in the two prior reviews on commits e271002 and 934e787 and is still unaddressed in HEAD 337c83f.)

Fix this →


SUGGESTION — normalize_ere substitutions are context-blind inside bracket expressions | confidence: medium

File: lib/resolve-convention-pattern.sh L116

The \w/\d/\s → POSIX class substitutions run globally, including inside bracket expressions. [\w-]+[[[:alnum:]_]-]+. The outer [ and the inner [ that begins [:alnum:] form a nested bracket expression; GNU grep ERE accepts this but parses it differently from the intended word-character class, and BSD regex (bash on macOS) may reject it with exit status 2, failing-open or failing-closed depending on the consuming hook. The failure is silent — exit 0 was already returned.

(Also raised in both prior reviews, still unaddressed.)

Fix this →


SUGGESTION — No compile-time ERE validation before emitting with exit 0 | confidence: medium

File: lib/resolve-convention-pattern.sh L151

printf '%s\n' "$(normalize_ere "$value")"
exit 0

Exit 0 means "enforce this." Any construct that evades the string-match detection in is_non_ere (a new PCRE feature, an unbalanced bracket, \p{Lu}) is emitted with exit 0 and no validation that the result is actually compilable by grep -E or bash's [[ =~ ]]. A one-line probe before emit would make exit 0 a guarantee rather than a claim:

normalized="$(normalize_ere "$value")"
if ! printf '' | grep -qE "$normalized" 2>/dev/null; then
  printf 'resolve-convention-pattern: %s resolved to an invalid ERE; enforcement disabled.\n' "$key" >&2
  exit 1
fi
printf '%s\n' "$normalized"

(Also raised in both prior reviews, still unaddressed.)

Fix this →


Confirmed clean

Pattern Verdict
pull_request trigger (not pull_request_target)
${{ github.base_ref }} via env: indirection, not direct run: interpolation
"origin/$BASE_REF" in git diff / git show — prefixed, no option injection
actions/checkout@9c091bb… SHA-pinned
persist-credentials: false
copies array hardcoded in sync script (not user-controllable)
key validated to subject_pattern|pr_title_pattern before awk -v key=
normalize_ere pipes to sed stdin (not as a sed script)
h2_value checks [[ -f "$file" ]] before awk
set -uo pipefail throughout; set -euo pipefail in sync script
No secrets, tokens, or credentials referenced
New CI job mirrors established SHA-pinned pattern

@kyle-sexton
kyle-sexton force-pushed the feat/913-convention-enforcement-seam branch from 337c83f to 75a97f5 Compare July 22, 2026 02:24
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

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


PR Review: feat(source-control): add machine-readable commit-convention enforcement seam

  • Gather context and check what changed since last review
  • Review lib/resolve-convention-pattern.sh for current state
  • Review lib/resolve-convention-pattern.test.sh for current state
  • Check which prior review findings were addressed
  • Compile and post final review

This review covers the current HEAD (75a97f5). Three prior reviews (on commits e271002, 934e787) and two Codex reviews all identified the same core issues — none have been addressed in the current code. The findings below are ordered by severity.


Findings

1. is_non_ere misses PCRE inline flags → undetected pattern emitted as valid ERE — correctness bug

File: lib/resolve-convention-pattern.sh:103–108

is_non_ere does not detect (?i:…), (?i), (?m), (?s), (?x), (?>…), or (?#…). These are PCRE-only constructs with no POSIX ERE equivalent.

Concrete failure path for subject_pattern: (?i:feat|fix): .+:

  1. is_non_ere returns false (none of the existing string checks match)
  2. normalize_ere leaves it unchanged — p="${p//'(?:'/'('}" does not match (?i: (the substring is (?i, not (?:)
  3. Resolver exits 0 and emits (?i:feat|fix): .+
  4. A consuming hook hands that to grep -E or [[ =~ ]] → regex compile error, exit status 2
  5. Depending on hook error handling: fail-open (every commit passes silently) or hook abort

The exit-0 guarantee is the entire contract this seam gives to #914 and #919. A false positive here means the resolver tells them "enforce this" for a pattern neither engine can compile.

Fix — extend is_non_ere before the final return 1:

# PCRE inline flags / flag groups — (?i, (?m, (?s, (?x — no ERE equivalent
[[ "$p" == *'(?i'* || "$p" == *'(?m'* || "$p" == *'(?s'* || "$p" == *'(?x'* ]] && return 0
# Atomic group, PCRE comment
[[ "$p" == *'(?>'* || "$p" == *'(?#'* ]] && return 0

Note: a corrollary test for each added construct should accompany the fix.

Fix this →


2. Negated PCRE shorthands \D/\W/\S pass through as-is — correctness bug

File: lib/resolve-convention-pattern.sh:113–117

normalize_ere maps \d[0-9], \w[[:alnum:]_], \s[[:space:]], but \D, \W, \S (the negated PCRE shorthands) are not handled. is_non_ere also has no check for them.

For subject_pattern: ^\D+$ (non-digit-only subjects):

  • is_non_ere returns false
  • normalize_ere leaves \D unchanged
  • Resolver exits 0 and emits ^\D+$
  • In POSIX ERE \D is undefined; GNU grep -E treats it as literal D; bash [[ =~ ]] may do the same or differ between Linux and macOS
  • The gate silently enforces "subject must be exactly D" instead of "subject must contain no digits"

Fix options: Either add \D/\W/\S to is_non_ere (treating them as non-enforceable, matching the \b/\B precedent), or add ERE-equivalent substitutions (\D[^0-9], \W[^[:alnum:]_], \S[^[:space:]]) with corresponding tests.

Fix this →


3. No ERE compile-time validation before emitting — contract gap

File: lib/resolve-convention-pattern.sh:151

printf '%s\n' "$(normalize_ere "$value")"
exit 0

The resolver exits 0 for any pattern that passes the string-match checks in is_non_ere, even if normalize_ere produces an output that isn't valid ERE (e.g., unbalanced brackets from ^[unclosed, an unsupported escape like \p{Lu}, or the inline-flag case from finding #1 before that fix lands). Exit 0 is the contract signal "enforce this"; callers that trust exit status without an independent compile check are left with a ticking bomb.

Fix: Probe the normalized result before emitting:

normalized="$(normalize_ere "$value")"
if ! printf '' | grep -qE "$normalized" 2>/dev/null; then
  printf 'resolve-convention-pattern: %s normalized to an invalid ERE; enforcement disabled.\n' "$key" >&2
  exit 1
fi
printf '%s\n' "$normalized"
exit 0

This is zero-overhead (empty input, fast ERE compile check) and makes the exit-0 guarantee unconditional.

Fix this →


4. \d/\w/\s normalization context-blind inside bracket expressions — correctness concern

File: lib/resolve-convention-pattern.sh:116

p="$(printf '%s' "$p" | sed -e 's/\\d/[0-9]/g' -e 's/\\w/[[:alnum:]_]/g' -e 's/\\s/[[:space:]]/g')"

For a pattern like ^[\w-]+$, the global substitution produces ^[[[:alnum:]_]-]+$. Whether this is valid on a given ERE engine depends on implementation: POSIX-compliant engines (where [[:alnum:]] inside a bracket expression starts a character class) may parse it correctly, but behavior is not guaranteed across all platforms the consumers run on. The compile-time probe from finding #3 would catch cases where this is an invalid ERE, preventing a silent mis-enforcement, but would not detect the case where the output compiles but matches differently than intended.

The contract header claims normalization is "match-equivalent" — that claim doesn't hold for bracket-expression contexts on all platforms. At minimum, document this known limitation in the header comment so consumers know not to use [\w-]-style patterns.

Fix this →


5. Raw GitHub URL in config-resolution.md — DX

File: plugins/source-control/reference/config-resolution.md:94

[commit-convention enforcement seam](https://raw.githubusercontent.com/melodic-software/claude-code-plugins/main/docs/conventions/commit-convention/README.md)

raw.githubusercontent.com serves Markdown source (raw text), not the rendered view. Also hardcodes main. Relative path is correct and branch-agnostic — the file is 3 levels up from plugins/source-control/reference/:

[commit-convention enforcement seam](../../../docs/conventions/commit-convention/README.md)

Fix this →


6. Unnecessary shellcheck disable=SC2016 — misleading

File: lib/resolve-convention-pattern.sh:51

SC2016 warns about backtick substitution inside double-quoted strings. PR_DEFERRAL is assigned with single quotes — SC2016 cannot fire. The disable directive has no effect and will mislead a reader into thinking this suppression was load-bearing.

Fix this →


7. Dangling — see note in newrepo helper — documentation

File: lib/resolve-convention-pattern.test.sh:36

#   newrepo "<team body>" [local-body] [does NOT create user-global — see note]

No "note" exists in the file. Inline the reason:

#   newrepo "<team body>" [local-body]  (no user-global: enforcement reads team-tracked layer only)

Fix this →


8. Missing test: pr_title_pattern deferral when subject_pattern is the CC keyword — test gap

File: lib/resolve-convention-pattern.test.sh:104–106

The existing deferral test uses a custom regex. The path where pr_title_pattern: Same as subject_pattern. defers to a CC-keyword subject_pattern (triggering keyword expansion) is untested. Since #914 and #919 will source this file, the deferral + CC keyword expansion combined is the case most likely to be relied on in practice.

# --- pr_title_pattern deferral + CC keyword expansion ---
r="$(newrepo $'## subject_pattern\nConventional Commits\n\n## pr_title_pattern\nSame as `subject_pattern`.')"
assert_eq "pr deferral + CC keyword -> CC_ERE" "$CC_ERE" "$(run "$r" pr_title_pattern)"

Fix this →


Summary

Findings 1–3 should be fixed before #914 and #919 vendor this resolver — they all produce exit-0 for non-ERE-expressible patterns, which means consumers will receive false "enforce this" signals and either fail-open or abort. The compile-time probe (#3) is a cheap defense-in-depth that catches any detection gap including the ones in #1 and #2. Findings 4–8 are polish that doesn't block the seam contract but should be addressed before the consuming PRs merge.

The architecture (team-tracked-only enforcement, unresolved → no-op, never-CC-fallback, is_non_ere-before-normalize ordering) is correct and the test isolation is solid.

@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: 75a97f535b

ℹ️ 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/resolve-convention-pattern.sh
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 8s —— View job


Security Review — PR #925

  • Gather context and read prior review findings
  • Read current HEAD files (lib/resolve-convention-pattern.sh, test, sync script, CI)
  • Determine which prior findings are fixed vs. still present
  • Check for any new vulnerabilities at HEAD 75a97f5
  • Post final findings

Summary

No CRITICAL findings. Two IMPORTANT issues and three SUGGESTION-level issues. All five affect enforcement correctness: a pattern that should be rejected is instead emitted with exit 0, causing the consuming hook to either silently mis-enforce the wrong rule or fail-open with no diagnostic. Neither IMPORTANT finding was introduced in this commit — both were carried forward from prior pushes to this branch and all three prior Claude security reviews flagged them. They remain unaddressed at HEAD 75a97f535b.


Findings

IMPORTANT — Negated PCRE shorthand classes (\D, \W, \S) are silently emitted as mis-enforcing ERE | confidence: high

File: lib/resolve-convention-pattern.sh L103–L118

is_non_ere does not detect \D, \W, or \S. normalize_ere only substitutes the lowercase counterparts:

sed -e 's/\\d/[0-9]/g' -e 's/\\w/[[:alnum:]_]/g' -e 's/\\s/[[:space:]]/g'

Scenario: A team sets subject_pattern: ^\D+$ (PCRE intent: reject subjects containing any digit). is_non_ere returns 1 (not detected), normalize_ere leaves \D unchanged, and the resolver emits ^\D+$ with exit 0, declaring it enforceable. Both grep -E '^\D+$' and bash's [[ =~ '^\D+$' ]] treat the unrecognized \D as the literal character D — the emitted gate silently accepts any commit subject that is not a string of Ds, the opposite of the intended rule. No diagnostic is produced; the consumer observes enforcement is active but the gate enforces the wrong thing.

The same mis-enforcement applies to \W (literal W) and \S (literal S).

Fix options (either works): Add detection in is_non_ere:

[[ "$p" == *'\D'* || "$p" == *'\W'* || "$p" == *'\S'* ]] && return 0

Or add normalization in normalize_ere expanding \D[^0-9], \W[^[:alnum:]_], \S[^[:space:]].

(Raised in prior review at 337c83f; unaddressed at HEAD 75a97f5.)

Fix this →


IMPORTANT — PCRE inline flags and atomic groups pass is_non_ere and are emitted with exit 0 | confidence: high

File: lib/resolve-convention-pattern.sh L103–L108

is_non_ere detects lookaround ((?=, (?!, (?<) and named groups ((?P<), but not:

Construct Example Behavior when emitted
Inline flag group (?i:feat|fix) normalize_ere does not match (?i: (only matches (?: exactly); emitted as-is; invalid ERE
Mode modifier (?i)^feat: .+ Not detected; emitted as-is; invalid ERE
Atomic group (?>feat|fix) Not detected; emitted as-is; invalid ERE
PCRE comment (?#type)^feat Not detected; emitted as-is; invalid ERE

Worst-case scenario: Team sets subject_pattern: (?i:feat|fix): .+ expecting case-insensitive enforcement. is_non_ere returns 1; normalize_ere changes (?:( but (?i: starts with (?i, not (?:, so the pattern is not transformed and is emitted as (?i:feat|fix): .+ with exit 0. A consuming hook passes this to grep -E or [[ =~ ]]. GNU grep returns exit 2 (invalid regex); bash [[ =~ ]] errors similarly. If the hook interprets any non-zero as "no match" and passes the commit, the gate is silently bypassed with no diagnostic to the consumer.

# Add to is_non_ere:
[[ "$p" == *'(?i'* || "$p" == *'(?m'* || "$p" == *'(?s'* || "$p" == *'(?x'* ]] && return 0
[[ "$p" == *'(?>'* || "$p" == *'(?#'* ]] && return 0

(Raised in all three prior Claude security reviews at e271002, 934e787, and 337c83f; unaddressed at HEAD 75a97f5.)

Fix this →


SUGGESTION — PCRE absolute anchors \A, \z, \Z silently enforce against literal characters | confidence: medium

File: lib/resolve-convention-pattern.sh L103–L118

\A (PCRE start-of-string), \z (PCRE end-of-string, no trailing newline), and \Z (PCRE end-of-string, tolerates trailing newline) are not detected by is_non_ere and not normalized by normalize_ere. POSIX ERE has no equivalent anchors; both GNU grep and bash [[ =~ ]] treat them as the literal characters A, z, and Z.

Scenario: A consumer writes \Afeat: .+\z. is_non_ere returns 1; normalize_ere leaves the escapes unchanged; the resolver emits \Afeat: .+\z with exit 0. The consuming hook enforces this against commit subjects: it now requires a subject that contains Afeat: followed by any characters ending in z — a semantically wrong gate with no diagnostic.

# Add to is_non_ere:
[[ "$p" == *'\A'* || "$p" == *'\z'* || "$p" == *'\Z'* ]] && return 0

Fix this →


SUGGESTION — normalize_ere substitutions are context-blind inside bracket expressions | confidence: medium

File: lib/resolve-convention-pattern.sh L116

The \w/\d/\s substitutions run globally and do not track whether the match site is already inside a bracket expression. [\w-]+ becomes [[[:alnum:]_]-]+: the outer [ and the [ that opens [:alnum:] nest, which GNU grep ERE accepts but interprets differently from the intended character class. Bash [[ =~ ]] on macOS (BSD regex) may reject it with exit status 2. Either failure is silent — exit 0 was already returned.

(Raised in all three prior Claude security reviews; unaddressed at HEAD 75a97f5.)

Fix this →


SUGGESTION — No compile-time ERE validation before emitting with exit 0 | confidence: medium

File: lib/resolve-convention-pattern.sh L151

printf '%s\n' "$(normalize_ere "$value")"
exit 0

Exit 0 means "enforce this." Any construct that evades is_non_ere's string-match detection is emitted with exit 0 and no validation that the result compiles as a valid ERE. A one-line probe before emit would make exit 0 a guarantee:

normalized="$(normalize_ere "$value")"
if ! printf '' | grep -qE "$normalized" 2>/dev/null; then
  printf 'resolve-convention-pattern: %s resolved to an invalid ERE; enforcement disabled.\n' "$key" >&2
  exit 1
fi
printf '%s\n' "$normalized"

This would have caught the \D/\W/\S case and the (?i: case above as defense-in-depth, even before the dedicated is_non_ere checks are added.

(Raised in all three prior Claude security reviews; unaddressed at HEAD 75a97f5.)

Fix this →


Confirmed clean

Pattern Verdict
pull_request trigger (not pull_request_target)
${{ github.base_ref }} via env: indirection, not direct run: interpolation
"origin/$BASE_REF" in git diff/git show — prefixed, no option injection
actions/checkout@9c091bb… SHA-pinned
persist-credentials: false on checkout
copies array hardcoded in sync script (not user-controllable)
key validated to subject_pattern|pr_title_pattern before awk -v key=
normalize_ere pipes to sed stdin (not as a sed script)
h2_value checks [[ -f "$file" ]] before awk
set -uo pipefail throughout; set -euo pipefail in sync script
No secrets, tokens, or credentials referenced
New CI job mirrors established SHA-pinned pattern
git show "$base:$manifest"$base is a git ref from env var, not user content

@kyle-sexton
kyle-sexton force-pushed the feat/913-convention-enforcement-seam branch from 75a97f5 to 31d182f Compare July 22, 2026 02:37
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 18s —— View job


PR Review: feat(source-control): add machine-readable commit-convention enforcement seam

  • Gather context and check current HEAD vs prior reviews
  • Read lib/resolve-convention-pattern.sh at current HEAD (31d182f)
  • Read lib/resolve-convention-pattern.test.sh at current HEAD
  • Cross-check all prior findings against current state
  • Compile and post final review

What changed since the last review (75a97f531d182f)

The two changed files — lib/resolve-convention-pattern.sh and lib/resolve-convention-pattern.test.sh — address all four blocking/important issues that were flagged across the previous five reviews. The fix is substantive and the approach is correct.


Resolved findings (all blocking/important items closed)

✅ Negated PCRE shorthand classes (\D, \W, \S) → now detected

lib/resolve-convention-pattern.sh:115

[[ "$p" =~ \\[DSW] ]] && return 0

Uses the same =~ backreference idiom already established for \\[0-9]. A consumer pattern like ^\D+$ now exits 1 with a diagnostic rather than silently enforcing ^D+$ (literal D). Tests added at lines 87–89.


✅ Shorthand escapes inside bracket expressions → now detected

lib/resolve-convention-pattern.sh:121–122

local bracket_shorthand='\[[^]]*\\[dwsDSW]'
[[ "$p" =~ $bracket_shorthand ]] && return 0

The variable indirection to hold the regex is the correct way to avoid [^]] closing the [[ ]] prematurely. Correctly handles the full set of affected escapes (lowercase and uppercase). Tests added at lines 96–99.


✅ Absolute PCRE anchors (\A, \Z, \z) → now detected

lib/resolve-convention-pattern.sh:112

[[ "$p" == *'\A'* || "$p" == *'\Z'* || "$p" == *'\z'* ]] && return 0

Tests added at lines 91–94. The diagnostic at line 162 is updated to enumerate all three anchors.


✅ Compile-time ERE validation before emitting → now present

lib/resolve-convention-pattern.sh:172–176

grep -E -- "$normalized" </dev/null >/dev/null 2>&1
if (($? >= 2)); then
  printf '%s\n' "resolve-convention-pattern: $key does not compile as POSIX ERE after normalization; enforcement disabled." >&2
  exit 1
fi

This is the defense-in-depth layer that catches anything that evades is_non_ere. The </dev/null input with -E -- is the correct form (the -- prevents a pattern starting with - from being interpreted as a flag). Tests added at lines 101–106.

Important side effect: PCRE inline flags ((?i:feat|fix), (?i), (?m), (?s), (?x)), atomic groups ((?>…)), and PCRE comments ((?#…)) — the remaining constructs not explicitly detected in is_non_ere — now all fall through to the compile check and exit 1 correctly. The failure mode is the generic "does not compile" message rather than a specific diagnostic, but they no longer reach a consuming hook with a false exit-0.


Open findings (all minor — none blocking)

1. No explicit is_non_ere detection for PCRE inline flags — diagnostic quality

File: lib/resolve-convention-pattern.sh:103–123

(?i:feat|fix), (?i), (?m), (?s), (?x), (?>…), and (?#…) all reach the compile probe rather than the specific is_non_ere diagnostic. This is safe — they exit 1 correctly — but the consumer sees:

resolve-convention-pattern: subject_pattern does not compile as POSIX ERE after normalization; enforcement disabled.

instead of the more actionable:

resolve-convention-pattern: subject_pattern uses a PCRE construct with no POSIX-ERE equivalent …; enforcement disabled

Whether to add explicit detection is a trade-off: the compile check is a cleaner net but the is_non_ere diagnostic guides consumers toward a fix. A test case for the compile-check path with an inline-flag pattern (e.g., (?i:feat|fix)) would at minimum verify the coverage guarantee.

Fix this →


2. Raw GitHub URL in config-resolution.md — DX

File: plugins/source-control/reference/config-resolution.md:94

[commit-convention enforcement seam](https://raw.githubusercontent.com/melodic-software/claude-code-plugins/main/docs/conventions/commit-convention/README.md)

raw.githubusercontent.com serves Markdown source (raw text), not the rendered view. It also hardcodes main. Relative path renders correctly and is branch-agnostic:

[commit-convention enforcement seam](../../../docs/conventions/commit-convention/README.md)

Fix this →


3. Unnecessary shellcheck disable=SC2016 — misleading comment

File: lib/resolve-convention-pattern.sh:51

SC2016 warns about backticks inside double-quoted strings. PR_DEFERRAL uses single quotes — SC2016 cannot fire here. The suppression directive has no effect and will mislead readers.

Fix this →


4. Dangling — see note in newrepo helper comment

File: lib/resolve-convention-pattern.test.sh:36

#   newrepo "<team body>" [local-body] [does NOT create user-global — see note]

No "note" exists in the file. The reason is clear from context but the forward reference will confuse a future editor:

#   newrepo "<team body>" [local-body]  (no user-global: enforcement reads team-tracked layer only)

Fix this →


5. Missing test: pr_title_pattern deferral when subject_pattern is the CC keyword

File: lib/resolve-convention-pattern.test.sh:126–128

The deferral test uses a custom regex (^ABC-[0-9]+: .+). The combined path — pr_title_pattern defers to subject_pattern which is the Conventional Commits keyword — exercises CC expansion through the deferral branch and is the most likely real-world config. A test case would close the gap:

r="$(newrepo $'## subject_pattern\nConventional Commits\n\n## pr_title_pattern\nSame as `subject_pattern`.')"
assert_eq "pr deferral + CC keyword -> CC_ERE" "$CC_ERE" "$(run "$r" pr_title_pattern)"

Fix this →


What's solid

The substantive changes in this update directly address the issues that would have made the seam unsafe for #914/#919 to vendor:

  • \D/\W/\S detection is correct and tested. The =~ idiom matches the existing \\[0-9] pattern consistently.
  • Bracket-context shorthand detection is correct. The variable indirection for the regex is the right call; the character class [dwsDSW] covers both lowercase (for normalization candidates) and uppercase (for negated forms).
  • \A/\Z/\z detection is straightforward string-match and matches the existing \b/\B approach.
  • Compile-time ERE probe is the right design. grep -E -- "$normalized" </dev/null is zero-overhead, catches any remaining gaps (including PCRE inline flags), and makes exit-0 a meaningful guarantee rather than a best-effort claim. The (($? >= 2)) idiom correctly distinguishes "no match" (exit 1, valid ERE) from "compile error" (exit 2+).
  • Test isolation and count: 23 cases now (19 previous + 4 new). All new tests follow the existing assert_exit/assert_eq pattern.
  • The policy-floor contract, CC keyword expansion, unresolved → no-op, and is_non_ere-before-normalize_ere ordering remain correct and unchanged.

The four minor open items are cosmetic (3) and a test gap (2) — none affect correctness or the seam contract.

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 58s —— View job


Security Review — PR #925

  • Gather context and read PR diff
  • Review lib/resolve-convention-pattern.sh (core library)
  • Review lib/resolve-convention-pattern.test.sh (test suite)
  • Review scripts/sync-resolve-convention-pattern.sh (sync scaffold)
  • Review .github/workflows/ci.yml changes
  • Cross-check against prior security findings
  • Post final findings

Summary

No CRITICAL or IMPORTANT findings at HEAD 31d182f. All IMPORTANT vulnerabilities raised in the six prior security reviews have been addressed in this push. One SUGGESTION-level gap remains.


Prior findings — now resolved

The four blocking issues identified across prior reviews are confirmed fixed in the current HEAD:

Prior finding Fix in current HEAD
\D/\W/\S negated shorthands silently emit mis-enforcing ERE is_non_ere L115: [[ "$p" =~ \\[DSW] ]] && return 0
\A/\Z/\z absolute anchors silently emit mis-enforcing ERE is_non_ere L112: explicit pattern checks
\d/\w/\s inside bracket expressions produce structurally wrong ERE is_non_ere L121–122: bracket_shorthand regex detects \[[^]]*\\[dwsDSW] and rejects
No compile-time ERE validation — exit 0 could mean invalid ERE L172–176: grep -E -- "$normalized" </dev/null probe; exits 1 on compile failure

The (?i:, (?>, (?# PCRE inline-flag / atomic-group / comment constructs that were also repeatedly flagged are not explicitly detected in is_non_ere, but they are now caught by the compile-time probe at line 172 — grep -E on the ubuntu-24.04 runner rejects them with exit 2, the probe detects $? >= 2, and the resolver correctly exits 1 with a diagnostic. The safety contract (exit 0 ↔ valid enforceable ERE) is maintained; the diagnostic is less specific than an is_non_ere hit would be, but the outcome is correct.


Findings

SUGGESTION — \p{Lu}-style Unicode property escapes pass both is_non_ere and the compile probe — confidence: low

File: lib/resolve-convention-pattern.sh L103–L123 and L172

is_non_ere does not detect \p{Lu} or similar PCRE Unicode property escapes (\P{…}, \X). normalize_ere does not transform them. On GNU grep, \p in ERE is treated as the literal character p (the backslash is dropped or treated as escape-of-non-special), so grep -E -- "\\p{Lu}" typically exits 0 or 1 (valid ERE — matching literal p{Lu}) rather than 2. The compile probe therefore passes, and the resolver emits \p{Lu} with exit 0, claiming it is a valid ERE to enforce. A consuming hook then enforces the wrong rule — matching literal p{Lu} in subjects instead of Unicode uppercase letters — with no diagnostic to the consumer.

The practical exploitability is very low: \p{Lu} in a commit-subject convention pattern is an unusual author choice. But the silent-mis-enforcement pattern is the same class of issue that the \D/\W fixes addressed in this commit.

Suggested fix: Add to is_non_ere:

# Unicode property escapes (\p{…}, \P{…}) have no ERE spelling — GNU grep
# treats \p as literal p, producing silent mis-enforcement.
[[ "$p" == *'\p{'* || "$p" == *'\P{'* ]] && return 0

Fix this →


Confirmed clean

Pattern Verdict
pull_request trigger (not pull_request_target)
${{ github.base_ref }} via env: indirection, not direct run: interpolation
"origin/$BASE_REF" in git commands — prefixed, no option injection
actions/checkout@9c091bb… SHA-pinned
persist-credentials: false
copies array hardcoded (currently empty) — not user-controllable
key validated to subject_pattern|pr_title_pattern before awk -v key=
normalize_ere pipes to sed stdin (not as a sed script)
grep -E -- "$normalized" uses -- to prevent option injection
h2_value checks [[ -f "$file" ]] before awk; file content flows data-only
set -uo pipefail throughout; set -euo pipefail in sync script
No secrets, tokens, or credentials referenced
New CI job mirrors established SHA-pinned pattern
git show "$base:$manifest"$base is a git ref from env var, not user content
\D/\W/\S, \A/\Z/\z, bracket-context shorthands — all detected and rejected
Compile-time ERE probe covers PCRE inline flags and other detection gaps
Unresolved → exit 1, stdout empty — never falls back to CC default
Team-tracked layer only — local/user-global overlays cannot reach the gate

@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: 31d182fb59

ℹ️ 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/resolve-convention-pattern.sh Outdated
@kyle-sexton
kyle-sexton force-pushed the feat/913-convention-enforcement-seam branch from 31d182f to c75c2fc Compare July 22, 2026 02:44
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 7s —— View job


PR Review: feat(source-control): add machine-readable commit-convention enforcement seam

  • Gather context and check what changed since last review (31d182fc75c2fc)
  • Review lib/resolve-convention-pattern.sh at current HEAD
  • Review lib/resolve-convention-pattern.test.sh at current HEAD
  • Cross-check all prior findings against current state
  • Compile and post final review

Context

This review covers HEAD c75c2fc — a rebase-squash of all prior pushes to this branch. Prior reviews tracked the fix history across multiple commits (e271002934e78775a97f531d182f). All blocking and important findings from those reviews are confirmed resolved in the current HEAD. Five minor items remain.


What's new vs 31d182f

The is_non_ere function was substantially refactored. The earlier approach (individual string checks for \D/\W/\S/\A/\Z/\z) has been replaced with a general catch-all:

while IFS= read -r esc; do
  case "$esc" in
  '\d' | '\w' | '\s') ;;
  *) return 0 ;;
  esac
done < <(printf '%s' "$p" | grep -oE '\\[a-zA-Z]')

This is a better design: it enumerates every \letter escape in the pattern and rejects any that isn't one of the three normalizable shorthands. It now correctly handles \D/\W/\S, \A/\Z/\z, \b/\B, control escapes \t/\n/\r/\f/\v, \p (Unicode property prefix), and any future PCRE \letter construct — without needing explicit per-construct checks. A new test at line 96 (\t -> exit 1) exercises the control-escape path. The \p{Lu} suggestion from the last security review is now covered as a byproduct.


Resolved findings (all blocking/important items closed)

Prior finding Resolution in c75c2fc
\D/\W/\S silently emit mis-enforcing ERE General \\[a-zA-Z] loop; *) return 0 catches them
\A/\Z/\z absolute anchors Same loop
\t/\n/\r/\f/\v control escapes (Codex finding) Same loop; explicit test added at line 96
\p{Lu} Unicode property escapes (last security review) Same loop; \p triggers *) return 0
[\w-] bracket-context shorthand → broken ERE bracket_shorthand regex (\[[^]]*\\[dwsDSW])
No compile-time ERE validation grep -E -- "$normalized" </dev/null probe at line 177

Open findings (all minor — none blocking)

1. PCRE inline flags fall through to compile probe — diagnostic quality gap

File: lib/resolve-convention-pattern.sh:103–128

(?i:feat|fix), (?i), (?>…), (?#…) contain no \letter sequences, so the new while loop exits cleanly and is_non_ere returns 1 (not detected). normalize_ere doesn't transform (?i: (only (?: matches). The compile probe at line 177 catches them — grep -E returns exit 2 on invalid ERE — and the resolver correctly exits 1. The safety contract is upheld. The gap is that the consumer sees the generic diagnostic:

resolve-convention-pattern: subject_pattern does not compile as POSIX ERE after normalization; enforcement disabled.

rather than the specific PCRE-construct message that is_non_ere emits. Adding explicit detection:

[[ "$p" == *'(?i'* || "$p" == *'(?m'* || "$p" == *'(?s'* || "$p" == *'(?x'* ]] && return 0
[[ "$p" == *'(?>'* || "$p" == *'(?#'* ]] && return 0

would give consumers actionable guidance. A test exercising the compile-probe path for an inline-flag pattern would also close the coverage gap. This is a polish item, not a correctness issue.

Fix this →


2. Unnecessary shellcheck disable=SC2016 — misleading comment

File: lib/resolve-convention-pattern.sh:51

# shellcheck disable=SC2016  # backticks are a literal part of the deferral marker, not a substitution
readonly PR_DEFERRAL='Same as `subject_pattern`.'

SC2016 warns about backtick substitution inside double-quoted strings. PR_DEFERRAL uses single quotes — SC2016 cannot fire here regardless. The directive has no effect and misleads a reader into thinking suppression was required. Raised in every prior review; still present.

Fix this →


3. Raw GitHub URL in config-resolution.md — serves source, not rendered page

File: plugins/source-control/reference/config-resolution.md:94

[commit-convention enforcement seam](https://raw.githubusercontent.com/melodic-software/claude-code-plugins/main/docs/conventions/commit-convention/README.md)

raw.githubusercontent.com serves raw Markdown text, not the rendered view. It also hardcodes main. A relative path renders correctly in GitHub and is branch-agnostic (file is 3 levels up from plugins/source-control/reference/):

[commit-convention enforcement seam](../../../docs/conventions/commit-convention/README.md)

Raised in every prior review; still present.

Fix this →


4. Dangling — see note in newrepo helper comment

File: lib/resolve-convention-pattern.test.sh:36

#   newrepo "<team body>" [local-body] [does NOT create user-global — see note]

No "note" exists in this file. The intent is clear from context (enforcement reads the team-tracked layer only, so a user-global overlay is irrelevant to test), but the forward reference will confuse a future editor. Raised in every prior review; still present.

#   newrepo "<team body>" [local-body]  (no user-global: enforcement reads team-tracked layer only)

Fix this →


5. Missing test: pr_title_pattern deferral when subject_pattern is the CC keyword

File: lib/resolve-convention-pattern.test.sh:131–133

The deferral test uses a custom regex for subject_pattern. The path where pr_title_pattern: Same as \subject_pattern`.defers to a CC-keywordsubject_pattern(exercising CC expansion *through* the deferral branch) is not covered. This is the most likely real-world config for consumers of#914/#919`. Raised in every prior review; still absent.

r="$(newrepo $'## subject_pattern\nConventional Commits\n\n## pr_title_pattern\nSame as `subject_pattern`.')"
assert_eq "pr deferral + CC keyword -> CC_ERE" "$CC_ERE" "$(run "$r" pr_title_pattern)"

Fix this →


What's solid

  • General \\[a-zA-Z] catch-all is the right abstraction. Enumerating by allowlist (\d/\w/\s are the only normalizable shorthands) is more robust than a blocklist — any future PCRE \letter escape is rejected by default without a code change.
  • is_non_ere runs before normalize_ere: prevents PCRE constructs from being mangled into accidentally-valid ERE that enforces the wrong thing.
  • Compile probe (grep -E -- "$normalized" </dev/null) makes exit-0 a meaningful guarantee. Covers PCRE inline flags and any undetected constructs as defense-in-depth.
  • Policy-floor contract (h2_value reads TEAM_FILE only; unresolved → exit 1, never CC default; local overlay test at line 125–129).
  • Bracket-context shorthand detection via variable-held regex is the correct pattern to avoid [[ ]] early-close.
  • ${copies[@]+"${copies[@]}"} empty-array idiom in sync script — correct under set -u.
  • CI job mirrors established pattern: SHA-pinned checkout, persist-credentials: false, BASE_REF via env var, correct ci-status.needs wiring.

Summary

All blocking and important issues identified across six prior reviews are resolved in c75c2fc. The five remaining items are all minor polish. The seam contract (exit 0 = valid enforceable ERE; exit 1 = no enforcement; never CC fallback; team-tracked layer only) is correctly implemented and safe for #914/#919 to vendor.

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

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


Security Review — PR #925

  • Gather context and read prior review history
  • Get PR diff at current HEAD (c75c2fc)
  • Review lib/resolve-convention-pattern.sh
  • Review lib/resolve-convention-pattern.test.sh
  • Review scripts/sync-resolve-convention-pattern.sh and CI changes
  • Cross-check against prior security findings
  • Post final findings

Summary

No CRITICAL or IMPORTANT findings at HEAD c75c2fc. All IMPORTANT vulnerabilities raised across the prior six security reviews are confirmed resolved. One SUGGESTION-level gap remains.


Prior findings — confirmed fixed

Prior finding Fix at HEAD
\D/\W/\S negated shorthands emitted as literal-letter mis-enforcement Catch-all grep -oE '\\[a-zA-Z]' loop (L122–127): any \letter that isn't \d/\w/\sreturn 0
\A/\Z/\z absolute anchors emitted as literal-character mis-enforcement Same catch-all loop — \A, \Z, \z all hit the *) return 0 branch
\t/\n/\r/\f/\v control escapes emitted as literal-character mis-enforcement Same catch-all loop — confirmed by test at L96–99
\p{Lu} Unicode property escapes pass is_non_ere and compile probe (raised in final prior review as SUGGESTION) Catch-all loop extracts \p → not \d/\w/\sreturn 0 ✓ no longer a gap
\d/\w/\s inside bracket expressions produce wrong double-nested ERE Bracket-shorthand regex '\[[^]]*\\[dwsDSW]' at L114–115
No compile-time ERE validation — exit 0 could mean invalid ERE grep -E -- "$normalized" </dev/null probe at L177–181

Findings

SUGGESTION — PCRE (? constructs without \letter escapes bypass explicit is_non_ere detection | confidence: low

File: lib/resolve-convention-pattern.sh L103–L128

The catch-all grep -oE '\\[a-zA-Z]' loop correctly handles all \letter PCRE escapes. However, PCRE constructs that contain no backslash-letter sequence — specifically (?i:…), (?i), (?m), (?s), (?x), (?>…), and (?#…) — are not matched by any explicit is_non_ere check:

Construct Matched by is_non_ere? Caught by compile probe?
(?i:feat|fix) No Yes — grep -E on glibc ERE rejects it (exit 2)
(?i)^feat: .+ No Yes — same
(?>feat|fix) No Yes — same
(?#comment)^feat No Yes — same

On Linux runners with glibc regex (ubuntu-24.04), all four cases exit 2 from the compile probe and are correctly handled. The safety contract (exit 0 ↔ valid ERE) holds on this platform.

The residual risk is diagnostic quality only: a consumer who sets subject_pattern: (?i:feat|fix) sees the generic "does not compile as POSIX ERE after normalization" message rather than the informative is_non_ere diagnostic that names the specific construct and explains how to fix it. This is not a security gap — the exit-1 outcome is correct — but it degrades the consumer DX compared to the explicit checks.

If adding explicit detection, extend is_non_ere before the final return 1:

# PCRE inline flag groups / atomic group / comment — no ERE equivalent
[[ "$p" == *'(?i'* || "$p" == *'(?m'* || "$p" == *'(?s'* || "$p" == *'(?x'* ]] && return 0
[[ "$p" == *'(?>'* || "$p" == *'(?#'* ]] && return 0

Fix this →


Confirmed clean

Pattern Verdict
pull_request trigger (not pull_request_target)
${{ github.base_ref }} via env: indirection, not direct run: interpolation
"origin/$BASE_REF" in git commands — prefixed; no option injection from branch name
actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 SHA-pinned
persist-credentials: false on checkout
copies array hardcoded (currently empty) — not user-controllable
key validated to subject_pattern|pr_title_pattern before awk -v key=
User file content ($p) flows through printf '%s' | sed and printf '%s' | grep — data paths only, no eval or command construction
grep -E -- "$normalized" uses -- to prevent option injection from pattern
normalize_ere uses bash parameter expansion (${p//'(?:'/'('}) and sed stdin pipe — no code execution from pattern content
h2_value checks [[ -f "$file" ]] before awk; awk -v key="$k" with validated key
Catch-all \letter loop covers \D/\W/\S, \A/\Z/\z, \t/\n/\r/\f/\v, \p/\P (Unicode properties), and all other undeclared \letter escapes
Compile probe on glibc ERE catches (?i:…), (?>…), (?#…) that bypass explicit string checks
set -uo pipefail throughout; set -euo pipefail in sync script
No secrets, tokens, or credentials referenced
New CI job mirrors established SHA-pinned pattern; correctly placed in ci-status.needs
Unresolved → exit 1, stdout empty — never falls back to CC default
Team-tracked layer only — local/user-global overlays cannot reach the gate

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

ℹ️ 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/resolve-convention-pattern.sh Outdated
@kyle-sexton
kyle-sexton force-pushed the feat/913-convention-enforcement-seam branch from c75c2fc to 9c089fc Compare July 22, 2026 02:51
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 58s —— View job


PR Review: feat(source-control): add machine-readable commit-convention enforcement seam

  • Load tools
  • Read current state of changed files at HEAD (9c089fc)
  • Cross-check against prior open findings
  • Post comprehensive review

Context

This review covers HEAD 9c089fc. The files read (lib/resolve-convention-pattern.sh, lib/resolve-convention-pattern.test.sh, plugins/source-control/reference/config-resolution.md) are unchanged from the prior review at c75c2fc — the synchronize event was a rebase onto main, not a content change.

All blocking and important issues identified across eight prior reviews remain resolved. Five minor items carried forward from prior reviews are still open.


Confirmed resolved (all blocking/important items)

Prior finding Status
\D/\W/\S negated shorthands silently emitted as literal-letter mis-enforcement ✅ Fixed — catch-all grep -oE '\\[a-zA-Z]' loop, *) return 0
\A/\Z/\z absolute anchors silently emitted ✅ Fixed — same loop
\t/\n/\r/\f/\v control escapes silently emitted ✅ Fixed — same loop; test at line 97–99
\p{Lu} Unicode property escapes pass is_non_ere and compile probe ✅ Fixed — \p triggers *) return 0 in the loop
[\w-] bracket-context shorthands → broken double-nested ERE ✅ Fixed — bracket_shorthand regex at lines 120–121
Escaped-backslash ambiguity (\\d = literal, not class) ✅ Fixed — double_backslash check at lines 113–114
No compile-time ERE validation — exit 0 could mean invalid ERE ✅ Fixed — grep -E -- "$normalized" </dev/null probe at lines 183–187

The seam contract (exit 0 ↔ valid enforceable POSIX ERE; exit 1 = no enforcement; never CC fallback; team-tracked layer only) is correctly implemented and safe for #914/#919 to vendor.


Open findings (all minor — none blocking)

1. PCRE inline flags still fall through to compile probe — diagnostic quality

File: lib/resolve-convention-pattern.sh:103–134

(?i:feat|fix), (?i), (?m), (?s), (?x), (?>…), and (?#…) contain no \letter sequences, so the catch-all loop exits without triggering and is_non_ere returns 1. normalize_ere does not transform (?i: (only (?: matches exactly). The compile probe at lines 183–187 catches them — grep -E exits 2 on invalid ERE, and the resolver correctly exits 1.

Safety contract is maintained. The gap is diagnostic quality only: a consumer who configures subject_pattern: (?i:feat|fix) sees the generic "does not compile as POSIX ERE after normalization" message rather than the informative is_non_ere message that names the specific construct. Adding explicit detection before return 1:

# PCRE inline flag groups / atomic group / comment — no ERE equivalent
[[ "$p" == *'(?i'* || "$p" == *'(?m'* || "$p" == *'(?s'* || "$p" == *'(?x'* ]] && return 0
[[ "$p" == *'(?>'* || "$p" == *'(?#'* ]] && return 0

Fix this →


2. Unnecessary shellcheck disable=SC2016 — misleading comment

File: lib/resolve-convention-pattern.sh:51

# shellcheck disable=SC2016  # backticks are a literal part of the deferral marker, not a substitution
readonly PR_DEFERRAL='Same as `subject_pattern`.'

SC2016 warns about backtick substitution inside double-quoted strings. PR_DEFERRAL uses single quotes — SC2016 cannot fire on a single-quoted assignment regardless. The directive has no effect and misleads a reader into thinking suppression was required. Present in every prior review; still unaddressed.

Fix this →


3. Raw GitHub URL in config-resolution.md — serves Markdown source, not rendered page

File: plugins/source-control/reference/config-resolution.md:94

[commit-convention enforcement seam](https://raw.githubusercontent.com/melodic-software/claude-code-plugins/main/docs/conventions/commit-convention/README.md)

raw.githubusercontent.com serves Markdown source text, not the rendered view. Following the link in GitHub's PR or file viewer presents raw text. It also hardcodes main, which will break on forks or if the default branch changes. A relative path renders correctly in GitHub and is branch-agnostic (the file is 3 levels up from plugins/source-control/reference/):

[commit-convention enforcement seam](../../../docs/conventions/commit-convention/README.md)

Present in every prior review; still unaddressed.

Fix this →


4. Dangling — see note in newrepo helper comment

File: lib/resolve-convention-pattern.test.sh:36

#   newrepo "<team body>" [local-body] [does NOT create user-global — see note]

No "note" exists in this file. The reason is clear from context (enforcement reads the team-tracked layer only, so testing a user-global overlay is purposeless), but the forward reference will confuse a future editor. Present in every prior review; still unaddressed.

#   newrepo "<team body>" [local-body]  (no user-global: enforcement reads team-tracked layer only)

Fix this →


5. Missing test: pr_title_pattern deferral when subject_pattern is the CC keyword

File: lib/resolve-convention-pattern.test.sh:137–138

The deferral test at line 137 uses ^ABC-[0-9]+: .+ as subject_pattern. The combined path — pr_title_pattern: Same as \subject_pattern`.deferring to aConventional Commitskeywordsubject_pattern(triggering CC keyword expansion *through* the deferral branch) — is untested. This is the most likely real-world config for consumers of#914/#919`. Present in every prior review; still absent.

# --- pr_title_pattern deferral + CC keyword expansion ---
r="$(newrepo $'## subject_pattern\nConventional Commits\n\n## pr_title_pattern\nSame as `subject_pattern`.')"
assert_eq "pr deferral + CC keyword -> CC_ERE" "$CC_ERE" "$(run "$r" pr_title_pattern)"

Fix this →


What's solid

  • Catch-all \\[a-zA-Z] loop in is_non_ere is the right abstraction. Allowlist (\d/\w/\s normalizable; everything else rejected) is more robust than any blocklist — any future PCRE \letter escape is rejected by default without a code change.
  • is_non_ere runs before normalize_ere: the ordering prevents lookaround/backreference/etc from being mangled into accidentally-valid ERE that enforces the wrong thing.
  • Bracket-context shorthand detection via variable-held regex ('\[[^]]*\\[dwsDSW]') is the correct pattern to avoid [[ ]] early-close by [^]].
  • Escaped-backslash guard (double_backslash at line 113) correctly excludes patterns like ^foo\\d$ where the double-backslash makes the d a literal character, not a class.
  • Compile probe (grep -E -- "$normalized" </dev/null) makes exit-0 a meaningful guarantee and covers PCRE inline flags plus any future detection gap as defense-in-depth.
  • Policy-floor contract (h2_value reads TEAM_FILE only; unresolved → exit 1; never CC default; local overlay explicitly tested at line 132–134).
  • ${copies[@]+"${copies[@]}"} empty-array idiom in sync script — correct under set -u.
  • CI job structure mirrors the established parse-concern-value-sync pattern: SHA-pinned checkout, persist-credentials: false, BASE_REF via env var, fetch-depth: 0, correct ci-status.needs wiring.

Summary

No new issues at HEAD 9c089fc. All blocking and important findings from eight prior reviews remain resolved. Five minor polish items (2 misleading comments, 1 DX link, 1 doc comment, 1 test gap) are unchanged since the previous round — none affect the seam contract or the safety of #914/#919 vendoring this resolver. The architecture is sound and the implementation is correct.

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 42s —— View job


Security Review — PR #925

  • Gather context and read prior review history
  • Get PR diff at current HEAD (9c089fc)
  • Review lib/resolve-convention-pattern.sh
  • Review lib/resolve-convention-pattern.test.sh
  • Review scripts/sync-resolve-convention-pattern.sh and CI changes
  • Cross-check against prior security findings
  • Post final findings

Summary

No CRITICAL or IMPORTANT findings at current HEAD. All IMPORTANT vulnerabilities raised across the prior seven security reviews remain fixed. One SUGGESTION-level gap carries over unchanged from the most recent review.


Prior findings — confirmed still fixed

All fixes introduced in the 31d182fc75c2fc iteration are present in the current HEAD:

Prior finding Fix at HEAD
\D/\W/\S negated shorthands silently emit mis-enforcing ERE Catch-all grep -oE '\\[a-zA-Z]' loop (L128–135): any \letter not in the \d/\w/\s allowlist → return 0
\A/\Z/\z absolute anchors and \b/\B/\t/\n/\r/\f/\v control escapes Same loop — all trigger *) return 0
\p{Lu} Unicode property escapes pass is_non_ere (raised last review as SUGGESTION) Same loop — \p hits *) return 0; no longer a gap
\d/\w/\s inside bracket expressions produce double-nested ERE Bracket-shorthand regex '\[[^]]*\\[dwsDSW]' (L121–122)
\\d (escaped backslash + d) translated as if it were the \d class Double-backslash guard (L113–115): any \\ in the pattern → return 0
No compile-time ERE validation — exit 0 could mean invalid ERE grep -E -- "$normalized" </dev/null probe (L184–188)

Findings

SUGGESTION — PCRE (? constructs bypass explicit is_non_ere detection; caught only by compile probe | confidence: low

File: lib/resolve-convention-pattern.sh L103–L135

(?i:…), (?i), (?m), (?s), (?x), (?>…), and (?#…) contain no \letter sequences, so the catch-all loop exits cleanly and is_non_ere returns 1 (not detected). normalize_ere does not transform (?i: (the (?:( substitution matches exactly (?:, not (?i:). These patterns then reach the compile probe at line 184, where grep -E on the ubuntu-24.04 glibc ERE engine rejects them with exit 2 → resolver correctly exits 1.

The safety contract (exit 0 ↔ valid ERE) holds on the CI platform. The residual issue is diagnostic quality only: a consumer whose team file contains subject_pattern: (?i:feat|fix): .+ sees the generic message:

resolve-convention-pattern: subject_pattern does not compile as POSIX ERE after normalization; enforcement disabled.

instead of the more actionable is_non_ere message that names the specific construct. No correctness or security regression is possible from this gap — the exit-1 outcome is correct regardless.

If explicit detection is added, extend is_non_ere before the final return 1:

# PCRE inline flags / flag groups / atomic group / comment — no ERE equivalent
[[ "$p" == *'(?i'* || "$p" == *'(?m'* || "$p" == *'(?s'* || "$p" == *'(?x'* ]] && return 0
[[ "$p" == *'(?>'* || "$p" == *'(?#'* ]] && return 0

(Carried forward from the prior review; unaddressed at current HEAD. Given that the compile probe handles the safety outcome correctly, this is polish, not a blocker.)

Fix this →


Confirmed clean

Pattern Verdict
pull_request trigger (not pull_request_target)
${{ github.base_ref }} via env: indirection, not direct run: interpolation
"origin/$BASE_REF" in git commands — shell-variable expansion only, branch names cannot break double-quoted strings
actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 SHA-pinned
persist-credentials: false on checkout
Workflow-level permissions: contents: read; new job inherits it, no override
copies array hardcoded (currently empty) — not user-controllable
key validated to subject_pattern|pr_title_pattern before awk -v key=
User file content ($p) flows through printf '%s' | sed and printf '%s' | grep data paths only — no eval, no command construction
grep -E -- "$normalized" uses -- to prevent option injection from pattern content
normalize_ere uses bash parameter expansion and sed stdin pipe — no code execution from pattern content
grep -E -- "$normalized" </dev/null compile probe — empty input, no data-plane exposure
Catch-all \\[a-zA-Z] loop covers \D/\W/\S, \A/\Z/\z, \b/\B, control escapes, \p/\P, and all other undeclared \letter forms
Compile probe on glibc ERE catches PCRE (?i:…), (?>…), (?#…) as defense-in-depth
Double-backslash guard (\\ in pattern → return 0) prevents \\d being translated as if it were the \d class
set -uo pipefail throughout; set -euo pipefail in sync script
No secrets, tokens, or credentials referenced
New CI job mirrors established SHA-pinned pattern; correctly placed in ci-status.needs
git show "$base:$manifest"$base from env var; $manifest from hardcoded copies array
Unresolved → exit 1, stdout empty — never falls back to CC default
Team-tracked layer only — local/user-global overlays cannot reach the gate

@kyle-sexton
kyle-sexton force-pushed the feat/913-convention-enforcement-seam branch from 9c089fc to a0d8faf Compare July 22, 2026 02:51
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

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


PR Review: feat(source-control): add machine-readable commit-convention enforcement seam

  • Gather context and check what changed since last review (c75c2fca0d8faf)
  • Read lib/resolve-convention-pattern.sh at current HEAD
  • Read lib/resolve-convention-pattern.test.sh at current HEAD
  • Cross-check all prior findings against current state
  • Compile and post final review

Context

This review covers HEAD a0d8faf — the squash commit that incorporates all prior pushes to this branch. The code at this HEAD is identical to c75c2fc reviewed in the prior cycle. All blocking and important findings from the eight prior reviews are confirmed resolved. Five minor polish items remain unchanged.


Resolved findings (confirmed at HEAD)

Prior finding Status
\D/\W/\S negated shorthands → silent mis-enforcement ✅ Fixed — \\[a-zA-Z] loop rejects all non-normalizable \letter escapes
\A/\Z/\z absolute anchors → silent mis-enforcement ✅ Fixed — same catch-all loop
\t/\n/\r/\f/\v control escapes → silent mis-enforcement ✅ Fixed — same loop; test at L96–99
\\d (escaped backslash) → mistranslated by normalize_ere ✅ Fixed — double_backslash check at L114–115; test at L101–104
\d/\w/\s inside bracket expressions → broken double-nested ERE ✅ Fixed — bracket_shorthand regex at L121–122
No compile-time ERE validation — exit 0 could mean invalid ERE ✅ Fixed — grep -E -- "$normalized" </dev/null probe at L184–188

The (?i:…), (?>…), and (?#…) PCRE constructs have no \letter sequences so they pass the \\[a-zA-Z] loop, but the compile probe catches them — grep -E returns exit 2 on all three and the resolver correctly exits 1. The exit-0 contract is upheld.

The Codex c75c2fc finding about \\d (escaped backslash before d) is also resolved: the double_backslash check at L114–115 rejects any pattern containing \\ before normalization runs, and the test at L101–104 covers it directly.


Commit message accuracy note

The commit message says "19 tests." The test file now contains 26 assertions (the count grew across the fix iterations). The final ran $CASE_NUM cases output will print 26, not 19. The contract is solid; the description is stale.


Open findings — all minor, none blocking

1. Unnecessary shellcheck disable=SC2016 — misleading comment

File: lib/resolve-convention-pattern.sh:51

# shellcheck disable=SC2016  # backticks are a literal part of the deferral marker, not a substitution
readonly PR_DEFERRAL='Same as `subject_pattern`.'

SC2016 fires on backticks inside double-quoted strings. PR_DEFERRAL uses single quotes — ShellCheck cannot trigger SC2016 here regardless. The directive has no effect and will mislead future readers into thinking it was load-bearing. Raised in every prior review; still present.

Fix this →


2. Dangling — see note in newrepo helper comment

File: lib/resolve-convention-pattern.test.sh:36

#   newrepo "<team body>" [local-body] [does NOT create user-global — see note]

No "note" exists in this file. The reason is clear from context (enforcement reads the team-tracked layer only, so a user-global overlay is irrelevant to test), but the dangling forward-reference will confuse a future editor. Raised in every prior review; still present.

Suggested replacement:

#   newrepo "<team body>" [local-body]  (no user-global: enforcement reads team-tracked layer only)

Fix this →


3. Raw GitHub URL in config-resolution.md — serves source, not rendered page

File: plugins/source-control/reference/config-resolution.md:94

[commit-convention enforcement seam](https://raw.githubusercontent.com/melodic-software/claude-code-plugins/main/docs/conventions/commit-convention/README.md)

raw.githubusercontent.com serves Markdown source text, not the rendered view. It also hardcodes main. A relative path renders correctly in GitHub and is branch-agnostic (file is 3 levels up from plugins/source-control/reference/):

[commit-convention enforcement seam](../../../docs/conventions/commit-convention/README.md)

Raised in every prior review; still present.

Fix this →


4. PCRE inline flags reach the compile probe — diagnostic quality gap

File: lib/resolve-convention-pattern.sh:103–135

(?i:feat|fix), (?i), (?>…), (?#…) contain no \letter sequences, so the \\[a-zA-Z] loop exits cleanly and is_non_ere returns 1. normalize_ere does not transform (?i: (only (?: matches exactly). The compile probe at L184 catches them — grep -E returns exit 2 — and the resolver correctly exits 1 with no enforcement. The safety contract is maintained.

The only gap is diagnostic quality: consumers see:

resolve-convention-pattern: subject_pattern does not compile as POSIX ERE after normalization; enforcement disabled.

rather than the specific is_non_ere message that names the construct and explains how to fix it. Adding explicit checks:

# PCRE inline flags / atomic group / comment — no ERE equivalent
[[ "$p" == *'(?i'* || "$p" == *'(?m'* || "$p" == *'(?s'* || "$p" == *'(?x'* ]] && return 0
[[ "$p" == *'(?>'* || "$p" == *'(?#'* ]] && return 0

A test case for at least one inline-flag pattern would additionally confirm compile-probe coverage for this class.

Fix this →


5. Missing test: pr_title_pattern deferral when subject_pattern is the CC keyword

File: lib/resolve-convention-pattern.test.sh:136–138

The deferral test (L136–138) uses a custom ERE (^ABC-[0-9]+: .+). The path where pr_title_pattern: Same as \subject_pattern`.defers to aConventional Commitskeywordsubject_pattern` — exercising CC keyword expansion through the deferral branch — is untested. This is the most likely real-world consumer config.

r="$(newrepo $'## subject_pattern\nConventional Commits\n\n## pr_title_pattern\nSame as `subject_pattern`.')"
assert_eq "pr deferral + CC keyword -> CC_ERE" "$CC_ERE" "$(run "$r" pr_title_pattern)"

Fix this →


What's solid

  • General \\[a-zA-Z] catch-all in is_non_ere is the right abstraction — allowlist (\d/\w/\s are normalizable; everything else is rejected) is more robust than a blocklist, and any future PCRE \letter escape is covered without a code change.
  • double_backslash guard at L113–115 prevents \\d (literal backslash + d, not the PCRE class) from being mistranslated by normalize_ere. Test at L101–104 covers it.
  • bracket_shorthand guard at L121–122 correctly uses variable indirection to hold the regex — avoids [^]] closing the [[ ]] prematurely. Covers both lowercase and uppercase shorthand escapes.
  • Compile probe (grep -E -- "$normalized" </dev/null) makes exit-0 a meaningful guarantee. The (($? >= 2)) idiom correctly distinguishes "no match / exit 1" (valid ERE, no enforcement) from "compile error / exit 2+" (invalid ERE, enforce nothing). Covers PCRE inline flags and any future detection gap.
  • Policy-floor: h2_value reads TEAM_FILE only; [[ -n "$value" ]] || exit 1 covers both absent-file and absent-key; test at L130–134 explicitly verifies a local overlay can never enforce.
  • CC keyword expansion in one place: raw_value owns the Conventional CommitsCC_ERE expansion; no per-hook re-definition possible.
  • CI job structure: SHA-pinned checkout, persist-credentials: false, BASE_REF via env var (no script-injection), fetch-depth: 0 for bump checking, correctly wired into ci-status.needs.
  • ${copies[@]+"${copies[@]}"} empty-array idiom throughout sync script — correct under set -u when the array is intentionally empty.

Summary

All blocking and important issues identified across eight prior reviews are resolved at HEAD a0d8faf. The five open items above are cosmetic (1, 2, 3), a diagnostic-quality improvement (4), and a test coverage gap (5). The seam contract — exit 0 = valid enforceable POSIX ERE; exit 1 = no enforcement; never CC fallback; team-tracked layer only — is correctly implemented and safe for #914/#919 to vendor.

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

ℹ️ 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/resolve-convention-pattern.sh Outdated
Comment thread lib/resolve-convention-pattern.sh Outdated
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

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


Security Review — PR #925

  • Load tools
  • Get PR diff and read changed files
  • Review lib/resolve-convention-pattern.sh (core library)
  • Review lib/resolve-convention-pattern.test.sh
  • Review scripts/sync-resolve-convention-pattern.sh
  • Review .github/workflows/ci.yml changes
  • Review documentation changes
  • Post final security findings

Summary

No CRITICAL or IMPORTANT findings. All IMPORTANT vulnerabilities raised across the eight prior security reviews remain fixed at HEAD a0d8faf. One SUGGESTION-level finding is new to this review — not previously raised by Claude or Codex — and one was first flagged by the Codex reviewer at this same commit but not in a prior Claude review.


Prior findings — confirmed still fixed

All fixes introduced through the 31d182fc75c2fc iteration remain present at HEAD:

Prior finding Fix at HEAD
\D/\W/\S negated shorthands → literal-character mis-enforcement Catch-all grep -oE '\\[a-zA-Z]' loop (L128–134): any \letter outside the \d/\w/\s allowlist → return 0
\A/\Z/\z, \b/\B, control escapes \t/\n/\r/\f/\v Same loop
\p/\P Unicode property escapes Same loop — \p hits *) return 0
\d/\w/\s inside bracket expressions → broken double-nested ERE bracket_shorthand regex '\[[^]]*\\[dwsDSW]' (L121–122)
\\d (escaped backslash + d) → mistranslated by normalize_ere Double-backslash guard (L113–115)
No compile-time ERE validation — exit 0 could mean invalid ERE grep -E -- "$normalized" </dev/null probe (L184–188)

PCRE inline flags ((?i:…), (?>…), (?#…)) bypass explicit is_non_ere detection but are caught by the compile probe (grep -E exits 2 on all of them on the ubuntu-24.04/glibc runner) — exit-1 outcome is correct.


Findings

SUGGESTION — (?: normalization is bracket-expression-blind, producing a valid-but-stricter ERE with exit 0 | confidence: medium

File: lib/resolve-convention-pattern.sh L142

p="${p//'(?:'/'('}"

This bash parameter expansion replaces every occurrence of (?: with ( unconditionally, including inside bracket expressions where (?: carries no grouping semantics — it is three literal characters.

Concrete scenario: A team configures:

## subject_pattern
^[a-z(?:]+: .+

(Intended: accept subjects whose first token starts with any lowercase letter, (, ?, or :.)

is_non_ere returns 1 (no \letter escapes, no lookaround, no backreference — all checks pass). normalize_ere replaces (?: with (, producing ^[a-z(]+: .+. The compile probe at L184 succeeds (grep -E accepts valid ERE), so the resolver exits 0 and emits ^[a-z(]+: .+. A consuming hook now enforces a pattern that rejects subjects containing ? or : as the leading character — stricter than configured, silently, with no diagnostic.

Failure direction is strict, not permissive: enforcement blocks commits the configured pattern allows; it does not pass commits the pattern should block. This means there is no security bypass — the gate cannot be gamed using this path. The risk is false positives (legitimate commits rejected) and diagnostic opacity (the consumer sees enforcement active but the emitted pattern differs from what they wrote).

Fix options:

  1. Add bracket-expression context tracking before the (?:( substitution (complex; would require the same variable-held regex approach used for bracket_shorthand).
  2. Detect any (?: that appears between an unescaped [ and its closing ] and classify the pattern as non-enforceable (simpler; consistent with the bracket_shorthand treatment of \w inside brackets).
  3. Document the known limitation in the header comment so consumers know to avoid [(?:…] patterns.

Fix this →


SUGGESTION — bracket_shorthand regex is defeated by POSIX character classes preceding a shorthand escape | confidence: low

File: lib/resolve-convention-pattern.sh L121

local bracket_shorthand='\[[^]]*\\[dwsDSW]'

[^]]* matches any run of non-] characters. For a pattern like ^[[:alpha:]\d]+$, the outer [ starts the match; [^]]* then consumes [, :, a, l, p, h, a, : before hitting the ] that closes the inner POSIX class [:alpha:]; at that point \d follows immediately and the regex matches — so [[:alpha:]\d] is correctly detected and rejected.

However, for ^[[:alpha:][:digit:]\d]+$ (two POSIX classes before the shorthand), the first ] that stops [^]]* is the one closing [:alpha:], which leaves the cursor before [:digit:]\d. If \d appears only after a second POSIX class [:digit:], the check correctly detects it because [^]]* just stops at the first ] and the next chars would be [:digit:]\d — the regex would still need to advance past [:digit:] to reach \d, and since [^]]* already stopped, the pattern \\[dwsDSW] would need to match immediately after the first ], which it won't. So the check would not detect \d in [[:alpha:][:digit:]\d]+.

Concrete scenario: Pattern ^[[:alpha:][:digit:]\d]+$ — the bracket_shorthand check misses the \d inside the bracket expression. normalize_ere then replaces \d with [0-9], producing ^[[:alpha:][:digit:][0-9]]+$. On most ERE engines this is syntactically valid (though behaviorally redundant with [:digit:]), so the compile probe passes, and exit 0 is returned. The emitted pattern is functionally equivalent to the intended one on most platforms, so the failure mode is benign in this particular case (redundant but correct).

For a case that matters: ^[[:upper:]\W]+$ (PCRE: uppercase or non-word) — \W is caught by the catch-all \\[a-zA-Z] loop in is_non_ere, so this correctly exits 1 regardless of the bracket context. The bracket_shorthand check is defense-in-depth for \d/\w/\s; the catch-all loop already prevents \D/\W/\S.

The practical risk is limited: the case where the bracket_shorthand check is defeated (multiple POSIX classes before a normalizable shorthand) leads to a redundant substitution that the compile probe passes, producing correct (if verbose) ERE. Not a bypass.

Fix this →


Confirmed clean

Pattern Verdict
pull_request trigger (not pull_request_target)
${{ github.base_ref }} via env: indirection, not direct run: interpolation
"origin/$BASE_REF" in git commands — prefixed; branch names cannot inject options through double-quoted shell variable expansion
actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 SHA-pinned
persist-credentials: false on checkout
Workflow-level permissions: contents: read; new job inherits, no override
copies array hardcoded (currently empty) — not user-controllable
key validated to subject_pattern|pr_title_pattern before awk -v key=
User file content ($p) flows through printf '%s' | sed and printf '%s' | grep data paths only — no eval, no command construction
grep -E -- "$normalized" uses -- to prevent option injection from pattern starting with -
normalize_ere uses bash parameter expansion and sed stdin pipe — no code execution from pattern content
grep -E -- "$normalized" </dev/null compile probe — empty input, no data-plane exposure, exit-code-only signal
Catch-all \\[a-zA-Z] loop covers \D/\W/\S, \A/\Z/\z, all control escapes, \p/\P, and all other undeclared \letter forms
Compile probe on glibc ERE catches (?i:…), (?>…), (?#…) as defense-in-depth
Double-backslash guard prevents \\d from being translated as if it were the \d shorthand class
git show "$base:$manifest"$base from env var; $manifest from hardcoded copies array
set -uo pipefail throughout; set -euo pipefail in sync script
No secrets, tokens, or credentials referenced
New CI job mirrors established SHA-pinned pattern; correctly placed in ci-status.needs
Unresolved → exit 1, stdout empty — never falls back to CC default
Team-tracked layer only — local/user-global overlays cannot reach the gate

…ent seam

Seams-first foundation for hard-gating commit subjects and PR titles (#914,
#919): a shared, zero-dependency resolver that reads the effective enforcement
pattern from the consumer's tracked convention and hands hooks a POSIX-ERE regex.

- lib/resolve-convention-pattern.sh + 19 tests: team-tracked-layer-only reads
  (policy-floor by construction), ERE dialect normalization with non-ERE
  rejection, CC-keyword expansion owned in one place, unresolved -> no
  enforcement (never the bundled Conventional Commits default).
- scripts/sync-resolve-convention-pattern.sh + CI job: the sync scaffold the
  guardrails consumers (#914, #919) register into.
- docs/conventions/commit-convention/: concern owner-doc (enforcement vs
  drafting), registered in the PLUGIN-PHILOSOPHY convention registry.
- config-resolution.md: drafting-vs-enforcement reconciliation section.
- consumer-config-layering: correct the stale source-control row (all-three
  layering landed in #660; drop the in-flight #647 note).
- MIGRATION-PLAYBOOK: codify swim-lane execution.

Refs #913

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 5m 15s —— View job


Security Review — PR #925

  • Load tools
  • Get PR diff at current HEAD (c93de64)
  • Review lib/resolve-convention-pattern.sh
  • Review lib/resolve-convention-pattern.test.sh
  • Review scripts/sync-resolve-convention-pattern.sh and CI changes
  • Cross-check against prior security findings and determine what's new
  • Post final security findings

Context

This review covers HEAD c93de64. This commit is a squash of all prior iterations on this branch. The code is a substantial redesign relative to the versions reviewed in the eight prior security reviews (e271002934e78775a97f531d182fc75c2fc/9c089fc/a0d8faf). The primary change: normalize_ere is completely removed. The previous approach attempted PCRE→ERE translation via sed; the new approach is reject, never translate — a pattern that is not already valid POSIX ERE takes the no-enforcement path unconditionally.


Prior findings — all resolved

Every IMPORTANT finding raised across the prior eight security reviews is confirmed closed in the current HEAD:

Prior finding How resolved
\D/\W/\S negated shorthands silently emit literal-letter mis-enforcement [[ "$p" =~ \\[a-zA-Z0-9] ]] includes uppercase letters — all caught (L117)
\A/\Z/\z absolute anchors silently emit literal-character mis-enforcement Same catch — A, Z, z are in [a-zA-Z0-9]
\b/\B, \t/\n/\r/\f/\v, \p{Lu} — all \letter escapes Same catch — covers [a-zA-Z] exhaustively
\d/\w/\s inside bracket expressions produce wrong double-nested ERE normalize_ere is gone — no sed substitutions run at all
\\d (escaped backslash + d) mistranslated by normalize_ere normalize_ere is gone
(?: normalization bracket-expression-blind (e.g. [a(?:]+) normalize_ere is gone
PCRE inline flags (?i:…), (?>…), (?#…) bypassed explicit is_non_ere detection [[ "$p" == *'(?'* ]] catches ALL (? constructs — (?i:, (?>, (?# all contain (? (L111)
No compile-time ERE validation — exit 0 could mean invalid ERE grep -E -- "$value" </dev/null >/dev/null 2>&1 probe at L156–160

Findings

No CRITICAL or IMPORTANT findings.

SUGGESTION — \\[a-zA-Z0-9] conservatively rejects valid ERE patterns containing \\letter | confidence: low

File: lib/resolve-convention-pattern.sh:117

[[ "$p" =~ \\[a-zA-Z0-9] ]] && return 0

This ERE matches any literal backslash followed by a letter or digit anywhere in the string. A pattern like ^foo\\bar$ — intending to match a literal backslash between foo and bar — contains the subsequence \b (the second backslash of \\ followed by b), which triggers the check. The resolver exits 1 (no enforcement) even though ^foo\\bar$ is valid POSIX ERE.

The failure mode is a false rejection (no enforcement when enforcement was configured), not a false pass. A team whose convention requires a literal backslash in commit subjects gets a silent no-op gate with a diagnostic, not a gate that enforces the wrong thing. Given that commit-subject conventions requiring literal backslashes are extremely rare and the fix (test confirmed at line 107–110) shows this is intentional design, this is a noted limitation rather than an exploitable gap. The header comment should document it explicitly (currently it describes the (?...) case but not the double-backslash case) to avoid consumer surprise.

This is the same class of conservative-rejection behavior described in prior reviews for \\d; the test confirms it's intentional. No security action required.


Confirmed clean

Pattern Verdict
pull_request trigger (not pull_request_target)
${{ github.base_ref }} via env: indirection, not direct run: interpolation
"origin/$BASE_REF" in git commands — quoted shell variable, no option injection
actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 SHA-pinned
persist-credentials: false on checkout
Workflow-level permissions: contents: read; new job inherits, no override
copies array hardcoded (currently empty) — not user-controllable
key validated to subject_pattern|pr_title_pattern before awk -v key=
File content flows through awk data paths only — no eval, no command construction
grep -E -- "$value" uses -- to prevent option injection from pattern content
grep -E -- "$value" </dev/null — empty input, no data-plane exposure
[[ "$p" == *'(?'* ]] — catches ALL PCRE (? constructs including (?i:, (?>, (?#
[[ "$p" =~ \\[a-zA-Z0-9] ]] — catches all \letter/\digit escapes: \d\S, \A\z, \b/\B, \t/\n, \p/\P, \1\9, and any future \letter PCRE escape
Compile probe catches anything that evades string checks (unbalanced brackets, PCRE verbs (*ACCEPT))
normalize_ere removed — entire class of bracket-context translation bugs is gone
set -uo pipefail throughout; set -euo pipefail in sync script
No secrets, tokens, or credentials referenced
New CI job mirrors established SHA-pinned pattern; correctly placed in ci-status.needs
Unresolved → exit 1, stdout empty — never falls back to CC default
Team-tracked layer only — local/user-global overlays cannot reach the gate

Summary

The redesign — reject, never translate — is the right call and closes the entire class of normalization vulnerabilities that occupied prior reviews. The two-check is_non_ere (*'(?'* + \\[a-zA-Z0-9]) combined with the compile probe provides robust, defence-in-depth rejection. No CRITICAL or IMPORTANT findings at this HEAD.

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown

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


PR Review: feat(source-control): add machine-readable commit-convention enforcement seam

  • Load tools
  • Check what changed since last reviewed HEAD (a0d8fafc93de64)
  • Read current lib/resolve-convention-pattern.sh
  • Read current lib/resolve-convention-pattern.test.sh
  • Cross-check all prior findings against current state
  • Post final review

Context

This review covers the squash HEAD c93de64. The last reviewed commit was a0d8faf, which had a significantly more complex implementation. This commit represents a major architectural simplification of lib/resolve-convention-pattern.sh: the entire normalize_ere translation layer has been removed and is_non_ere has been collapsed to two concise checks. This resolves every prior blocking or important finding with a cleaner design.


What changed vs a0d8faf

lib/resolve-convention-pattern.sh was refactored substantially. The prior implementation attempted a normalize-then-reject strategy: translate (?:(, \d[0-9], \w[[:alnum:]_], \s[[:space:]], then reject remaining PCRE-only constructs. This required bracket-expression context tracking, double-backslash guards, and a catch-all grep -oE '\\[a-zA-Z]' loop — producing 60+ lines of logic that still had residual edge cases.

The current implementation takes a reject-only strategy: any pattern containing a PCRE-ism (a (?…) group or a \letter/\digit escape) is rejected outright. A team wanting a digit class writes [0-9], not \d. The result is 13 lines of is_non_ere:

is_non_ere() {
  local p="$1"
  [[ "$p" == *'(?'* ]] && return 0
  [[ "$p" =~ \\[a-zA-Z0-9] ]] && return 0
  return 1
}

Resolved findings (all prior blocking/important items)

Every finding raised across the nine prior reviews is resolved:

Prior finding How resolved
\D/\W/\S negated shorthands silently emit mis-enforcing ERE \\[a-zA-Z0-9] catches all \letter forms
\A/\Z/\z absolute anchors silently emitted Same — \A, \Z, \z all match \\[a-zA-Z0-9]
\t/\n/\r/\f/\v control escapes silently emitted Same — all match \\[a-zA-Z0-9]
\\d (escaped backslash + d) mis-translated by normalize_ere normalize_ere removed; the escaped form also matches \\[a-zA-Z0-9] via the trailing \d (test at L108–110)
\d/\w/\s inside bracket expressions → broken double-nested ERE normalize_ere removed; [\w-] is now caught by \\[a-zA-Z0-9] matching \w (test at L113–115)
PCRE inline flags (?i:…), (?i), (?>…), (?#…) not caught by is_non_ere All caught by *'(?'* — the glob matches any pattern containing the substring (?
\p{Lu} Unicode property escapes passing the compile probe \p matches \\[a-zA-Z0-9]
(?: normalization context-blind inside bracket expressions normalize_ere removed entirely; no normalization occurs
No compile-time ERE validation before emitting with exit 0 Compile probe at L156–160 still present

Open findings — all minor, none blocking

1. Unnecessary shellcheck disable=SC2016 — misleading comment

File: lib/resolve-convention-pattern.sh:51

# shellcheck disable=SC2016  # backticks are a literal part of the deferral marker, not a substitution
readonly PR_DEFERRAL='Same as `subject_pattern`.'

SC2016 fires on backticks inside double-quoted strings. PR_DEFERRAL uses single quotes — ShellCheck cannot trigger SC2016 here regardless of content. The directive has no effect and misleads a reader into thinking suppression was required. Raised in every prior review; still present.

Fix this →


2. Dangling — see note in newrepo helper comment

File: lib/resolve-convention-pattern.test.sh:36

#   newrepo "<team body>" [local-body] [does NOT create user-global — see note]

No "note" exists in this file. The intent is clear from context (enforcement reads the team-tracked layer only, so a user-global overlay is irrelevant to test), but the forward reference will confuse a future editor. Raised in every prior review; still present.

#   newrepo "<team body>" [local-body]  (no user-global: enforcement reads team-tracked layer only)

Fix this →


3. Raw GitHub URL in config-resolution.md — serves source text, not rendered page

File: plugins/source-control/reference/config-resolution.md:94

[commit-convention enforcement seam](https://raw.githubusercontent.com/melodic-software/claude-code-plugins/main/docs/conventions/commit-convention/README.md)

raw.githubusercontent.com serves Markdown source text, not the rendered page. It also hardcodes main, which breaks on forks or if the default branch changes. A relative path renders correctly in GitHub and is branch-agnostic (the file is three levels up from plugins/source-control/reference/):

[commit-convention enforcement seam](../../../docs/conventions/commit-convention/README.md)

Raised in every prior review; still present.

Fix this →


4. Missing test: pr_title_pattern deferral when subject_pattern is the CC keyword

File: lib/resolve-convention-pattern.test.sh:143–144

The deferral test uses a custom ERE (^ABC-[0-9]+: .+) as subject_pattern. The path where pr_title_pattern: Same as \subject_pattern`.defers to aConventional Commitskeywordsubject_pattern— exercising CC keyword expansion *through* the deferral branch — is not covered. This is the most common real-world config combination for consumers of#914/#919`. Raised in every prior review; still absent.

# --- pr_title_pattern deferral + CC keyword expansion ---
r="$(newrepo $'## subject_pattern\nConventional Commits\n\n## pr_title_pattern\nSame as `subject_pattern`.')"
assert_eq "pr deferral + CC keyword -> CC_ERE" "$CC_ERE" "$(run "$r" pr_title_pattern)"

Fix this →


5. Stale test count in commit message and PR description

The commit message says "19 tests" and the PR description says "19/19 pass." The test file now contains 27 assertions (after the fix iterations through prior review rounds). The actual ran N cases output at test completion will print 27, not 19. No code change needed, but the PR description should be updated to reflect the current count before merge.


6. *'(?'* glob may false-positive on literal (? inside bracket expressions — minor DX note

File: lib/resolve-convention-pattern.sh:111

[[ "$p" == *'(?'* ]] matches any pattern containing the substring (?, including inside character classes where ( and ? are literal characters. A pattern like ^[(? ]+: .+ (commits starting with (, ?, or space) is valid POSIX ERE but would be rejected with the "PCRE-only construct" diagnostic. The failure mode is conservative (enforcement disabled, not mis-enforcement), and such a pattern is extremely unlikely in practice. Worth documenting in the header comment if not fixing, so a future author knows the limitation:

# NOTE: *'(?'* catches literal `(?` anywhere, including inside `[...]`.
# A bracket expression like `[(? ]` is valid ERE but will be rejected.
# This is intentional: unambiguous ERE should not look like a PCRE group.

Fix this →


What's solid

  • Reject-only is the right call. Attempting PCRE→ERE translation by string rewriting was the source of every important finding across nine reviews. Removing normalize_ere eliminates an entire class of bugs — incorrectly translated bracket expressions, context-blind substitutions, escaped-backslash ambiguity. The new contract is simpler: the team writes POSIX ERE; if they don't, enforcement is disabled with a clear diagnostic.

  • *'(?'* + \\[a-zA-Z0-9] is comprehensive. The two-check design covers every PCRE-only construct identified across all prior reviews (inline flags, atomic groups, PCRE comments, lookaround, named groups, all shorthand classes, all anchors, control escapes, backreferences, Unicode property escapes) without needing per-construct detection. Any future PCRE \letter or (?...) form is rejected automatically.

  • Compile probe at L156–160 closes the remaining gap. Patterns that pass is_non_ere (no (? sequence, no \letter) but are structurally invalid ERE (e.g., unbalanced ^[$) are caught by grep -E -- "$value" </dev/null. Exit 0 guarantees the emitted pattern is both PCRE-free and ERE-compilable on the CI platform.

  • Policy-floor contract maintained: h2_value reads TEAM_FILE only; unresolved → exit 1, stdout empty; never falls back to CC_ERE (test at L125–129).

  • CC keyword expansion in one place: raw_value owns the Conventional CommitsCC_ERE mapping; no per-hook re-derivation possible.

  • Test isolation: mktemp -d per repo with a trap cleanup; no inter-test state.

  • CI job mirrors the established parse-concern-value-sync pattern: SHA-pinned checkout, persist-credentials: false, BASE_REF via env var, fetch-depth: 0, correct ci-status.needs wiring.


Summary

All blocking and important findings from the prior nine reviews are resolved in the current HEAD. The implementation is significantly simpler and more robust than what those reviews tracked. Six minor open items above — three are cosmetic (1, 2, 3), one is a test gap (4), one is a stale count in the PR description (5), and one is a documentation-only edge case (6). None affect the seam contract or the safety of #914/#919 vendoring this resolver.

@kyle-sexton

Copy link
Copy Markdown
Contributor Author

All codex review threads addressed by the resolver simplification (latest push): the enforcement resolver no longer translates PCRE→ERE (that string-rewriting was the source of every finding — shorthand-in-bracket, POSIX-class brackets, (?: in a class, control/anchor/negated escapes, escaped backslashes). It now accepts POSIX ERE and rejects any PCRE-only construct, plus a compile-validation net, so all those inputs take the documented no-enforcement path (fail-safe) rather than emitting a mistranslated pattern. Resolving these threads as superseded.

@kyle-sexton
kyle-sexton merged commit d115cbf into main Jul 22, 2026
25 checks passed
@kyle-sexton
kyle-sexton deleted the feat/913-convention-enforcement-seam branch July 22, 2026 03:10
kyle-sexton added a commit that referenced this pull request Jul 22, 2026
…file (#1055)

## Summary

The team-tracked `.claude/source-control.md` that `/source-control:setup
apply` writes lands in shared history with no signal for a teammate who
does not run these plugins — audit finding f6 on #912, declared folded
into #913 but never delivered by PR #925 (its file list contains no
setup-skill change).

- The `apply` template now opens with a self-describing preamble: read
by the source-control plugin (and the guardrails commit-convention gate
where installed), inert without them, and a **drafting aid, not
team-wide enforcement** — tool-agnostic enforcement for every committer
is a commit-msg hook or CI check.
- The `apply` report for a team write states the same draft-aid vs
enforcement distinction.
- The header is template-owned: a reconfiguration run rewrites it in
place, never appends a second copy.
- Preamble safety proven, not assumed: a new regression case in
`lib/resolve-convention-pattern.test.sh` shows a preambled file resolves
identically to a bare one (the parse contract reads only the first
non-empty body line under a `## <key>` H2).

source-control `0.17.0` → `0.17.1` with CHANGELOG entry.

## Test plan

- [x] `bash lib/resolve-convention-pattern.test.sh` — 28 cases, 0 failed
(new preamble case included)
- [x] `bash scripts/check-changelog-parity.sh --check-bump main` — pass
- [x] `bash scripts/check-changed-skills.sh main` — setup: PASS (0
errors)

## Related

- Closes #1046
- Refs #912 (audit umbrella, finding f6), #913 (claimed the fold), #925
(seam PR that shipped without it)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 22, 2026
…1071)

## Summary

Audit finding f4 (#912): the tracked convention config enforced nothing
on its own — any direct `git commit` / `gh pr create` path skipped it,
so the file was load-bearing only at skill draft time. This delivers the
CC-layer content gate, the zero-setup half of the f4 design (`#919`'s
opt-in commit-msg hook is the depth layer and ships separately).

**New guard `block-convention-violation.sh`** (PreToolUse,
`Bash|PowerShell`):
- Validates the **commit subject** of the canonical stdin form — first
non-empty line of the Bash heredoc / PowerShell here-string body —
against the team-tracked `subject_pattern`, and the **`gh pr create
--title`/`-t` value** against `pr_title_pattern` (incl. the `` Same as
`subject_pattern`. `` deferral).
- Reads patterns via the **vendored enforcement resolver**
(`resolve-convention-pattern.sh`, synced from `lib/` — first registered
consumer in `scripts/sync-resolve-convention-pattern.sh`, per the
commit-convention seam #913/PR #925).
- **Unresolved = no enforcement** — no team-tracked pattern / non-ERE
pattern → no-op; never gates against the bundled Conventional Commits
default (#912 contract 2).
- **Never blocks `gh pr create` itself** — only a present-and-violating
title; the documented inline fallback stays usable.
- **Inherits `block-noncanonical-commit`'s exemption taxonomy** —
`--amend`, `-C`/`-c`, `--fixup`/`--squash`, `-F <path>`,
sequencer-in-progress — so rebases/merges are never content-gated.
- **Declared bypass coverage** (out of scope, documented in the hook
header): `gh pr edit --title`, `--fill`, direct API calls, babysit
retitles, non-heredoc stdin producers.
- Kill switch `block_convention_gate_enabled` (default true). PowerShell
traffic reduces through the bundled classifier first; unparsable PS
never reaches a content decision here.

guardrails `0.10.3` → `0.11.0` with CHANGELOG entry.

Grounded against the official hooks reference (matcher semantics,
PreToolUse payload): <https://code.claude.com/docs/en/hooks>.

## Test plan

- [x] New 25-case contract suite `block-convention-violation.test.sh` —
unresolved/PCRE no-ops, subject block/allow, full exemption taxonomy,
sequencer, non-heredoc skip, `--title` forms incl. deferral +
edit/out-of-scope, PowerShell here-string forms, kill switch
- [x] Sibling suites unchanged: block-noncanonical-commit 90/0,
block-dangerous-git 251/0, flag-commit-pr-skill-bypass 27/0
- [x] `scripts/sync-resolve-convention-pattern.sh --check` — copy
matches source
- [x] `scripts/check-changelog-parity.sh --check-bump main` — pass
- [x] shellcheck clean

## Related

- Closes #914
- Refs #912 (locked contract), #913 / #925 (seam + resolver), #919
(depth layer, ships last), #960 (PowerShell classifier this gate
composes with)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 22, 2026
…t enforcement (#1077)

## Summary

Audit f1's depth layer / f4's backstop (#912): a git `commit-msg` hook
enforcing the team-tracked subject pattern **regardless of tool or
shell** — editor commits, `git commit -F <file>`, IDE integrations,
humans outside Claude — where the Claude-Code-layer gates see nothing.

**New installable template** `lib/git-hooks/commit-msg-convention.sh` +
**`/guardrails:setup apply install-commit-msg`** (explicit opt-in; bare
`apply` still writes nothing):

- **Chain-or-refuse.** Managed repos (`core.hooksPath`, lefthook, husky,
pre-commit) → refused with the manager-side remediation. An existing
`commit-msg` hook is never overwritten: chained as
`commit-msg.pre-guardrails` (runs first, its rejection is final) or the
install refuses. Covers the operator's machine-local commit-msg gate.
- **Sentinel-marked** (`guardrails-commit-msg-convention`) so
convention-inference tooling excludes the installed hook as a signal —
it is derived FROM the tracked config; counting it would echo-cycle.
Sentinel re-install is idempotent.
- **Personal `.git/hooks/` lane only.** `core.hooksPath`, hook-manager
configs, and tracked files are never touched — the committed team lane
is a human PR decision, and `core.hooksPath` changes are exactly the
shape `block-no-verify` refuses (interaction resolved by not going
there).
- **Unresolved = no enforcement** (never the bundled CC default);
resolver copy removed → fail open, never block blind;
`fixup!`/`squash!`/`amend!` exempt (autosquash).
- **Deadlock designed out.** The rejection message instructs fixing the
subject and never suggests `--no-verify` (which `block-no-verify`
refuses in-session anyway); in Claude sessions the CC-layer gate
(#914/PR #1071) blocks first, making this hook the cross-tool backstop.

Reads the same resolver contract as the CC-layer gate (copied beside the
hook at install time — an installed consumer-repo hook cannot
participate in the sync seam, so it carries its own unedited copy).

guardrails `0.11.0` → `0.12.0` with CHANGELOG entry.

## Test plan

- [x] New 15-case contract suite
`lib/git-hooks/commit-msg-convention.test.sh` — unresolved/PCRE
pass-through, enforce block/allow, comment-line skipping, autosquash
exemptions, empty message, resolver-removed fail-open, chain
rejection-final + pass-through, sentinel presence
- [x] `scripts/check-changed-skills.sh main` — setup skill PASS (0
errors)
- [x] `scripts/check-changelog-parity.sh --check-bump main` — pass
- [x] shellcheck clean; exec bits set

## Related

- Closes #919
- Refs #912 (locked contract), #913/#925 (resolver seam), #1071
(CC-layer gate this backstops), #915/#960 (tool-bypass class this closes
for non-CC surfaces)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.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.

feat(source-control): convention parse-contract + shared enforcement resolver seam

1 participant