Skip to content

fix(exec): resolve Windows PowerShell cmdlet allowlist miss#70341

Open
adamdaum wants to merge 4 commits intoopenclaw:mainfrom
adamdaum:fix/windows-exec-allowlist-52952
Open

fix(exec): resolve Windows PowerShell cmdlet allowlist miss#70341
adamdaum wants to merge 4 commits intoopenclaw:mainfrom
adamdaum:fix/windows-exec-allowlist-52952

Conversation

@adamdaum
Copy link
Copy Markdown

Summary

Fixes #52952.

  • PowerShell cmdlets (Get-ChildItem, Set-Location, etc.) are shell builtins without filesystem paths. The exec allowlist system assumed every command maps to a file, causing bare cmdlets on Windows to fail with exec denied: allowlist miss.
  • Adds a cmdlet fallback in the allowlist evaluation layer: when a bare command has no resolved path on Windows, tries matching against the PowerShell executable path with a synthetic argv that includes the cmdlet name for argPattern verification.
  • Extracts resolvePowerShellPath/resolveShellFromPath from src/agents/shell-utils.ts to src/infra/executable-path.ts so the allowlist code can resolve the PS path without a circular agents→infra dependency.

Design decisions

  • Resolution layer stays honest: ExecutableResolution.resolvedPath remains undefined for cmdlets — no fake path. The fallback is confined to the allowlist evaluation layer.
  • argPattern includes cmdlet name: A PowerShell path allowlist entry only matches when the argPattern also verifies the specific cmdlet + arguments. No broad grant unless the user explicitly adds a bare PowerShell entry without argPattern.
  • Safe bins remain disabled on Windows: No change to existing conservative policy.

Test plan

  • Bare cmdlet matches PS path allowlist entry with correct argPattern
  • Bare cmdlet rejected when argPattern doesn't include cmdlet name
  • Bare cmdlet rejected when no PS entry in allowlist
  • Wrapped powershell -Command "cmdlet" matches after wrapper stripping
  • Broad PS path entry (no argPattern) matches bare cmdlets
  • Allow-always generates PS path pattern with cmdlet-inclusive argPattern
  • Non-Windows platforms don't trigger the fallback
  • All existing exec-approvals, allowlist-matching, and shell-utils tests pass
  • Manual: verify on Windows gateway with Telegram-triggered exec

🤖 Generated with Claude Code

…#52952)

PowerShell cmdlets (Get-ChildItem, etc.) are shell builtins without
filesystem paths.  The allowlist system assumed every command maps to a
file, causing bare cmdlets to fail with 'exec denied: allowlist miss'
on Windows.

Add a cmdlet fallback in the allowlist evaluation layer: when a bare
command has no resolved path on Windows, try matching against the
PowerShell executable path with a synthetic argv that includes the
cmdlet name for argPattern verification.  This keeps the resolution
layer honest (no fake resolvedPath) while letting cmdlets match
allowlist entries scoped to the PowerShell executable.

Also extract resolvePowerShellPath/resolveShellFromPath to the infra
layer (executable-path.ts) so the allowlist code can resolve the PS
path without a circular agents->infra dependency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: M labels Apr 22, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 22, 2026

Greptile Summary

This PR fixes Windows PowerShell cmdlet exec-allowlist misses by adding a targeted fallback that, when a bare command has no resolved filesystem path on Windows, matches it against the PowerShell executable with a synthetic argv carrying the cmdlet name for argPattern verification. The refactor of resolvePowerShellPath/resolveShellFromPath into src/infra/executable-path.ts is clean and avoids a circular dependency. All guard conditions (non-Windows, already-matched, path-separator-containing names, already-resolved paths) are correctly ordered.

Confidence Score: 5/5

Safe to merge; all findings are minor P2 style/performance suggestions with no correctness impact.

The logic is sound, the priority chain is correctly ordered, guards cover all edge cases, and the test plan is comprehensive. Remaining observations (uncached resolvePowerShellPath and hand-constructed argPattern strings in tests) are non-blocking.

No files require special attention.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/infra/exec-approvals-allowlist.ts
Line: 431

