Repository navigation
Description
Description
Commands with inline environment variable prefixes (e.g. CI=true git commit) bypass bash permission rules. A user who has configured "git *": "ask" will not be prompted — the command executes silently.
This affects any inline env var prefix, not just CI=true. FOO=bar git commit, ANYTHING=1 git push — all bypass the pattern match.
Root Cause
Introduced in commit e7ff714 (2026-01-30): "fix: handle redirected_statement treesitter node in bash permissions (#6737)"
In packages/opencode/src/tool/bash.ts, line 97:
let commandText = node.parent?.type === "redirected_statement" ? node.parent.text : node.textnode.text returns the full text of the command AST node, including any variable_assignment children. So for CI=true git commit -m "msg", commandText becomes "CI=true git commit -m \"msg\"".
Then on line 139:
patterns.add(commandText) // "CI=true git commit -m \"msg\""The permission system then tries to match "CI=true git commit -m \"msg\"" against the configured rule "git *" — no match — and falls through to "*": "allow", so the command runs without a dialog.
Meanwhile, the command[] array (used for always) correctly strips variable_assignment children (the loop on lines 100–113 only includes command_name, word, string, raw_string, concatenation node types). So command[0] = "git" is correct — but it's not used for the patterns.add() that drives permission matching.
Before the offending commit
The previous code was:
patterns.add(command.join(" ")) // "git commit -m msg" ← correctly strippedThis correctly matched "git *" regardless of any inline env var prefix.
The change broke it
The commit changed patterns.add(command.join(" ")) to patterns.add(commandText) to fix redirect handling (so users see git commit 2>&1 in the permission dialog instead of just git commit). The redirect fix is valid, but it unintentionally included variable_assignment nodes in the pattern used for permission matching.
Steps to Reproduce
- Configure
opencode.json:
{
"permission": {
"bash": {
"git *": "ask",
"*": "allow"
}
}
}- Have an AI agent run:
CI=true git commit --allow-empty -m "test" - Expected: Permission dialog fires for
git commit - Actual: Command executes silently — no dialog
Works with any inline env var: FOO=bar git commit, GIT_AUTHOR_NAME=x git commit, etc.
Note: export CI=true; git commit (separate commands) is not affected — only the inline VAR=value command syntax.
Environment
- OpenCode version: 1.1.51 (bug present, introduced in 1.1.x around 2026-01-30)
- Confirmed via binary strings extraction and source code analysis
- Affects all platforms
Activity
test: add test capturing env var prefix permission bypass (anomalyco#…
fazzledev commentedon Mar 8, 2026
@thdxr Please look into this. The current situation is very dangerous as none of the permissions are respected.
prullanferragut commentedon May 20, 2026
Confirming this also bypasses
denyrules — not justaskThe report focuses on
askrules being skipped, but the same bypass applies todenyrules, which is more severe — it silently executes commands that are explicitly blocked.Reproduction with
deny:Config:
{ "permission": { "bash": { "*": "allow", "git push --force*": "deny", "git push * -f*": "deny", "git push -f*": "deny" } } }Command executed by the agent:
Expected: blocked — matches
"git push --force*".Actual: executed silently — the
GITHUB_TOKEN=...prefix caused no match, fell through to"*": "allow".The force push completed and rewrote remote history on a branch that was part of a live stacked PR chain. The user only noticed afterward when reviewing what happened during the session.
The impact of this variant is higher than the
askbypass: withask, the user at least gets a chance to reject. Withdeny, the expectation is unconditional blocking — there's no fallback.fix: strip inline env var prefixes from bash permission patterns
github-actions commentedon Jul 20, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.