Skip to content

fix(execpolicy): distinguish shell syntax from exact grant data - #6758

Merged
Hmbown merged 11 commits into
mainfrom
fix/shell-grant-syntax
Sep 29, 2026
Merged

Hmbown merged 11 commits into
mainfrom
fix/shell-grant-syntax

Conversation

@Hmbown

@Hmbown Hmbown commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

No-Issue: verified shell grant matching findings from the current audit.

Depends on #6732 and preserves its original history. Land that PR first; this is its bounded five-path follow-up. Targets main so the normal hosted workflows run; the dependency remains required.

Shell grants now distinguish actual control operators and redirections from quoted argument data using the existing scanner. Broad grants share checks for unresolved arguments and known write-producing argument forms across trusted prefixes, typed rules and TOML rules. Exact saved grants retain literal command and workspace identity. File permissions and deny precedence remain intact.

Validation: 248 focused Rust tests passed (228 library, 1 authorization-order, 11 redirection, 8 parsing; 0 failed, 0 ignored). npm test: 636 passed; npm run check:web: passed. Root and independent source reviews, formatting and diff checks passed. The first gate found five classification/file-permission regressions; narrow repairs retain all five original tests and add a mixed file/shell regression. Hosted checks remain required.

Hmbown and others added 11 commits September 28, 2026 23:42
…fter -c

`bash -c -e 'cmd'`, `sh -c -- 'cmd'` and `bash -c -o pipefail 'cmd'` run
`cmd`: options may follow `-c`, and the command string is the first operand
once option parsing ends. The expander took the word right after `-c`, so a
deny rule was matched against `-e` / `--` and the real command went
unchecked. `shell_input` now keeps scanning options after `-c` (honouring
`-o`/`-O` values, `--` and `-`) and reports the first operand, plus the word
right after `-c` for shells that read it directly. The command-safety
classifier's `-c` peel uses the same rule.

Tests: execpolicy unit + integration suites green (unit 222 passed; integration 1 + 5 + 7
passed; doc 1 passed); the new shell_parse_policy entries fail on origin/main.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks
…time input

`bash /dev/stdin <<< 'cmd'`, `echo cmd | sh /proc/self/fd/0` and
`. /dev/stdin <<< 'cmd'` read their script from stdin exactly like
`bash <<< 'cmd'` or `bash -s`, which the expander already reports as
dynamic. With a script operand present it was treated as an opaque file, so
deny rules were never consulted. A shell or `source`/`.` operand naming
`/dev/stdin`, `/dev/fd/N` or `/proc/<pid|self>/fd/N` is now dynamic, unless
it is stdin and a heredoc body (which is expanded as the script) feeds it.

Tests: execpolicy suites green (unit 223 passed; integration 1 + 5 + 7
passed; doc 1 passed); the new shell_parse_policy entries fail without the
change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks
Deny rules were matched against the launcher instead of the command it
runs for `pkexec`, `run0`, `fakeroot`, `taskset`, `chrt`, `prlimit`,
`strace`, `ltrace`, `systemd-run`, `numactl`, `firejail`, `xvfb-run`,
`dbus-launch`, `dbus-run-session`, `sg`, `proxychains`, `torsocks`,
`eatmydata`, `cpulimit`, `setpriv`, `trickle` and the macOS GNU coreutils
spellings (`gtimeout`, `gnice`, `gnohup`, `gstdbuf`, `gchroot`), so
`pkexec rm -rf /` or `taskset -c 0 git push` passed a matching deny rule.
They are now entries in the existing wrapper table (with their option and
operand grammar; unknown options are still read both ways), and `sg GROUP
[-c] 'cmd'` hands its joined operands to the shell parser like `watch`.

Tests: execpolicy suites green (unit 223 passed; integration 1 + 5 + 7
passed; doc 1 passed); the 23 new shell_parse_policy entries fail without
the change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks
…y matcher

A typed `action = deny` rule that is not promoted to `denied_prefixes`
(for example one scoped with `workspace = ...`) was matched with the
allow-direction arity matcher, which requires the subcommand to be spelled
literally at the front. `git -C . push`, `git -c a=b push` and
`git --no-pager push` therefore did not meet a `git push` deny rule. Typed
Deny rules now also match via `denied_prefix_matches`, the matcher the
engine already uses for denied prefixes; Ask and Allow rules are unchanged
and the workspace scope still applies.

Tests: execpolicy suites green (unit 223 passed; integration 1 + 5 + 8
passed; doc 1 passed); the new
typed_deny_rule_skips_global_options_before_the_subcommand fails without
the change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks
`deny = ["git push --force"]` only denied the exact text `git push --force`:
deny patterns were whole-command anchored globs, so
`git push --force origin main` and `rm -rf / --no-preserve-root` fell
through to AskUser, which auto-approving postures may grant. Deny patterns
now also match through `denied_prefix_matches`, the permission engine's
deny matcher (prefix at a word boundary, global options skipped), with the
glob kept for patterns that spell `*`.