Comment:
**`resolvePowerShellPath` called without caching per segment**

`resolvePowerShellPath()` does multiple `fs.existsSync` probes on every call. In `resolveWindowsCmdletAllowlistFallback` it runs once per unresolved bare-name segment, and again in `collectAllowAlwaysPatterns`. For a multi-cmdlet pipeline on Windows this results in redundant filesystem I/O each time the allowlist is evaluated.

Consider memoising the result (a module-level `let cachedPsPath: string | undefined` is enough since the PowerShell install location won't change mid-process) or passing the resolved path in from the call site.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/infra/exec-approvals-windows-cmdlet.test.ts
Line: 47-49

Comment:
**Hardcoded `buildArgPatternFromArgv` internals**

The test constructs the expected `argPattern` string manually (null-byte separator, trailing null, regex escaping) rather than calling `buildArgPatternFromArgv` on the same synthetic argv. If the pattern format ever changes this test will produce a confusing failure — it will appear to reject a valid allowlist entry rather than flagging a format change.

Consider building the expected pattern the same way production code does:
```ts
const syntheticArgv = [psPath, "Get-ChildItem", "-LiteralPath", "C:\\Users\\foo"];
const argPattern = buildArgPatternFromArgv(syntheticArgv, WINDOWS_PLATFORM);
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(exec): resolve Windows PowerShell cm..." | Re-trigger Greptile

if (!rawExecutable || !isBareName(rawExecutable) || params.resolution?.resolvedPath) {
return null;
}
const psPath = resolvePowerShellPath();
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.

P2 resolvePowerShellPath called without caching per segment

resolvePowerShellPath() does multiple fs.existsSync probes on every call. In resolveWindowsCmdletAllowlistFallback it runs once per unresolved bare-name segment, and again in collectAllowAlwaysPatterns. For a multi-cmdlet pipeline on Windows this results in redundant filesystem I/O each time the allowlist is evaluated.

Consider memoising the result (a module-level let cachedPsPath: string | undefined is enough since the PowerShell install location won't change mid-process) or passing the resolved path in from the call site.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/exec-approvals-allowlist.ts
Line: 431

Comment:
**`resolvePowerShellPath` called without caching per segment**

`resolvePowerShellPath()` does multiple `fs.existsSync` probes on every call. In `resolveWindowsCmdletAllowlistFallback` it runs once per unresolved bare-name segment, and again in `collectAllowAlwaysPatterns`. For a multi-cmdlet pipeline on Windows this results in redundant filesystem I/O each time the allowlist is evaluated.

Consider memoising the result (a module-level `let cachedPsPath: string | undefined` is enough since the PowerShell install location won't change mid-process) or passing the resolved path in from the call site.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +47 to +49
{ pattern: psPath, argPattern },
]);

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.

P2 Hardcoded buildArgPatternFromArgv internals

The test constructs the expected argPattern string manually (null-byte separator, trailing null, regex escaping) rather than calling buildArgPatternFromArgv on the same synthetic argv. If the pattern format ever changes this test will produce a confusing failure — it will appear to reject a valid allowlist entry rather than flagging a format change.

Consider building the expected pattern the same way production code does:

const syntheticArgv = [psPath, "Get-ChildItem", "-LiteralPath", "C:\\Users\\foo"];
const argPattern = buildArgPatternFromArgv(syntheticArgv, WINDOWS_PLATFORM);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/exec-approvals-windows-cmdlet.test.ts
Line: 47-49

Comment:
**Hardcoded `buildArgPatternFromArgv` internals**

The test constructs the expected `argPattern` string manually (null-byte separator, trailing null, regex escaping) rather than calling `buildArgPatternFromArgv` on the same synthetic argv. If the pattern format ever changes this test will produce a confusing failure — it will appear to reject a valid allowlist entry rather than flagging a format change.

Consider building the expected pattern the same way production code does:
```ts
const syntheticArgv = [psPath, "Get-ChildItem", "-LiteralPath", "C:\\Users\\foo"];
const argPattern = buildArgPatternFromArgv(syntheticArgv, WINDOWS_PLATFORM);
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b0e879c994

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

effectiveArgv,
inlineCommand,
match: executableMatch ?? shellPositionalArgvMatch ?? shellScriptMatch,
match: executableMatch ?? windowsCmdletMatch ?? shellPositionalArgvMatch ?? shellScriptMatch,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve wrapper for allowlisted Windows cmdlets

This new fallback can mark bare PowerShell cmdlets as allowlisted, but downstream Windows node execution assumes an allowlisted shell payload is directly executable and swaps to segments[0].argv (src/node-host/invoke-system-run-allowlist.ts:124-131). In the cmdlet case (Get-ChildItem, Set-Location, etc.), that drops the shell wrapper and attempts to spawn the cmdlet token as a process, which fails (ENOENT) on reruns after allow-always/manual allowlist entries.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1dffcd4d93

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

effectiveArgv,
inlineCommand,
match: executableMatch ?? shellPositionalArgvMatch ?? shellScriptMatch,
match: executableMatch ?? windowsCmdletMatch ?? shellPositionalArgvMatch ?? shellScriptMatch,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep shell wrapper for cmdlet allowlist matches

By adding windowsCmdletMatch into segment matching, bare PowerShell cmdlets can now satisfy allowlist on Windows, but execution still rewrites allowlisted shell payloads to segments[0].argv in src/node-host/invoke-system-run-allowlist.ts:121-131. In the cmdlet case that drops the PowerShell host and attempts to spawn Get-ChildItem (or similar) as a process, which fails with ENOENT on reruns after allow-always/manual allowlist entries. Fresh evidence from this commit is src/infra/exec-approvals-windows-cmdlet.test.ts, which now asserts bare cmdlets are allowlist-satisfied and therefore makes this execution path reachable.

Useful? React with 👍 / 👎.

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 29, 2026

Codex review: found issues before merge.

What this changes:

The PR adds a Windows-only PowerShell cmdlet fallback for exec allowlist matching and allow-always persistence, moves PowerShell path resolution into infra, and adds cmdlet allowlist tests.

Maintainer follow-up before merge:

This needs maintainer security/product review rather than an automated repair because it changes exec allowlist semantics and the right direction may be exact-command durable approvals instead of reusable PowerShell cmdlet fallback semantics.

Security review:

Security review needs attention: The diff changes exec allowlist trust semantics and currently lets the approved PowerShell target diverge from the argv that the node host executes.

Review findings:

  • [P1] Preserve the PowerShell host for cmdlet matches — src/infra/exec-approvals-allowlist.ts:536
  • [P3] Add the required changelog entry — src/infra/exec-approvals-allowlist.ts:536
Review details

Best possible solution:

Revise the design so the allowlist decision and execution plan bind the same command: either preserve a PowerShell-hosted argv for cmdlet matches all the way through node-host execution, or avoid reusable cmdlet fallback semantics and rely on exact-command durable approvals. The final PR should include Windows node-host execution coverage and the required changelog entry.

Do we have a high-confidence way to reproduce the issue?

Yes. The source-level reproduction path is high confidence: the PR makes a bare cmdlet allowlist-satisfied via a synthetic PowerShell match, while current node-host execution rewrites allowlisted Windows shell payloads to the bare segment argv before spawning.

Is this the best way to solve the issue?

No. The PR updates allowlist evaluation and persistence, but not the execution plan, so the approved target can diverge from what the node host attempts to execute; carrying cmdlet-match metadata or an exact PowerShell argv into execution is the safer fix.

Full review comments:

  • [P1] Preserve the PowerShell host for cmdlet matches — src/infra/exec-approvals-allowlist.ts:536
    This fallback can mark bare PowerShell cmdlets as allowlisted, but the Windows node-host path later rewrites allowlisted shell commands to segments[0].argv. For Get-ChildItem or Set-Location, that drops PowerShell and tries to spawn the cmdlet directly, so allow-always reruns fail with ENOENT instead of executing the approved command.
    Confidence: 0.91
  • [P3] Add the required changelog entry — src/infra/exec-approvals-allowlist.ts:536
    This is a user-facing Windows exec fix, but the PR does not add a CHANGELOG.md entry. OpenClaw policy requires user-facing fix entries with attribution, so release notes would miss this behavior change.
    Confidence: 0.86

Overall correctness: patch is incorrect
Overall confidence: 0.9

Security concerns:

  • [medium] Allowlist trust diverges from execution target — src/infra/exec-approvals-allowlist.ts:536
    The PR approves an unresolved bare cmdlet by matching a synthetic PowerShell executable argv, while the node-host execution path can later run the bare cmdlet argv. Exec approvals should bind what is approved to what is actually executed.
    Confidence: 0.88

What I checked:

  • PR fallback approves synthetic PowerShell argv: The PR adds resolveWindowsCmdletAllowlistFallback and returns windowsCmdletMatch from segment allowlist matching, so an unresolved bare command can satisfy the allowlist via a synthetic PowerShell executable argv. (src/infra/exec-approvals-allowlist.ts:536, 1dffcd4d9314)
  • Node-host execution rewrites allowlisted Windows shell payloads: Current main rewrites an allowlisted Windows shell command to segments[0].argv when no ask approval was used; for a cmdlet segment that argv begins with Get-ChildItem rather than powershell.exe. (src/node-host/invoke-system-run-allowlist.ts:120, 068b33de8717)
  • Planned allowlist argv is not used for shell commands: Current main's resolvePlannedAllowlistArgv exits when shellCommand is present, so the existing planned-argv path does not preserve a PowerShell wrapper for the PR's cmdlet shell-command case. (src/node-host/invoke-system-run-allowlist.ts:92, 068b33de8717)
  • Current main does not implement the cmdlet fallback: Current main segment matching returns executableMatch, shellPositionalArgvMatch, or shellScriptMatch only; there is no Windows bare-cmdlet synthetic PowerShell match on main. (src/infra/exec-approvals-allowlist.ts:486, 068b33de8717)
  • PR tests make the execution path reachable: The added test asserts a bare Get-ChildItem command can be allowlist-satisfied by the derived PowerShell allow-always entry, which is the condition that later reaches the Windows execution rewrite path. (src/infra/exec-approvals-windows-cmdlet.test.ts:34, 1dffcd4d9314)
  • Related discussion context: The linked report Windows Telegram execs fail in allowlist/command-rebuild path #52952 is closed, but that closure is not equivalent to merging this PR's reusable PowerShell-executable allowlist semantics; prior PR discussion also recorded this same PowerShell-host preservation blocker after the minor Greptile comments were addressed.

Likely related people:

  • steipete: History shows repeated recent work on exec allowlist hardening, Windows argPattern allowlist flow, wrapper trust, and the current extracted allowlist files. (role: recent maintainer and primary adjacent owner; confidence: high; commits: fff6333773f2, d8cef14eb118, d82c042b0972; files: src/infra/exec-approvals-allowlist.ts, src/node-host/invoke-system-run-allowlist.ts, src/infra/exec-approvals-analysis.ts)
  • Vincent Koc: History shows related remote approval and shell-wrapper positional allowlist work in the same exec approval surface. (role: adjacent maintainer; confidence: medium; commits: 2d53ffdec1da, 3814f956d131, d9a3ecd109ee; files: src/infra/exec-approvals-allowlist.ts, src/node-host/invoke-system-run-allowlist.ts)
  • Jacob Tomlinson: History shows nearby exec approval hardening for wrapper carrier and interpreter allow-always persistence, which overlaps the trust-boundary question here. (role: adjacent approval-hardening contributor; confidence: medium; commits: 0c8375424620, 1a75906a6fe5, 9ec44fad390f; files: src/infra/exec-approvals-allowlist.ts)

Remaining risk / open question:

  • Maintainer direction is still needed on whether reusable PowerShell-path cmdlet approvals are desired, because the linked report may instead be better served by exact-command durable approval semantics.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 068b33de8717.

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

Labels

agents Agent runtime and tooling size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows Telegram execs fail in allowlist/command-rebuild path

1 participant