fix(approvals): drop quoted free-text operands from stored approval patterns (#1406) - #1815
Merged
Merged
Conversation
…atterns (#1406) A single-line quoted operand flowed verbatim into the stored shell approval pattern. Every unique value became a new pattern. The gate re-prompted for each one. `git commit -m "fix the bug"` and `freshdesk ticket reply --message "Single line body"` are two examples. The existing termination rules did not fire. The digit rule needs a digit in the token. The #1402 rule needs an embedded line break. This change adds a third rule to ReconstructClauseText. A quote-wrapped arg whose decoded text holds internal whitespace is call-specific free text. The rule drops it from the normalized pattern. It runs beside the digit (#1331) and multi-line (#1402) rules. The rule is conservative. A single-word quoted arg has no internal whitespace, so the pattern keeps it. `git commit -m "fix"` still keeps `fix`, and a quoted and an unquoted single token normalize the same way. Only a multi-word quoted value operand drops. Security scope: - The change shapes the stored pattern and the approve-retry key only. It does not change the live authorization decision. That decision re-parses each command and scopes every path arg through the zone gate. - The drop applies to value tokens only. A path arg is exempt. A quoted path with a space (`cat "my file.txt"`) keeps its directory scope, which ExtractCandidates resolves from the same parsed arg. - Wrapper verbs are unaffected. The analyzer expands `bash -c` and `sh -c` command strings into inner clauses before this walk. Prefix wrappers such as `env` and `timeout` keep their existing termination behavior.
Aaronontheweb
added a commit
that referenced
this pull request
Aug 8, 2026
…-gates (#1406) (#1816) Reconcile the spec with the behavior that PR #1815 merged. Add the quoted-free-text termination rule to the "Shell command pattern matching" requirement. The rule sits next to the digit-bearing rule and the multi-line rule. A quoted argument with internal whitespace is call-specific free text and stops pattern extraction. Add four scenarios that pin the shipped constraints: - a multi-word quoted operand drops from the pattern - multi-word quoted operands generalize across values - a single-word quoted argument stays in the pattern - a quoted path keeps directory scoping Archive the completed change under openspec/changes/archive/.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A single-line quoted operand flowed verbatim into the stored shell approval
pattern. Every unique value became a new pattern. The gate re-prompted for each
one. Two examples:
git commit -m "fix the bug"keptfix the bugin the pattern, so everycommit message re-prompted.
freshdesk ticket reply --message "Single line body"kept the body.The existing termination rules did not fire.
IsCallSpecificValueTokenneeds adigit in the token. The #1402 rule needs an embedded line break.
The fix (predicate 1)
The change adds one rule to
ReconstructClauseText. A quote-wrapped arg whosedecoded text holds internal whitespace is call-specific free text. The rule
drops it from the normalized pattern. It runs beside the digit (#1331) and
multi-line (#1402) rules. The composition is
digit OR multi-line OR whitespace-quoted.Predicate 1 is the conservative choice. A single-word quoted arg has no
internal whitespace, so the pattern keeps it.
git commit -m "fix"still keepsfix, and a quoted and an unquoted single token normalize the same way. Only amulti-word quoted value operand drops.
Security reasoning
key only. It does not change the live authorization decision. That decision
re-parses each command and scopes every path arg through the zone gate.
FormatForDisplaystill shows the full command to the operator, because asingle-line command has no line break and takes the verbatim fast path.
parser marks a real path
IsPath, and the rule exempts it. A quoted pathwith a space (
cat "my file.txt") keeps its directory scope, whichExtractCandidatesresolves from the same parsed arg. A quoted searchpattern (
grep "foo bar" ./notes.txt) drops, and the trailing path stillscopes the candidate.
bash -candsh -ccommand strings into inner clauses before this walk, so the rule never hides
an inner command from the gate. Prefix wrappers such as
envandtimeoutkeep their existing termination behavior.
Tests
New cases in
ShellApprovalMatcherTests:git commit -m "fix the bug"normalizes togit commit -m.freshdesk ticket reply --message "Single line body"drops the body.git commit -m "fix"keepsfix(single-word quoted).find . -name "*.cs"keeps the glob (no internal whitespace).cat "my file.txt"keeps its directory scope inExtractCandidates.grep "foo bar" ./notes.txtkeeps the path scope and ignores the free text.FormatForDisplayshows the full command for a single-line message.No existing test needed a normalization update.
git commit -m fixandbash -c "git push --force"stay unchanged, because neither carries amulti-word quoted value operand.
Validation
dotnet test src/Netclaw.Security.Tests— 678 passed, 0 failed, 0 skipped.Netclaw.Actors.Tests— 437passed, 0 failed.
dotnet slopwatch analyze— 0 issues.Add-FileHeaders.ps1 -Verify— all files have headers.Spec follow-up
The
openspec/specs/tool-approval-gates/spec.md"Shell command patternmatching" requirement defines call-specific classification as one
morphological rule (digit-bearing). This change adds the quoted-free-text
rule. Reconcile the spec through
/opsx-sync. Do not hand-edit it.