Description
The external-comms PreToolUse gate leak-scans a git commit message by extracting it from the Bash tool command with a regex list, and it stops at the first match. Git's own semantics are cumulative: every -m becomes a paragraph of one concatenated message. So on git commit -m "subject" -m "body para 1" -m "body para 2", only subject reaches the confidential-information review. Every paragraph after the first is committed unscanned.
Source, hooks/external-comms-gate.sh v0.18.6. The extraction runs in a python3 heredoc embedded in the shell script, so the line numbers below are file lines, not lines of a separate .py:
- Line 243, the pattern-list annotation:
# (pattern, flags, unescape) - first match wins.
- Lines 260 and 261, the single-line commit-message patterns:
(?:-m|--message)[= ]'([^']*)' and (?:-m|--message)[= ]"([^"]*)".
- Lines 263, 264 and 270, the extraction loop:
for pat, flags, unescape in patterns: / m = re.search(pat, cmd, flags) / break.
re.search returns the first match only, and the break exits after the first matching pattern. Neither the loop nor the patterns account for a command carrying more than one -m.
This is security-relevant. The gate exists to keep confidential content out of a public commit history, and the body is exactly where the long-form prose most likely to carry it lives. The subject line is the one part of a commit message least likely to contain a leak, and it is the only part currently checked.
The single--m HEREDOC form is unaffected: the HEREDOC pattern is tried first and captures the whole body. Only the multi--m form loses coverage.
Symptoms
- A
git commit with two or more -m arguments passes the external-comms gate on the strength of its subject line alone.
- Confidential content placed in any
-m after the first is committed without a leak review.
- The failure is silent in both directions. The gate reports PASS, and nothing records that only a fraction of the message was examined, so there is no audit trace of the partial scan.
Workaround
Write commit messages as a single -m argument with embedded newlines, or as a single HEREDOC. The whole message is then the first and only match, so the gate scans all of it.
Affected plugin or component
@windyroad/risk-scorer
Frequency
Every commit authored with more than one -m, which is a common shape for both agents and humans writing a subject plus a multi-paragraph body.
Local plugin version
@windyroad/risk-scorer@0.18.6
Upstream package version
not applicable
Claude Code CLI version
2.1.219 (Claude Code)
Node version
v24.16.0
Operating system
Darwin 25.3.0 x86_64
Evidence
Reproduction:
- In a repo with the external-comms gate active, run
git commit -m "chore: touch" -m "<planted sentinel the gate should reject>".
- The gate reviews
chore: touch and returns PASS. The sentinel in the second -m is never seen.
- Run the same content as a single
-m with an embedded newline. The gate now sees the sentinel and routes it through review.
Candidate fix direction, triage's call: collect every match rather than the first. re.findall over the two commit-message patterns, joined with the blank-line separator git itself inserts, would reconstruct the message git actually writes. The break-on-first-pattern behaviour is correct for choosing between the HEREDOC, --body, and -m families, so the change is narrower than it looks: keep first-family-wins, but make the -m family collect all its occurrences.
A reproduction test alongside the fix would fit hooks/test/external-comms-gate.bats.
Additional context
Related, same gate, three distinct failure modes that do not overlap:
Worth co-triage with #368: both are false-negative coverage gaps in the same gate, and a fix for one is a natural place to add a test for the other.
Cross-reference
Reported from https://github.com/mountain-pass/addressr/blob/master/docs/problems/open/064-external-comms-commit-gate-scans-only-first-m-value.md
This issue is tracked locally as P064 in the downstream project's docs/problems/ directory.
Description
The external-comms PreToolUse gate leak-scans a
git commitmessage by extracting it from the Bash tool command with a regex list, and it stops at the first match. Git's own semantics are cumulative: every-mbecomes a paragraph of one concatenated message. So ongit commit -m "subject" -m "body para 1" -m "body para 2", onlysubjectreaches the confidential-information review. Every paragraph after the first is committed unscanned.Source,
hooks/external-comms-gate.shv0.18.6. The extraction runs in apython3heredoc embedded in the shell script, so the line numbers below are file lines, not lines of a separate.py:# (pattern, flags, unescape) - first match wins.(?:-m|--message)[= ]'([^']*)'and(?:-m|--message)[= ]"([^"]*)".for pat, flags, unescape in patterns:/m = re.search(pat, cmd, flags)/break.re.searchreturns the first match only, and thebreakexits after the first matching pattern. Neither the loop nor the patterns account for a command carrying more than one-m.This is security-relevant. The gate exists to keep confidential content out of a public commit history, and the body is exactly where the long-form prose most likely to carry it lives. The subject line is the one part of a commit message least likely to contain a leak, and it is the only part currently checked.
The single-
-mHEREDOC form is unaffected: the HEREDOC pattern is tried first and captures the whole body. Only the multi--mform loses coverage.Symptoms
git commitwith two or more-marguments passes the external-comms gate on the strength of its subject line alone.-mafter the first is committed without a leak review.Workaround
Write commit messages as a single
-margument with embedded newlines, or as a single HEREDOC. The whole message is then the first and only match, so the gate scans all of it.Affected plugin or component
@windyroad/risk-scorer
Frequency
Every commit authored with more than one
-m, which is a common shape for both agents and humans writing a subject plus a multi-paragraph body.Local plugin version
@windyroad/risk-scorer@0.18.6
Upstream package version
not applicable
Claude Code CLI version
2.1.219 (Claude Code)
Node version
v24.16.0
Operating system
Darwin 25.3.0 x86_64
Evidence
Reproduction:
git commit -m "chore: touch" -m "<planted sentinel the gate should reject>".chore: touchand returns PASS. The sentinel in the second-mis never seen.-mwith an embedded newline. The gate now sees the sentinel and routes it through review.Candidate fix direction, triage's call: collect every match rather than the first.
re.findallover the two commit-message patterns, joined with the blank-line separator git itself inserts, would reconstruct the message git actually writes. Thebreak-on-first-pattern behaviour is correct for choosing between the HEREDOC,--body, and-mfamilies, so the change is narrower than it looks: keep first-family-wins, but make the-mfamily collect all its occurrences.A reproduction test alongside the fix would fit
hooks/test/external-comms-gate.bats.Additional context
Related, same gate, three distinct failure modes that do not overlap:
wr-risk-scorer-restage-commit-wrapped commits never reach the gate at all. That issue is about which invocations are seen. This one is about how much of the message is read once an invocation is seen. Fixing either leaves the other open.-mversus HEREDOC in passing, but as a source of hash churn, not as a coverage gap.Worth co-triage with #368: both are false-negative coverage gaps in the same gate, and a fix for one is a natural place to add a test for the other.
Cross-reference
Reported from https://github.com/mountain-pass/addressr/blob/master/docs/problems/open/064-external-comms-commit-gate-scans-only-first-m-value.md
This issue is tracked locally as P064 in the downstream project's
docs/problems/directory.