Skip to content

fix(core): scan PowerShell commands the grammar fails to parse - #54448

Closed
saurav-shakya wants to merge 2 commits into
anomalyco:devfrom
saurav-shakya:ccr-bf38cb46-bt5x73
Closed

saurav-shakya wants to merge 2 commits into
anomalyco:devfrom
saurav-shakya:ccr-bf38cb46-bt5x73

Conversation

@saurav-shakya

@saurav-shakya saurav-shakya commented Oct 11, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #54393

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

tree-sitter-powershell can't parse a bare --, so it returns an ERROR root with no command nodes. The legacy scanner then reports zero commands, and the shell tool skips permission.assert, so git rev-parse HEAD -- runs under shell * deny. The same thing happens for tab and CR separators.

In scanLegacy, when the tree has errors and no commands were found, this PR falls back to scanPortable. That scanner either finds the commands (git grep -n x -- README.md → git grep *, so existing allow rules still match) or fails closed. Parses that still yield commands keep the legacy behavior.

This is complementary to #49954, which makes shell.ts assert on the raw command when the scanner returns nothing. This PR fixes the scanner so the real command prefix is matched.

How did you verify your code works?

  • Added regression cases in shell-parse.test.ts for the four commands in the issue. They fail on dev and pass with this change.
  • Three cases in shell-parse-compatibility.test.ts that recorded this as a known legacy gap (Get-ChildItem\rRemove-Item victim, git\tstatus, Set-Location a,b) now pass the parity check, so I moved them to the parity list.
  • bun typecheck and bun run test in packages/core: everything passes except two flock tests that fail identically on dev when run as root.

Screenshots / recordings

N/A, not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Related: #49948, #52615

tree-sitter-powershell returns an ERROR tree with no command nodes for
commands containing a bare `--` (and for tab or CR separators), so the
legacy scanner reported zero commands and the shell tool skipped its
permission check entirely. When the parse has errors and yields no
commands, fall back to the portable scanner, which either finds the
commands or fails closed.

Fixes anomalyco#54393
Copilot AI balanced review requested due to automatic review settings October 11, 2026 04:39

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.

@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@opencode-agent opencode-agent Bot 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.

Thanks for tracking this down. This fixes the cases in #54393, but the bypass is still open whenever the command also includes a part the grammar parses.

I ran the issue's reproduction from source on the base and on this branch: a stand-in pwsh, the legacy scanner, and rules shell * deny plus a git status * allow. echo BYPASS-RAN -- is now denied, as it should be. But git status --short; echo MIXED-RAN -- still runs with no prompt or denial. Tree-sitter finds the git status command node, so commands.length > 0, the fallback is skipped, and only git status is checked against the rules. Calling the scanner directly gives the same result:

  • git status; Remove-Item victim -- → only git status
  • Remove-Item x --; Get-ChildItem → only Get-ChildItem

Anyone who has a single allow rule can still run any command by adding it after an allowed one with a trailing --. See the inline comment.

The tests in packages/core (the shell-parse* files and tool-shell.test.ts) pass, and so does typecheck. A regression case covering a mixed command like the ones above would catch this.

The public API is unchanged.

Related: #49948, #52615

Before

After

Comment thread packages/core/src/shell/parse.ts Outdated
(tree) => Effect.sync(() => tree.delete()),
)
// Grammar errors (e.g. a bare `--` under PowerShell) can hide every command node and skip permission checks.
if (error && result.commands.length === 0) return yield* scanPortable(command, shell, cwd)

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.

This only falls back when the broken parse finds no commands. If an ERROR node sits next to a parsed command, the hidden part is never checked:

  • git status; Remove-Item victim -- scans to just git status
  • Remove-Item x --; Get-ChildItem scans to just Get-ChildItem

With a git status * allow under * deny, git status --short; echo MIXED-RAN -- ran end to end on this branch. Falling back to scanPortable whenever tree.rootNode.hasError is true would close this. scanPortable either finds every command or fails closed. You could limit it to PowerShell if the effect on bash is a concern.

A bare `--` could still hide a command when another command in the
same line parsed (e.g. `git status; Remove-Item victim --`): legacy
found one command, so the fallback was skipped and only `git status`
was checked. For PowerShell parses with errors, compare against the
portable scanner and use its result unless both agree on the commands
and directories, which keeps saved prefixes stable when nothing is
hidden.

A tab-separated `git\tstatus; Write-Output done` was also dropping
`git\tstatus`; the permission impact fixtures now expect legacy to
check it like the portable scanner does.
@saurav-shakya

Copy link
Copy Markdown
Author

Thanks, good catch. Fixed in fb3a88a: for PowerShell parses with errors, the legacy result is now compared with the portable scanner's, and the portable result is used unless both agree on the commands and directories, so a command hidden beside a parsed one is always checked. Added your three mixed cases as regression tests. This also fixed git\tstatus; Write-Output done, which was dropping git\tstatus, so I updated those permission-impact fixtures to match.

@github-actions

Copy link
Copy Markdown
Contributor

Automated PR Cleanup

Thank you for contributing to opencode.

Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions.

This PR was closed because it matched the following cleanup criteria:

  • The PR was created more than 1 month ago
  • The PR had fewer than 2 positive reactions
  • Positive reactions are counted as thumbs-up, heart, celebration, or rocket reactions on the PR

PRs created within the last month are not affected by this cleanup.

If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate.

Thanks again for taking the time to contribute.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

shell: bare "--" in a pwsh command skips the permission check

2 participants