Skip to content

[Bug]: Inline env var prefix (e.g. CI=true git commit) bypasses bash permission rules #16075

Closed as not planned

Description

@fazzledev

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.text

node.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 stripped

This 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

  1. Configure opencode.json:
{
  "permission": {
    "bash": {
      "git *": "ask",
      "*": "allow"
    }
  }
}
  1. Have an AI agent run: CI=true git commit --allow-empty -m "test"
  2. Expected: Permission dialog fires for git commit
  3. 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

  1. added
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    on Mar 5, 2026
  2. fazzledev commented on Mar 8, 2026

    @fazzledev
    Author

    @thdxr Please look into this. The current situation is very dangerous as none of the permissions are respected.

  3. assigned and unassigned on Apr 26, 2026
  4. removed
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    on May 3, 2026
  5. prullanferragut commented on May 20, 2026

    @prullanferragut

    Confirming this also bypasses deny rules — not just ask

    The report focuses on ask rules being skipped, but the same bypass applies to deny rules, 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:

    GITHUB_TOKEN=$(gh auth token --hostname github-enterprise.example.com) git push --force origin my-feature-branch
    

    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 ask bypass: with ask, the user at least gets a chance to reject. With deny, the expectation is unconditional blocking — there's no fallback.

  6. github-actions commented on Jul 20, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions