Windows execpolicy cannot bind a bare PowerShell command to an approved launcher path

Open 💬 0 comments Opened Jul 30, 2026 by starriet9

What version of Codex CLI is running?

codex-cli 0.146.0

Reproduced with the official signed codex-x86_64-pc-windows-msvc.exe release asset for rust-v0.146.0.

What subscription do you have?

Pro

Which model were you using?

gpt-5.6-sol

What platform is your computer?

Microsoft Windows NT 10.0.19045.0 x64

What terminal emulator and version are you using (if applicable)?

Windows Terminal 1.24.11911.0 with PowerShell 7.6.4 (Core, x64).

Codex doctor report

Not included. The reproduction uses only the standalone codex execpolicy check command with isolated rule files. It does not depend on installation discovery, authentication, terminal state, MCP servers, networking, or the Codex state database.

What issue are you seeing?

When a Windows user creates a durable allow rule for a bare PowerShell command, Codex cannot bind that rule to the launcher path the user intended to trust.

For example, a global npm installation of Agent Playwright CLI is normally invoked as:

playwright-cli --version

PowerShell resolves that command to:

C:\Users\<user>\AppData\Roaming\npm\playwright-cli.ps1

The two available rule forms each lose one important property:

  1. An absolute-path prefix_rule restricts authority to the intended launcher, but it does not match the normal bare invocation.
  2. A bare-name prefix_rule(pattern = ["playwright-cli"]) matches the normal invocation, but it grants authority to whichever command PowerShell later resolves under that name.

The current host_executable() support does not bridge this gap. It supports the opposite direction: when the parsed command already contains an absolute executable path, a basename rule can match that path subject to host_executable() metadata.

This leaves users choosing between less ergonomic, shell-specific absolute-path invocations and durable authority that is sensitive to later PATH or command-resolution changes. This is not a claim that the current bare-rule behavior violates its documented contract; it is a gap in safely expressing the user's intended trust boundary.

What steps can reproduce the bug?

  1. Install a command that PowerShell resolves by name. Agent Playwright CLI is one concrete example:
npm install -g @playwright/cli@0.1.17
Get-Command playwright-cli | Format-List Name,Path,CommandType

Observed resolution:

Name        : playwright-cli.ps1
Path        : C:\Users\<user>\AppData\Roaming\npm\playwright-cli.ps1
CommandType : ExternalScript
  1. Create absolute.rules:
prefix_rule(
    pattern = ["C:\\Users\\<user>\\AppData\\Roaming\\npm\\playwright-cli.ps1"],
    decision = "allow",
)
  1. Check the normal bare invocation:
codex execpolicy check --pretty --rules .\absolute.rules -- playwright-cli --version

Observed:

{
  "matchedRules": []
}
  1. Check the absolute invocation:
codex execpolicy check --pretty --rules .\absolute.rules -- "C:\Users\<user>\AppData\Roaming\npm\playwright-cli.ps1" --version

Observed:

{
  "matchedRules": [
    {
      "prefixRuleMatch": {
        "matchedPrefix": [
          "C:\\Users\\<user>\\AppData\\Roaming\\npm\\playwright-cli.ps1"
        ],
        "decision": "allow"
      }
    }
  ],
  "decision": "allow"
}
  1. Create bare.rules with a bare-name rule:
prefix_rule(
    pattern = ["playwright-cli"],
    decision = "allow",
)
  1. Check the normal invocation again:
codex execpolicy check --pretty --rules .\bare.rules -- playwright-cli --version

Observed:

{
  "matchedRules": [
    {
      "prefixRuleMatch": {
        "matchedPrefix": [
          "playwright-cli"
        ],
        "decision": "allow"
      }
    }
  ],
  "decision": "allow"
}

No thread ID is applicable because the reproduction uses codex execpolicy check.

Evidence boundary

The checks above demonstrate only the rule-language mismatch; codex execpolicy check does not execute the command. In rust-v0.146.0, preflight can lower a PowerShell -Command body for matching, but the shell runtime later executes the original wrapper argv; resolvedProgram is matcher metadata, not an executor-bound target. This source-level separation motivates the execution-binding concern, but the checker output alone does not demonstrate a sandbox escape.

What is the expected behavior?

Codex should provide a path-bound way to authorize a bare external command without granting durable authority to any future command that happens to resolve under the same name.

The policy check and execution must bind to the same target. Codex should not verify one resolved path and then hand the original bare name back to PowerShell for a second, potentially different resolution.

If Codex cannot safely preserve that binding for a particular PowerShell command shape, it should conservatively keep the command sandboxed or request approval.

Additional information

Suggested design direction

One possible extension would let host_executable() opt into resolving a bare command through an explicit path mapping:

