What's broken
split_segments() gives every Segment in a compound command (a && b, a; b, ...) the exact same raw text: the whole input string, not that segment's own text. guard_commit_trailer scans seg.raw for a Co-Authored-By: trailer, so a git commit with a completely clean message gets denied if any other segment on the same line happens to mention that phrase (a comment, a grep, an echo explaining the rule).
Which layer
tools/agent-guard/src/agent_guard/__init__.py, Segment.__init__ / split_segments() (around line 137 and 155-172). Segment's own docstring says raw is "the original text of the segment", but split_segments constructs every segment as Segment(current, command), passing the full, unsliced input each time.
How to reproduce
python3 -c "
import sys; sys.path.insert(0, 'tools/agent-guard/src')
from agent_guard import dispatch
print(dispatch('echo \"note: never add a Co-Authored-By: trailer\" && git commit -m \"clean fix, no trailer here\"'))
"
Expected vs actual
Expected: None (allow), since the commit's own message has no trailer.
Actual: the commit-trailer deny string, because seg.raw for the git commit segment is the entire compound command, including the unrelated echo segment's text.
Surface area
GuardContext.raw exposes the same field to skill-contributed guards.d guards, but a repo-wide grep shows only the bundled guard_commit_trailer currently reads .raw, so the fix's blast radius is that one guard.
What's broken
split_segments()gives everySegmentin a compound command (a && b,a; b, ...) the exact samerawtext: the whole input string, not that segment's own text.guard_commit_trailerscansseg.rawfor aCo-Authored-By:trailer, so agit commitwith a completely clean message gets denied if any other segment on the same line happens to mention that phrase (a comment, a grep, an echo explaining the rule).Which layer
tools/agent-guard/src/agent_guard/__init__.py,Segment.__init__/split_segments()(around line 137 and 155-172).Segment's own docstring saysrawis "the original text of the segment", butsplit_segmentsconstructs every segment asSegment(current, command), passing the full, unsliced input each time.How to reproduce
Expected vs actual
Expected:
None(allow), since the commit's own message has no trailer.Actual: the commit-trailer deny string, because
seg.rawfor thegit commitsegment is the entire compound command, including the unrelatedechosegment's text.Surface area
GuardContext.rawexposes the same field to skill-contributedguards.dguards, but a repo-wide grep shows only the bundledguard_commit_trailercurrently reads.raw, so the fix's blast radius is that one guard.