Skip to content

[BUG] Destructive Command Guard parses commands as POSIX shell while execute_command runs cmd.exe on Windows #2082

Open

Description

@SteveALee

Summary

On Windows, execute_command runs through cmd.exe, but the Destructive Command Guard parses the same string as
POSIX shell. The two disagree, so the guard refuses commands cmd would run happily, and cannot recognise the
destructive forms that belong to the shell it is actually protecting.

What happened (Zoo Code 3.90.0, Windows 11, VS Code)

A command that is valid cmd — a python -c "…" one-liner with nested quotes, chained after a git command:

git diff --stat PROJECT_SPEC_CORRECTION.md && python -c "import subprocess,re; old=subprocess.run(['git','show','HEAD:PROJECT_SPEC_CORRECTION.md'],capture_output=True).stdout.decode('utf-8-sig'); …"

was refused before the user was asked:

Destructive Command Guard blocked this command. Message from DCG: ambiguous command substitution could not be parsed
as POSIX shell syntax

Nothing in that string is a command substitution under cmd.exe; the quotes are ordinary.

The same session shows which shell is really in play: type, findstr and dir /b all return cmd output, and
%ERRORLEVEL% and set X=Y behave as cmd. So the executor is cmd.exe, and the guard is reading a different language.

Why it matters

  1. False positives. Any quoting a POSIX parser cannot resolve is refused, though cmd.exe would run it. Each refusal
    costs a turn and gives the model an error it cannot act on — it does not know whether to rephrase for POSIX or cmd.
  2. Blind spots. cmd-only destructive forms — rd /s, del /q, format, diskpart — sit outside the grammar the
    guard reasons in, so they cannot be matched as destructive. The guard is strongest where it is least needed.
  3. It is silent about the dialect. Nothing in the refusal says which shell it assumed, so the user cannot tell
    whether the command is dangerous or merely unparseable.

Suggested direction

Parse in the dialect the command will actually run in — whatever that is, rather than a fixed one. Today it is COMSPEC
(cmd.exe) on Windows; if the shell selection raised in the executor issue is fixed, it becomes whatever the configured
profile names, bash or PowerShell or WSL. Where the dialect genuinely cannot be determined, a refusal is still the right
default — but say which shell was assumed, so the operator can tell "dangerous" from "unreadable".

Related

#82, #705, #1673 (closed) — earlier rounds of shell selection on Windows; #1947, #2068 (open) — the opposite direction,
nested substitutions bypassing the allowlist.

Activity

  1. SteveALee commented on Oct 17, 2026

    @SteveALee
    Author

    Companion issue: #2083 - on Windows the command shell is COMSPEC (cmd.exe), with no way to point it at the configured terminal profile. This issue is the guard side of the same assumption: whichever shell is settled on, the guard has to parse that one, and say which it assumed when it refuses.

  2. SteveALee commented on Oct 18, 2026

    @SteveALee
    Author

    One more symptom, and what it narrows to

    Measured on Windows 11, Zoo Code 3.90.0, with cmd.exe as the executor - the same setup as the report above. The refusals
    line up with a second shell inside the command rather than with the command itself, which may be useful for the fix
    and for the refusal text.

    Four shapes, one session:

    • powershell -NoProfile -ExecutionPolicy Bypass -File tools/read-window.ps1 -ProcessId <pid> - refused:
      PowerShell host option "-File" is unknown or ambiguous.
    • powershell -NoProfile -Command "<literal body>" at the head of the command - ran.
    • tools\read-window.cmd -ProcessId <pid>, a .cmd shim whose launcher carries %~dp0 and %* - ran three times, its
      switches arriving each time.
    • A launcher embedded in a longer command - chained behind something else, or carrying $ or % in its body -
      refused: embedded PowerShell launcher command contains runtime expansion that DCG cannot statically verify.

    Two observations:

    1. The -File refusal is the POSIX parse this issue describes: -File is a PowerShell host option, and POSIX grammar
      has no notion of it.
    2. The "runtime expansion" refusal is worded as a static-analysis limit rather than a parse error. If refusing what the
      guard cannot verify is deliberate - and it seems a reasonable default - then that one is not a bug. What is hard for
      the operator is that both arrive as the same kind of card: today one session was blocked for something genuinely
      destructive (deleting a git branch, which needs approval) and blocked again for something merely unreadable, and
      nothing in either message says which shell was assumed.

    So the ask in this issue - name the dialect the guard assumed - would separate "dangerous" from "unparseable" for the
    person deciding whether to approve.

  3. edelauna commented on Oct 19, 2026

    @edelauna
    Contributor

    Findings: cause and what upstream already offers

    Cause. runner.ts runs dcg test --format json --no-color <command>. It passes no shell or dialect option. dcg v0.7.7 has no such option and parses every command as POSIX. The refusal text is hardcoded: ambiguous command substitution could not be parsed as POSIX shell syntax.

    Pinned version is old. DCG_VERSION in src/services/destructive-command-guard/constants.ts is v0.7.7. The latest upstream release is v0.15.3 (2026-10-08).

    Upstream already handles dialects.

    Blind spots today (v0.7.7, Linux build). The windows.filesystem and windows.misc packs are off by default, and Zoo Code does not enable them. With --with-packs windows.filesystem,windows.misc:

    • rd /s /q foo: denied.
    • format c:: denied.
    • del /q foo: still allowed.

    Without the packs, all three are allowed.

    Still open upstream.

    Proposed fix in Zoo Code

    1. Bump DCG_VERSION and the archive SHA-256 values. Related: chore(dcg): automate DCG_VERSION + checksum updates via Renovate #1067.
    2. On Windows, pass --dialect cmd when the executor is cmd.exe. Pass --dialect unknown when the shell is not known. This depends on [BUG] On Windows, execute_command always runs cmd.exe via COMSPEC and ignores the configured terminal shell #2083.
    3. Pass --with-packs windows.filesystem,windows.misc on Windows.
    4. Name the assumed shell in the denial message, so the user can tell "dangerous" from "unparseable".

    Step 4 needs no dcg change. I tested only the Linux build of v0.7.7.

  4. PierrunoYT commented on Oct 20, 2026

    @PierrunoYT
    Contributor

    Working on it!

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

Metadata

Metadata

Assignees

No one assigned

    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