host_executable(
    name = "playwright-cli",
    paths = [
        "C:\\Users\\<user>\\AppData\\Roaming\\npm\\playwright-cli.ps1",
    ],
    resolve_bare = True,
)

prefix_rule(
    pattern = ["playwright-cli"],
    decision = "allow",
)

The syntax is only illustrative. In 0.146.0, host_executable() is reverse-match metadata, paths is an allowlist rather than target-selection order, and Windows executable-name normalization does not include .ps1. A separate typed binding primitive may therefore be clearer than overloading host_executable(). The important semantics are:

  • Construct one immutable launch plan before allow: the logical command, exact configured target, launcher kind, required interpreter, and lossless arguments.
  • Carry that plan through policy display, approval/cache identity, sandbox transforms, retries, and spawn. Never re-resolve it; if it cannot be honored, require fresh evaluation and approval.
  • Collect every authored explicit match across the logical command, bound path, and required interpreter, then apply forbidden > prompt > allow. Apply the unmatched-command fallback once only if that combined explicit-match set is empty.
  • Initially require exactly one eligible target. Future multiple-candidate priority must be explicit policy data, not PATH/PATHEXT, discovery order, existence, or unordered-list order.
  • After binding, shell resolution mechanisms must not replace the launcher or interpreter. Missing, non-file, unsupported, ambiguous, or mismatched plans must fail closed.

For a provably single external invocation with lossless arguments, Codex could construct a typed launcher plan. .exe/.com can use the configured absolute application path. .cmd/.bat must bind an exact trusted cmd.exe and script, disable AutoRun with /d, and use injection-safe /s /c encoding. .ps1 must bind the exact script and PowerShell path, flavor, and flags without another lookup. Do not rebuild arbitrary source from lossy argv-like lowering; unsupported forms must remain sandboxed or approval-gated.

Suggested regression coverage
  • One-to-one bindings for .exe, .com, .cmd, .bat, and .ps1, including the exact interpreter for script launchers.
  • Changes to PATH, PATHEXT, ComSpec, file associations, aliases, functions, cmdlets, module auto-loading, or profiles must not replace the bound target or interpreter.
  • An unlisted absolute path must not inherit allow, and a restrictive authored rule on the logical name, bound path, or interpreter must win over an allow on another representation.
  • Approval-cache reuse and sandbox or credential retries must retain the complete plan; any re-binding must trigger fresh policy evaluation and approval.
  • Empty arguments, embedded quotes, metacharacters, expressions, splatting, redirection, pipelines, and --% must either retain their original semantics or fail closed before a path-bound allow.

This proposal binds configured launcher and interpreter paths, not file objects or content digests. In-place modification, rename or replacement, hard-link substitution, and symlink, junction, reparse-point, or ancestor-path retargeting remain TOCTOU unless separate file-identity controls are added. Those are the same class of limitations as existing absolute-path rules. The narrower invariant here is that Codex performs no second name-based lookup of the launcher or required interpreter after policy selects the plan.

Related work
  • #13175 (closed as a duplicate of #11298), #13502, and #15294 cover broader shell-wrapper, Windows rule-matching, and native-CLI ergonomics problems.
  • #24201 covers execpolicy check not mirroring runtime PowerShell-wrapper normalization; this reproduction intentionally uses raw inner tokens and concerns bare-name authority instead.
  • #37079 covers the complementary case where a basename forbidden rule can be bypassed through an unregistered absolute lookalike path. This report concerns binding a bare-name allow to one configured launcher and carrying that same target through execution.
  • Merged PRs #12964 and #13065 added host_executable() matching for commands that already contain absolute paths, and merged PR #20336 made top-level PowerShell wrappers transparent to inner-command matching.
  • Closed, unmerged PR #29845 carried an optional explicit application path through Windows launchers without changing resolution policy. Closed, unmerged PR #30036 built on it to resolve native executables once and reuse the path, while intentionally rejecting .cmd/.bat.
  • Open PRs #30628 and #31019 recognize that parsed inner command names do not by themselves bind runtime PATH, module, profile, or wrapper resolution.
  • Open PR #30631 applies a resolve-once, carry-to-policy/display/cache/spawn invariant to model-selected shell executables, but not to general parsed inner commands or script launchers.
  • Open PR #30990 preserves restrictive decisions across Windows path aliases; the same fail-closed principle applies when combining logical-name, bound-path, and interpreter matches.

This report is about providing a supported path-bound mechanism rather than restoring unrestricted PATH-sensitive durable authority.

I did not find an existing public issue or PR that provides a policy-configured binding from a general parsed inner bare PowerShell command to one exact Windows launcher plan across native and script launcher kinds.

View original on GitHub ↗