anomalyco / anomalyco/opencode
shell: bare redirect bypasses the permission check (scanner returns no commands)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
What is the issue?
A shell command that the scanner finds no commands in skips the permission check entirely and runs unchecked. A bare redirect is the clearest case: > file is valid POSIX, truncates or creates the file, and parses to zero commands.
packages/core/src/tool/plugin/shell.ts only asserts when the scan produced something:
const parsed = yield* ShellParse.scan(invocation.command, invocation.shell, target.absolute, { portable })
…
if (parsed.commands.length > 0)
yield* permission.assert({ … })
So with
permission:
shell:
"*": "deny"
> important.txt still empties the file — no prompt, no denial, no permission record.
Why it happens
Two things line up:
1. Both scanners report zero commands for a bare redirect. Checked against ShellParse.scan directly, which is what the tool calls:
| command | legacy | portable |
|---|---|---|
> victim.txt |
0 commands | 0 commands |
>> victim.txt |
0 commands | 0 commands |
echo hi > out.txt |
1 | 1 |
rm -rf x |
1 | 1 |
Command substitutions are handled correctly — FOO=$(whoami) yields 1 — so this is specific to a redirect with no command word.
2. An empty resource list evaluates to allow. In packages/core/src/permission.ts:
const effects = input.resources.map((resource) => evaluate(input.action, resource, all).effect)
const effect = effects.includes("deny") ? "deny" : effects.includes("ask") ? "ask" : "allow"
With resources: [], effects is [], so neither branch matches and the result is allow — even under { action: "*", resource: "*", effect: "deny" }. I added an assertion for exactly that in the existing permission test harness and it passes, so the fail-open is real rather than theoretical.
The commands.length > 0 guard means the second condition is never reached from this path today, but it is the reason the guard is load-bearing.
Reproduction
printf 'important data\n' > victim.txt # 15 bytes
/bin/sh -c '> victim.txt'
wc -c < victim.txt # 0
Then the same command through the shell tool with permission.shell "*": "deny" — it is not denied.
Expected behavior
A command the scanner cannot decompose should still be evaluated, using the raw invocation as the resource, so a deny rule applies and an explicit allow can still permit it. Failing closed seems clearly right for a permission boundary.
I have a small patch and can open a PR.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/core/src/tool/plugin/shell.ts and trace how ShellParse.scan results reach permission.assert; then read packages/core/src/permission.ts and the existing permission test harness. Done means bare redirects are evaluated against the raw invocation, denied under a matching shell rule, explicitly allowed when configured, and covered by tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- authorization, cli, security
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100