That matcher also lets a rule's option appear after positionals, since
most CLIs permute arguments: a `git push --force` rule now covers
`git push origin main --force`, while the command word and the rule's own
positionals stay anchored.

The `compiled_glob` doc claimed `*` crosses newlines; it does not, and the
doc now says so (behavior unchanged, so allow globs do not widen).

Tests: execpolicy suites green (unit 225 passed; integration 1 + 5 + 8
passed; doc 1 passed); deny_pattern_is_a_command_prefix fails without the
change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks
… the default registry

The agent read-only grammar admitted `npm view|show|info` with any
arguments, while `readonly_network_reads` reports the host as
`registry.npmjs.org`. `npm view x --registry=https://other.example/` was
therefore approved and checked against the wrong host, and `--cache` /
`--userconfig` pointed npm at arbitrary files. The grammar now admits only
`--json` as an option and refuses package specs that name a URL, a git or
file source, or a local path; a refused npm read is reported as an option
refusal.

Tests: execpolicy suites green (unit 225 passed; integration 1 + 5 + 8
passed; doc 1 passed); the new refusal and network-read assertions fail
without the change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks
…ad-only arguments

`analyze_command` rated a command `Safe` when its text merely started with
an entry of the safe table, so `cdk deploy`, `psql -c ...`, `setsid curl`
and `topgrade -y` read as `cd`, `ps`, `set` and `top`, and entries that run
their arguments or write files (`env CMD`, `awk 'BEGIN{system(...)}'`,
`sed -i`, `find -exec`, `git branch -D`, `git remote set-url`,
`git diff --output=...`) were `Safe` with any arguments. Auto-Review treats
`Safe`/`WorkspaceSafe` as routine and approval offers a persistent grant on
that verdict.

The safe and workspace-safe tables now match word for word, and each entry
that can execute or write is limited to its read-only forms: `env` lists
only, `find`/`sed` reuse the agent read-only grammar, `awk` leaves the
table, `rg --pre`, `fd -x/--exec`, `man -P/-H/-C`, `less +cmd`, a second
`uniq` operand and `hostname NAME` are refused, `git branch`/`tag`/`remote`
admit listing forms only, and `git diff`/`log`/`show` refuse `--output`.
`touch` and `mkdir` are workspace-safe only for workspace-relative
operands, as `cp`/`mv` already were. Refused forms fall back to
RequiresApproval.

Tests: execpolicy suites green (unit 226 passed; integration 1 + 5 + 8
passed; doc 1 passed); clippy -D warnings clean for the crate;
safe_classification_needs_whole_words_and_read_only_arguments fails
without the change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks
npm view/show/info cannot be admitted as registry-only reads from argv:
project/user config and environment can redirect registry traffic, and npm
package specs can name Git, remote archives, or local files. Remove their
automatic Safe/read-only admission while preserving ordinary explicitly
approved shell execution. Keep potential npm network detection independent
of admission so network-denied children continue to fail closed.

Extend classifier, shell, registry, and subagent regressions for ordinary,
scoped, --json, Git, file, and configured-registry forms. Preserve the
original PR #6732 commits and their author/session attribution.

Verified by the coordinating root on this frozen four-file source:
- codewhale-execpolicy: 227 unit + 14 integration + 1 doc = 242 passed, 0 failed.
- Five focused TUI policy/network consumer tests: 5 passed, 0 failed.
- npm test: 636 passed (68 + 16 + 50 + 502), 0 failed.
- npm run check:web: passed, including 839 generated pages.
- cargo fmt --all -- --check and git diff --check: passed.

Logs: /private/tmp/cw-6732-{execpolicy,tui,npm,web}-root.log.
Hosted CI must qualify the updated PR head separately; no provider calls.
Remove the stale npm view allowance from subagent and Fleet guidance.
Explain why a network grant does not authorize npm executable helpers.

Validation: npm test636passed/0failed; check:web passed. The already
qualified Rust implementation and test inputs are unchanged.

Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Prepare a bounded five-path execpolicy follow-up to PR #6732. Source implementation and validation remain pending; no passing gate is claimed.
Reuse scanner provenance for control operators, redirections and unresolved arguments across trusted, typed and TOML prefix grants. Share existing read-argument grammars while retaining exact command/workspace grants, deny precedence and file permissions.

Validation: 248 focused Rust tests passed, 0 failed, 0 ignored (228 library, 1 authorization order, 11 redirection, 8 parsing). npm test 636 passed; npm run check:web passed. Root and independent source reviews passed. Initial gate223/5 exposed file-rule and remote-pipe classification regressions; original failing tests were retained and pass after narrow repairs.
Copilot AI balanced review requested due to automatic review settings September 29, 2026 13:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Hmbown
Hmbown changed the base branch from fix/bh-execpolicy-shell-grammar to main September 29, 2026 13:22
@Hmbown Hmbown closed this Sep 29, 2026
@Hmbown Hmbown reopened this Sep 29, 2026
@Hmbown
Hmbown merged commit be94b37 into main Sep 29, 2026
36 checks passed
@Hmbown
Hmbown deleted the fix/shell-grant-syntax branch September 29, 2026 18:39
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.

2 participants