Skip to content

cordis-plugin-sandbox-grant-advisor

Verified

@argszero/cordis-plugin-sandbox-grant-advisor Β· v0.23.0 Β· MIT

Turns seven sandbox environment failures that name neither their cause nor a remedy into a diagnosis with a path forward. Family 1, the Windows workspace ACL: twelve reports (#7538, #7622, #7646, #7720, #7750, #7735, #7771, #7804, #7816, #8232, #8272, #82

Install

dsh plugin add @argszero/cordis-plugin-sandbox-grant-advisor

Confirm the layer applied with dsh --profile default --dump-config β€” see the install guide.

Source

Tags

Readme

@argszero/cordis-plugin-sandbox-grant-advisor

Turns a sandbox environment failure that has no path forward into a diagnosis the model β€” and the user reading the transcript β€” can act on. Seven signatures, one mechanism:

SetNamedSecurityInfoW failed (Win32 5): grantWrite(D:\ws)   # Windows workspace ACL
PTY shell exited during startup                             # persistent shell Γ— confining mode
[exit code: -1073741502]  (0xC0000142)                      # a confined child that never started
[exit code: 1]  sandbox: { mode: "workspace-write", denied: true }
                                                            # denied INSIDE the workspace
Windows ACL temp root must be outside the workspace: …      # the private temp root is inside the workspace
0x80041003 (WBEM_E_ACCESS_DENIED) in the command's stderr   # CIM/WMI refused to the restricted token
sandbox-local windows-acl temp grant materialization failed and its cleanup also failed
                                                            # an ACL refusal that withholds its own cause

This plugin is the stopgap for "the error does not name the outstanding condition". It repairs nothing: no ACL is written, no privilege is requested, nothing is elevated, no environment variable is set for another process, no preset is installed and no mode is changed.

The seven failures it recognizes

The first two are recognized on the public tools/post-execute waterfall (@deepseek-ai/dsh-tools) from the failure text. That seam is the one that has all three of what a diagnosis needs: the failure (providers propagate their error unchanged and the tool pipeline settles it as an isError result), an agent identity to attribute it to (exec.agent), and a channel that speaks to the model in the same step (PostToolDecision's additionalContexts, which the agent loop turns into a durable user-role message β€” packages/core/agent-loop/src/tool-calls.ts). The third, fourth and sixth are recognized at the same seam from the canonical value of a result the pipeline calls a success, for reasons Β§3, Β§4 and Β§6 give in full. The fifth and the seventh are read from a thrown error's own message like the second; the fifth is the one family the plugin also reports before anything fails, for reasons Β§5 gives in full.

That seam, not ctx.sandbox.confine: confine(argv, policy, signal) sees the confinement failure too, but its signature carries no agent, so a wrapper could detect the condition and never deliver a word about it to the session that is stuck.

1. Workspace provisioning β€” the Windows ACL failure (acl-provisioning)

Twelve reports describe this exact line: [discussion #7538], [discussion #7622], [discussion #7646], [discussion #7720], [discussion #7750], [discussion #7735], [discussion #7771], [discussion #7804], [discussion #7816], [discussion #8232], [discussion #8272] and [discussion #8275] (three of them β€” [#7750], [#7735], [#8232] β€” on data-volume workspaces, where no ACE names the caller at all: the inherited Authenticated Users: Modify is the whole of their access). In each one every sandboxed command fails the same way, before it runs, and the error names neither the missing right nor a remedy.

Two further reports β€” [discussion #8312] and [discussion #8314] β€” and a third, [discussion #8412], describe the other end of the same backend: what its grant leaves behind after it applies. The advisory states that too (see What the grant leaves behind), because the advisory is the thing handing over the command that applies the grant.

#8232 contributes two facts about the shape of the failure rather than its cause, and both are in the advisory now. One is that the failure belongs to the workspace, not the command: there a Get-Date failed exactly like anything that writes, because the grant is materialized before the command runs at all. The other is the scope of the repair: the (OI)(CI)(WO) line covers this directory and its children, so a second workspace root on the same volume is a sibling rather than a child and needs the same line once more β€” which is why that reporter saw a second workspace fail on a machine whose first one was already repaired. The report also reaches the same root cause on its own (the merged write wanting WRITE_OWNER, which owner-implicit rights do not carry), matching the backend's documented prerequisite.

#8272 and #8275 add neither a cause nor a remedy β€” both land on the same missing right β€” but each one closes a reading the diagnosis leaves open, and both are in the advisory since 0.9.0.

#8272 reached the same conclusion by a better probe than the one above: it set a Low integrity level on a directory it owned, unelevated (icacls <dir> /setintegritylevel "(OI)(CI)Low"), and was refused. That isolates the label half of the merged call, which is the half that needs WRITE_OWNER, and it does so without asking anyone to interpret a merged failure. It is quoted here as evidence and deliberately not offered as a check: where the caller holds Full control the same command succeeds, and then the label β€” and its inheritance β€” is already written. A diagnostic that writes when it succeeds is not a diagnostic this plugin hands out; the two read-only checks above are.

The report then went looking for the backend's own repair (diagnose-windows-sandbox-acl) on a 0.1.7-rc.2 install and found it missing, which it read as the package having dropped the skill from its files glob. It has not: measured on the published tarballs (2026-09-29), 0.1.7-rc.2 names the skill zero times in either README, ships no assets/ directory at all, and carries no file containing the registration symbol β€” while 0.2.0-rc.2 ships assets/diagnose-windows-sandbox-acl/{SKILL.md,scripts/diagnose-windows-sandbox-acl.ps1} and names it three times per README. The skill is new in 0.2.0; a 0.1.7 install that found the name was reading a 0.2.0-era document. So the reach that works is the upgrade, not a packaging fix, and the advisory says so β€” item 4 below.

#8275 is the other direction: it had the skill, and the skill's own remedy (Full control) worked, after which it noticed that the Low label the successful apply writes is what regresses the two side effects it documents. Its proposal is to degrade provisioning to DACL-only when the label is the half being refused, which reads as though the label were one of two independent layers. It is not, and the advisory answers that with the mechanism rather than with a refusal β€” item 6 below.

#7720 is worth reading for where the failure lands: the grant is materialized at sandbox initialization, so this is not one refused operation but every shell tool at once β€” the reporter could not run netstat or even icacls to diagnose the error they were staring at (on 0.1.5-rc.3 the same directory worked, because confinement was skipped silently rather than failing closed). They also report the two remedies that look right and are not (takeown /F <dir> /R /D Y, icacls <dir> /reset /T /C), which is why the advisory names them with the reason each fails instead of leaving the reader to discover it.

The Windows backend provisions a workspace by writing the directory's DACL and its mandatory-integrity label in one SetNamedSecurityInfoW call (packages/sandbox/sandbox-windows-acl/src/acl.ts):

if (applyResult !== abi.ERROR_SUCCESS) throwWin32(api, 'SetNamedSecurityInfoW', applyResult, `${label}(${path})`)

Two consequences follow from that one line:

  1. The label lives in the SACL, and its half is what gets refused. The owner's implicit rights cover only READ_CONTROL and WRITE_DAC, so the combined apply additionally needs WRITE_OWNER on the directory β€” an object right, which a Full-control directory (the normal workspace case) has and a mkdir-created one inheriting "Authenticated Users: Modify" (0x1301bf) does not. This is the backend's own documented prerequisite ("granted directories must be caller-owned and grant WRITE_OWNER").

  2. It is not SeSecurityPrivilege, and elevation is the wrong lever. That is the token-privilege form of the same idea, and it is the hypothesis the reports naturally reach for β€” whoami /priv cannot tell the two apart, because WRITE_OWNER is an object right and never appears in that table. Granting WRITE_OWNER on the workspace root needs no elevation. #7735 settles the gate with an isolation table on one machine and one unprivileged account: the label write succeeds with Full control or with Take-ownership alone, and fails with ChangePermissions, ReadPermissions or Modify alone β€” so the gate is WRITE_OWNER and nothing else.

  3. The remedy is the narrowest form of that right. The advisory recommends icacls <dir> /grant "<user>:(OI)(CI)(WO)" β€” WO is WRITE_OWNER, i.e. literally the right the documented prerequisite names, so the one-liner grants nothing the harness did not ask for β€” and offers Full control second, as the same line with F in place of (WO). Both assume the caller owns the directory: owner-implicit rights cover the DACL half of the merged write.

  4. The version boundary is stated, because the error cannot carry it. Up to 0.1.6-alpha.x the backend's SetNamedSecurityInfoW wrote the DACL only (flag 4) and a Modify-only workspace provisioned fine; the mandatory label β€” and with it the SACL, flag 20 β€” arrives in 0.1.7-alpha.1. So the same string on an older line belongs to a different cause space, and the natural reach (downgrade to the build that "worked") is refused with its reason: the label is what confines deletes to the workspace, and reverting it reintroduces the escape it closed. #7750 asks for exactly this and explains why it is a usability regression traded for a security fix.

    There is a second boundary on that same line, and #8272 is what made it worth stating: the backend's own repair, diagnose-windows-sandbox-acl, is not part of 0.1.7-* at all β€” it arrives with the 0.2.0 line, where the package starts shipping it under assets/. A reader who found the skill named in a README while running 0.1.7-rc.2 was reading a 0.2.0-era document, not a package that dropped something: that release's own README names it zero times and no file in its tree carries the registration symbol. So the useful reach is the upgrade, and the unhelpful one β€” repairing a files glob in a release that has no such directory β€” is not offered.

  5. The repair is per-directory, and the report is what established that. #8232 applied the (WO) line, watched the workspace start working, and then hit the same error on a second workspace root on the same volume. (OI)(CI) carries the ACE into children of the directory that received it and nowhere else, so a sibling root is untouched by it. One line per workspace root is therefore the correct shape of the remedy, not one line per machine β€” and the advisory says so, because the natural reading of "it worked" is "it is fixed".

  6. A "weaker grant" is a mechanism this backend cannot express, not a policy it declines (added in 0.9.0, from #8275). The label is what makes the apply a SACL write, so declining it looks like dropping the expensive half β€” but there is no DACL-only path to fall back to: the label rides the same SetNamedSecurityInfoW as the grant (acl.ts: the flags are DACL_SECURITY_INFORMATION alone only when the label edit is keep, and grantWrite's apply branch always passes apply), and the idempotence fast path requires the exact label among its three conditions. And dropping the label alone would not leave a working workspace: the confined token is itself lowered to Low before any child starts (token.ts's restrictTokenIntegrity, whose own comment calls Low "the level the mandatory labels grantWrite applies are matched against"), so the directory's Low label is what lets that Low child write there at all under no-write-up. A DACL-only mode that keeps the token lowering yields a workspace the sandbox can start a command in and the command then cannot write to; one that drops the token lowering too β€” which is what #8275's own appendix records a community patch having to do β€” gives up half the confinement rather than one of two independent layers. The advisory says this instead of a bare "no", because a reader told only "no" reaches for the workaround without knowing what else it has to change.

  7. The missing right was confirmed from outside, unelevated (added in 0.14.0, from #8426). Everything above is argued from the backend's source; #8426 measured it with none of that. On a directory whose ACL named the account only through the inherited Authenticated Users:(M) entry, asking that directory for a Low mandatory-integrity label β€” the label half alone, no DACL write, so it isolates the right the merged call wants β€” was refused, and the identical operation succeeded the moment the same account granted itself (OI)(CI)F, whose mask carries WRITE_OWNER, and was reversible from there (back to Medium works just as well). No elevation anywhere in it. That makes the claim in item 2 a measurement rather than a reading: the missing right is an object right on the directory, not SeRelabelPrivilege and not a token privilege. The same report then repaired two real trees the same way β€” object right first, integrity label second, 9521 and 5592 objects, no failures β€” an order that cannot be swapped, since the second step is the one that needs what the first supplies, and it is the order the advisory's own remedy already follows. The probe itself is quoted here and not in the advisory, for the reason item 6 of the #8272 paragraph above gives: a label write is not a check, because where the caller already holds Full control it succeeds and writes the label. The advisory hands over only the two read-only checks.

The grant is materialized lazily, on the first confined call, and nothing is cached when it throws β€” so the same failure repeats per command (850 calls across 39 sessions in #7622; 52,588 output tokens with no output in #7538), which is why the loop cannot separate it from ordinary command noise.

On the first recognized failure, the result is enriched with:

Sandbox provisioning failed β€” no sandboxed command can run in this workspace until its ACL applies.

What was reported:
  SetNamedSecurityInfoW failed (Win32 5): grantWrite(D:\ws)

Why it is refused while the directory looks writable: that call is a MERGED write ...
  ... the label half additionally needs WRITE_OWNER on the directory. ...

A version boundary worth knowing before reaching for an older build: the label half is new to this package.
Up to `0.1.6-alpha.x` the backend touched the DACL only (flag 4) ... `0.1.7-alpha.1` is where the mandatory
label β€” and with it the SACL, flag 20 β€” arrives. ... Rolling back is not the fix either: the label is what
confines deletes to the workspace, and reverting it reintroduces the escape it closed.

The other boundary on that last line: the `diagnose-windows-sandbox-acl` skill is not part of `0.1.7-*` at all
β€” it arrives with `0.2.0`, where the backend starts shipping it under `assets/`. The reach that works is the
upgrade, not a packaging fix.

Confirm the cause (unelevated) β€” `icacls` is a normal user command:
  icacls "D:\ws"

Ownership decides which of the two commands below can work, so read it first β€” PowerShell 5.1 or later:
  (Get-Acl "D:\ws").Owner      # compare with: whoami

IF YOU OWN THE DIRECTORY β€” the usual workspace, on a data volume as much as on C::
  one unelevated line, then run the command again:
  PowerShell: icacls "D:\ws" /grant "$env:USERNAME:(OI)(CI)(WO)"
  cmd:        icacls "D:\ws" /grant "%USERNAME%:(OI)(CI)(WO)"
  ... Full control works just as well β€” the same line with `F` in place of `(WO)`

IF YOU DO NOT OWN IT β€” a directory an installer or another account created, e.g. owner
`BUILTIN\Administrators`:
  the line above cannot run at all. Changing a DACL takes WRITE_DAC, which you hold neither as owner nor
  through any ACE, so `icacls /grant` is refused with `Access is denied` β€” for the very command that would
  fix it. ... Run the grant once from an account that already holds both β€” that is, from an ELEVATED prompt:
  icacls "D:\ws" /grant "<your-account>:(OI)(CI)F"
  ... or take ownership first (also elevated; it wants SeTakeOwnership), after which the unelevated `(WO)`
  line above applies: icacls "D:\ws" /setowner "<your-account>"
  ... or sidestep the ACL: create the workspace under `%USERPROFILE%`.

What will NOT fix it on its own β€” both look like the right move, and both were tried and reported:
  takeown /F "D:\ws" /R /D Y
    makes you the owner, and ownership's implicit rights are READ_CONTROL and WRITE_DAC only β€” so it
    supplies the DACL half and still not WRITE_OWNER, the right this call needs.
  icacls "D:\ws" /reset /T /C
    restores inheritance, and inheritance is what supplied the Modify-only ACE above. Where it strips the last
    entry naming you, the directory ends up in exactly the state the diagnosis above describes β€” its only access
    the inherited `Authenticated Users:(M)` (#8314 measured that follow-on failure).

What the grant leaves behind, once it applies β€” worth knowing before you run the command above, because the
backend does not take it back:
  The three entries are STANDING, deliberately, and nothing revokes them. ... They outlive the session and the
  harness exiting (`sandbox-windows-acl/src/grant.ts`, `src/index.ts`).
  The Low integrity label is INHERITABLE (`(OI|CI)`) and it lives in the SACL β€” which is why the `icacls /reset`
  above does not take it off: that command rebuilds the DACL. Windows starts a process at the minimum of the
  user's and the program's integrity, so anything started from a tree the harness has written to runs at LOW
  integrity, and none of the symptoms names DSH (#8312 collects them): ... (#7709), ... (#8175), ... (#7735).
  It can also leave the workspace. An NTFS hard link is a SECOND NAME for one file object, so both names share
  one security descriptor β€” and a pnpm workspace is largely hard links (`node_modules` pointing into a
  content-addressed store on the same volume). ... `vite build` unable to remove its own temp file, `pnpm
  install` unable to replace a hook (#8314 measured the whole chain). ...
  It is also why the label is not removable by the commands above: removing an integrity label needs
  `WRITE_OWNER`, and the maintainers' own `diagnose-windows-sandbox-acl` skill reports it without removing it.
  A procedure that does remove it exists, and the advisory quotes it as its reporter's rather than this
  project's β€” with the boundaries that come with it ([#7846]; see the section below).
None of this makes the command above the wrong move β€” without it, nothing sandboxed runs in this workspace. It
is what the harness does to a directory it has been pointed at, and it is worth knowing before rather than
discovering it as a broken build in some other project later.

One thing to know before asking for a weaker grant, because that is the next idea after this diagnosis β€”
and it is not a smaller version of the same grant:
  The label rides the SAME `SetNamedSecurityInfoW` as the DACL, so there is no DACL-only path to fall back
  to β€” it would have to be built. And dropping the label alone would not leave a working workspace: the
  backend lowers the confined token to Low before any child starts, and the directory's Low label is what
  lets that Low child write here at all. Declining the label usefully means declining the token's Low level
  with it, which gives up half the confinement rather than one of two independent layers.

Why the remedy forks (added in 0.5.0). The same error covers two different rights situations, and one command cannot serve both. Where the caller owns the directory, the owner's implicit WRITE_DAC satisfies the DACL half of the merged write, WRITE_OWNER is the single missing right, and the unelevated icacls /grant that supplies it can itself run β€” [#7750] measured exactly that fix working. Where the caller does not own it ([#7771]: owner BUILTIN\Administrators, held deny-only for that token), WRITE_DAC is missing too, so the very same command is refused before it does anything, and (WO) alone would not be enough even if it went through. The failure text is identical in both, so the classifier cannot pick a branch β€” the advisory hands over the ownership check as the selector instead of guessing, which is also the actionable-guidance half of what [#7771] asked for. Until 0.5.0 a single unconditional one-liner was printed, with a sentence noting it assumed ownership; that would have sent the second environment to a command that is denied β€” the same defect this plugin exists to answer.

What the grant leaves behind (added in 0.10.0; the recovery quoted as its reporter's since 0.21.0)

[Discussion #8312] and [discussion #8314] report the other half of this backend, and the advisory now states it β€” before the reader runs the command it is being handed, because that command is what makes the backend's grant apply:

  • The three entries are standing, by design, and nothing revokes them. The workspace grant is a reuse cache: the backend's dispose path revokes the revocable (temp) grants and leaves the workspace edits, and its fail-closed cleanup says the same in plainer words β€” standing ACEs "are NOT revoked β€” they are the intended end state (the reuse cache), not an error artifact" (src/grant.ts, src/index.ts). They outlive the session and the harness exiting. ([#8312] arrived at this from the README's own description of the cache; the source is quoted in the advisory.)
  • The Low integrity label is inheritable, and it lives in the SACL. That is why icacls /reset β€” already listed as a non-fix for a different reason β€” does not remove it: the command rebuilds the DACL. Windows starts a process at min(user, image) integrity, so anything started from a tree the harness has written to runs at Low integrity, and none of the symptoms points at DSH: an Electron/Chromium app exiting 0x80000003 with no output ([#7709]), msbuild / dotnet / npm refusing or warning about the files as if they came from the Internet when no Zone.Identifier exists ([#8175]), a double-clicked .exe / .cmd reporting "publisher could not be verified" ([#7735]).
  • It can leave the workspace. An NTFS hard link is a second name for one file object, so both names share one security descriptor β€” and a pnpm workspace is largely hard links (node_modules pointing into a content-addressed store on the same volume). The inheritable label therefore lands on the store's objects and stays there, after which every project building from that store gets executables that start at Low integrity and failures that name the build tool ([#8314] measured the whole chain: vite build unable to remove its own temp file, pnpm install unable to replace a hook). The backend's own suite pins the reach as a known boundary β€” "a workspace hard link lets the grant reach an external file object" (tests/runner.spec.ts) β€” and its README calls refusing multiply-linked files unviable for ordinary pnpm installs, which leaves the out-of-tree reach open rather than unknown.
  • A removal procedure exists, and it is a user's rather than ours. The maintainers' own diagnose-windows-sandbox-acl skill reports LOW_LABEL and by design does not remove it, and removing an integrity label needs WRITE_OWNER β€” the same right this whole failure is about. But [#7846]'s reporter measured one end to end on a real tree, and the advisory hands it over as theirs, with its boundaries: back the ACLs up (icacls <dir> /save), drop the world deny with icacls <dir> /remove:d "Everyone", remove the capability ACE through the ACL API β€” icacls cannot name an S-1-4-x-y SID at all and answers ERROR_NONE_MAPPED (1332), so a PowerShell (Get-Acl).Access / RemoveAccessRule / Set-Acl is the only route β€” and relabel with icacls <dir> /setintegritylevel M /T (85 613 objects, no failures). What it leaves behind is an explicit Medium mandatory label, not the absence of one; the relabel visits the whole tree; and the label edit needs WRITE_OWNER per object. This project still verifies no line of it on a Windows host, which is why it is quoted rather than shipped as our own command β€” the same standard the hard-link reach above is held to.
  • read-only writes no label. The grant and the label it carries are applied only where the seam materializes a writable root: the session-scoped path returns before that point for read-only, and the runner builds its sandbox with an empty writable list, so the loop that calls the label writer has nothing in it. A label found after a read-only session came from an earlier workspace-write period on that directory β€” which is a different statement about who is exposed, and worth separating before attributing the symptom.
  • What was priced is the other direction. The decision record's own cost line says the standing Low label "widens the workspace tree for ANY other process running at Low integrity as the same user", and this package's known limitations say it plainer β€” such a process "can write and delete inside the workspace, where a Medium label would have denied it". Both describe a loosening. Neither prices what [#7846] measured: the user's own Medium-integrity toolchain losing the tree.

The section is emitted for every ACL class, like the version boundary and for the same reason: it is a fact about the package's grant, and that grant is the remedy the advisory hands over in all three classes. It also closes with what the fact does not mean β€” the command is still the right move, because without it nothing sandboxed runs at all.

What is deliberately not shipped: icacls ... /grant "<user>:(OI)(CI)(WD,WO)", the two needed rights named explicitly. It is the tighter form and it is plausibly correct syntax, but this project has no Windows host to run it on, and shipping an unverified command in a remedy whose whole point is that it works is the failure mode being fixed. F (verified by [#7804]'s reporter) and /setowner (named by both reports) are given instead.

2. Persistent shell startup (pty-startup)

[Discussion #7638] reports the second shape: with the minimal preset on Windows, and under a confining sandbox mode (workspace-write / read-only, not danger-full-access), every shell call dies instantly with

PTY shell exited during startup

The terminal backend spawns the shell through the sandbox (packages/terminal/terminal-bash/src/index.ts; the throw is in src/session.ts and src/index.ts, both on the same waitReason === 'session_exit' branch), and there the pseudo-console cannot be created at all, so the child exits before its first prompt. Retrying never helps; the message points at no cause.

The reporter's own three-arm control makes the sandbox mode the discriminator: minimal Γ— confining fails, minimal Γ— danger-full-access succeeds, standard (one-shot shell) Γ— confining succeeds. That is why the advisory is only ever built with the resolved mode the failing call actually ran under β€” from ctx.sandboxPolicy.resolve({ session }), the same resolver the terminal layer calls before spawning, with the same session.

Inside a confining mode, the host binary decides (added in 0.10.0). [Discussion #8322] ran the control one level deeper β€” same runner, same ConPTY, every arm β€” and separated what the mode alone does not: with the runner hosted by a plain console-subsystem node.exe, the confined shell starts and its prompt and shell-integration marks are correct; with the runner hosted by the packaged desktop's GUI-subsystem Electron executable, the child dies silently β€” zero bytes on stdout and stderr, and the non-interactive arm exits 0 with everything it printed lost. It is the same rule the 0xC0000142 family states for its own case: under the restricted token a console can be inherited but not created, so the binary that owns one (or owns none) is what the arms turn on. That is also why the same version behaves differently depending on how it was started: the desktop app fails where the Web UI launched from a terminal β€” whose process.execPath is a real node.exe β€” is reported working under the same confining mode ([#8313], whose sibling report is the 0xC0000142 shape of the same host difference). The advisory therefore names the host alongside the mode, says the outcome is deterministic per (session mode Γ— host) rather than intermittent, and adds that the mode which counts is the one the session records, not the one the environment now holds β€” a session that recorded the confining mode keeps failing after the app is restarted with another mode in its environment, while switching it inside the session takes effect at once.

[Discussion #9170] (added in 0.16.0) supplies that host split from the user's own side: on one machine the identical confined command works under dsh web and fails under the packaged dsh desktop, with the mode and the command held fixed β€” the comparison the three most recent 0xC0000142 reports could not make, because those machines only ever ran one app. It also corrects the conclusion that comparison invites: switching the session to read-only is not a reliable way out, because it goes through the same restricted runner started from the same host binary β€” it changes what is granted, not who spawns the child β€” and the one report where read-only succeeded while workspace-write died was driven through the sandbox API from a real node host, not from the packaged desktop app. Worth one try, not worth counting on; a host that owns a console remains the first move.

The advisory that follows is addressed to two different readers:

Persistent shell failed to start β€” command execution is unavailable in this session, and retrying cannot fix it.

What was reported:
  PTY shell exited during startup

The `bash` tool is a PERSISTENT PTY session (a shell that stays alive between calls), and this session's sandbox mode is
`workspace-write` β€” not `danger-full-access`. A confining mode spawns the shell through the sandbox, and there the
terminal backend cannot create the pseudo-console at all, so the child exits before its first prompt. ...

Which sessions fail inside that combination is not chance β€” it is deterministic per (session mode Γ— the host
binary carrying the sandbox runner) ... Measured against the desktop build with the same runner and the same
ConPTY in every arm (#8322):
  - runner hosted by a plain console-subsystem `node.exe` β†’ the confined shell starts ...;
  - runner hosted by the packaged desktop's GUI-subsystem Electron executable ... β†’ the child dies silently ...
The rule behind both this and the `0xC0000142` family ... is the one stated there for its own case: under the
restricted token a console can be INHERITED but not CREATED ... And the mode that decides is the one the SESSION
records, not the one the environment now holds ...

Do NOT retry, and do not look for a command that fixes it: every attempt will fail identically, and there is no
shell to run a command in. Use your file read/write tools instead, and hand the choice below to the user.

What unblocks the session β€” the user's decision, not the model's:
  1. switch the agent preset to `standard`, whose shell tool is a one-shot subprocess (no PTY) and works
     under the sandbox; or
  2. override the `preset-minimal` row in your profile patch β€” `$DSH_HOME/profiles/<profile>/cordis.patch.yml`, or
     `$DSH_HOME/cordis.patch.yml` for every profile β€” replacing its `persistent-shell` group with
     `@deepseek-ai/dsh-tool-pwsh` (a one-shot subprocess, no PTY); the patch layer is yours, so an upgrade
     will not overwrite it; or
  3. run the session from a host that owns a console instead of the packaged desktop app β€” the Web UI started
     from a terminal (`process.execPath` is a real `node.exe` there) was reported working under the same
     confining mode and the same version (#8313); or
  4. run the session with `danger-full-access`, which drops the very confinement the sandbox exists to give.
     Prefer 1 to 3.

The model's instruction is to stop β€” not to run a command (there is no shell to run it in) and not to call a fallback shell tool (minimal mounts exactly one platform-selected persistent shell and no one-shot shell, by design: .agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md). Naming a tool the failing composition does not mount would be a wrong remedy, which is the main risk this family's text is written to avoid.

The remedy is a patch layer, not a directory. The pre-declarative $DSH_HOME/.agent-presets/<id>/ preset folder is a plausible-looking trap: it still reads as the natural place to put a preset, and nothing in the harness reads it any more (@deepseek-ai/dsh-agent-preset-registry: the registry "neither scans directories nor accepts preset paths"). Preset changes are @deepseek-ai/dsh-agent-preset rows β€” an insert for a new one, a patch keyed by row id (preset-minimal) for a change to a shipped one. A test arm asserts the advisory never names the dead directory.

3. A confined child that never started (native-init)

Thirteen reports of one exit code: [#7876] and [#8193] (the packaged desktop app), [#8313] (the same version and the same mode run as the desktop app versus the Web UI launched from a terminal, which works), [#8334] (the same code with the authorization side attached: the grant succeeded and every confined child still died), [#7877] (MSYS2 / Git Bash), [#8990] and [#8991] (the same code on two more Windows builds, with no dependence on the PowerShell version or the install layout, and β€” for those machines β€” no way to tell which producer it was) and [#9186], which isolated it to a single input β€” and [#8208], which found the mechanism the console cases share, [#8336], which measured the creation flags that mechanism turns on, [#9238], which isolated the mechanism's input by varying only the runner's console topology, [#9336], which filed the combination a bare mode switch mis-attributes and the two costs that go with it, and [#9467], which turned the third producer's input into a rule β€” measured on Windows 10 19044, so the build number is not the variable. All are 0xC0000142 STATUS_DLL_INIT_FAILED β€” the Windows loader terminated the process while it was initializing its native images, i.e. before the program's entry point. A command that ran and then failed exits with its own status and prints its own output; this one produced neither.

Two reports filed in the same wave are scope, not members, and the advisory names them as such rather than folding them in: [#9463] is the same backend stalling before any child is spawned at all (no approval/request event, no process), and [#9471] is a child that does start and then fails to load a library at runtime, dying with E_ACCESSDENIED (0x80070005) and a message of its own instead of this bare status. Neither carries 0xC0000142.

what was run what came out exit code
cmd.exe /c "echo cmd-ok" cmd-ok 0
pwsh -NoLogo -NoProfile -Command "Write-Output pwsh-ok" pwsh-ok 0
D:\Git\bin\bash.exe -c "echo bash-ok" couldn't create signal pipe, Win32 error 5 -1073741502

Three producers have been measured under a confining mode:

  1. An MSYS2 / Git-Bash program ([#7877]). The restricted token's runtime cannot create the pipe it uses for signals, so bash aborts in the loader phase, while cmd.exe and pwsh run fine in the same workspace under the same mode. The plugin's own composition has no way around this: tool-bash and bash-sandbox are disabled on win32 (@deepseek-ai/dsh-base/cordis.patch.yml), so the combination is likely never covered upstream. The one conversion a model can make itself is to write the same work as a PowerShell or cmd command.

  2. The packaged desktop app's sandbox runner ([#8193], [#8208]). dsh-sandbox-local launches the runner as [process.execPath, entry], and in the packaged build process.execPath is the Electron executable. The mechanism is the runner's console, not its token ([#8208]): the confined child inherits a console from the runner, and a runner that owns none leaves the child to ask for one of its own β€” which a restricted token may not have. The capture is three lines: conhost.exe is created by the restricted child, exits 0xC0000022 STATUS_ACCESS_DENIED, and the child then dies with 0xC0000142. Two console-less configurations are measured, and they share that mechanism: (a) the host binary is a GUI-subsystem program, which never owns a console β€” the packaged desktop, where every confined command dies this way ([#8193]); (b) the host binary is a real console-subsystem node.exe and the runner was still spawned without a console, because DETACHED_PROCESS was set β€” spawnSync(node, [runner, …], { detached: true }) returns 0xC0000142 where the identical call without that flag returns 0 ([#8208]). Shape (b) is the one worth handing over, because it needs no desktop and no particular machine. What is not the discriminator is the token: [#8208] compared whoami /groups and /priv from children of a working node host and of the failing Electron host and found them identical, and a low-integrity cmd.exe runs fine on that machine. The plugin reports whether this process is an Electron binary (process.versions.electron) as a measured fact rather than assuming it.

    Since 0.18.0 this axis has its own measurement, and it is the reason the third producer's check is two steps rather than one. [#9238] built a ~90-line Win32 launcher that varies only the runner's console topology, held everything else β€” machine, workspace, temp directory, mode, argv, restricted token β€” constant, and ran six arms on one machine: DETACHED_PROCESS (no console at all) died with 0xC0000142 and zero output, while CREATE_NO_WINDOW (Windows allocates the runner an invisible console), CREATE_NEW_CONSOLE, inheriting an existing console, and a hidden-console wscript β†’ cmd carrier all reached the program with its stdout intact, and the same DETACHED_PROCESS launch without the restricted token was fine. So the console is not an inference from two host binaries any more: it is a variable one launcher can turn on and off, which is what makes "does the runner own a console" a check rather than a suspicion about a build. The same report states this family's CI blind spot in one sentence and it is worth quoting in the plugin's own words: the repository's Windows checks run in a console-bearing chain β€” the one configuration the matrix shows working β€” so a green check there cannot falsify this producer. The clarification [#9238] adds to the backend's recording is the distinction the flag vocabulary below turns on: CREATE_NO_WINDOW on the restricted child is fatal, while CREATE_NO_WINDOW on the runner is the fix, because Windows then gives the runner an invisible console for the child to inherit.

    0.7.x named two shapes and explained them with the wrong mechanism, and 0.8.0 withdraws one of the shapes outright. The withdrawn shape is "the runner does not start at all, because ELECTRON_RUN_AS_NODE=1 is missing": the desktop sets that variable on its own host child (apps/desktop/src/host-process.ts β†’ desktopNodeEnvironment(), apps/desktop/src/node-environment.ts:15), so nothing on the runner path can fail to start for want of it β€” and [#8208] measured the variable present in both hosts, including the one that works. The token story goes with it, for the same measurement. A cause the advisory shipped twice earns its sentence when it is retracted, and test arms keep both retractions in place.

    The remedy is a real node host β€” with one condition riding on it. [#8193] measured the way out: hosted on the desktop's own bundled standalone node (resources/runtime/primary-runtime/dependencies/node/bin/node.exe, v24.21.0) the same confined pwsh.exe / cmd.exe spawns succeed (exit 81 / 82), with workspace, temp directory, mode, SIDs, target and runner sha256 all held constant. [#8208] reproduced it (exit 0, the command's own stdout intact) on the same runtime installed for workspace dependencies (%USERPROFILE%\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\node\bin\node.exe, present once load_workspace_dependencies has run) and with windowsHide both set and unset. The condition is that the runner must be spawned with a console β€” not with DETACHED_PROCESS β€” because a real host alone is not enough when that flag is set ([#8208]): the host binary supplies the console and the spawn flag is what can take it away again. Where no real host can be put in front of the runner, the same effect fits inside it: AllocConsole before the restricted spawn, with the three standard handles restored afterwards, in dsh-win32-process's createRestrictedProcess β€” the funnel every restricted child goes through. [#8208] measured cmd /c exit and pwsh -c "Write-Output …" both reaching 0 with stdout intact under that change, and as a no-op on a runner that already owns a console; it costs a user32 binding and one console host per runner process. danger-full-access is demoted to what it actually is β€” a way to confirm the diagnosis, not a fix, and on this platform one that silently removes the sandbox from every shell call. The reporter's own reason is the one the plugin repeats: "the practical effect is that Windows Desktop users must escalate to full access for all shell work, which silently removes the sandbox on that platform", and they asked explicitly that this not be "fixed" with --disable-sandbox / --disable-gpu-sandbox β€” those disable Chromium's renderer sandbox, a different layer from the DSH file policy. The advisory also warns off the opposite-looking move: unsetting ELECTRON_RUN_AS_NODE does not help, because the desktop's runner is that Electron binary and dropping the variable would take runner.js down with it β€” the interaction [#8193] records with [#8174], where a fix that tombstones the variable in the shared child environment would take the ACL runner down with it, so the two changes have to land together.

    Added in 0.23.0 β€” the same producer, isolated one input narrower. [#9626] held the token recipe, the grants, the integrity lowering, the stdio shapes, the workspace and the command fixed and varied only the process image hosting the runner: the packaged DeepSeek Harness.exe (with ELECTRON_RUN_AS_NODE=1) dies with 3221225794, while a real node.exe running that same runner with those same arguments exits 0. Its controls close the neighbouring readings β€” the outer subprocess runner's image is irrelevant (Electron outside + node ACL runner succeeds; node outside + Electron ACL runner fails), a token inherited from an Electron parent is harmless, --write-sid / --temp-write-sid are not it, cmd.exe / node.exe / pwsh.exe all die as targets, and windowsHide is not it β€” so what decides is the image that mints the token. The report also names the line the product still assumes otherwise: the invocation is built as [process.execPath, runner.js] (packages/sandbox/sandbox-local/src/index.ts), whose own docstring promises "the prefix stays [node, runner, ...]" and whose suite asserts windowsAclRunnerInvocation always yields [node, ...] in product β€” which the packaged build is the one form to falsify, because there process.execPath is the Electron executable and not a Node binary. And it supplies a runtime unblock the advisory carries as the reporter's, not as the fix: the provider re-reads its windowsAclRunnerArgs test hook on every call, so a host-side plugin can point the invocation at the packaged node.exe plus a copy of runner.js living outside app.asar (a plain node.exe cannot read the archive) and the next confined call is on a real host. It is a test hook rather than an interface, which is why the fix stays the line above. A sibling report of the same shape, filed by a second author twenty seconds later, is not tracked separately.

    What the flag vocabulary actually is. The backend's own source records one inherent boundary: "console isolation is unavailable β€” children share the host console (CREATE_NO_WINDOW / CREATE_NEW_CONSOLE children die with STATUS_DLL_INIT_FAILED under the restriction)" (packages/sandbox/sandbox-windows-acl/README.md:119), echoed at packages/subprocess/win32-process/src/process.ts:454. [#8208] explains that recording instead of repeating it, and the explanation is the invariant the two shapes share: in a restricted token a console can be inherited but not created. The creation flags actually passed are three sets and none of them is CREATE_NO_WINDOW β€” 0 on the piped path (process.ts:243, the path a shell call takes), CREATE_SUSPENDED on the inherited-job path (process.ts:542) and CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT on the ordinary path (process.ts:567, i.e. 0x404); CREATE_NO_WINDOW (0x08000000) is not a constant anywhere in that source. Those flags are fatal under a console-less runner and harmless under one that owns a console β€” so there the console decides. 0.7.x wrote the two-set version of that list and called the flags "a necessary ingredient ... the host process image is what turns it fatal"; both halves were wrong, and 0.8.0 replaces them.

    0.11.0 and earlier concluded too much from those three sets β€” "it is the console and not the flag list that decides" β€” and [#8336] measured the case that falsifies it: with a console-owning host, a restricted token and the Low integrity level, varying only the creation flags, CREATE_NO_WINDOW and CREATE_NEW_CONSOLE both died with 0xC0000142, while 0, DETACHED_PROCESS and CREATE_NEW_PROCESS_GROUP reached the program. So the console decides whether a flag that merely shares the inherited console is fatal, and a flag that forces the child to create one of its own is fatal regardless. The pair inside that matrix is what makes it a rule rather than a list of forbidden flags: the same CREATE_NO_WINDOW on an unrestricted token reached the program, and STARTF_USESHOWWINDOW with SW_HIDE β€” how the harness hides a window without isolating a console β€” was harmless on the restricted one. 0.12.0 replaces the withdrawn sentence with the rule and that matrix.

    The windowsHide suspicion, answered. It is the first thing a search turns up for this failure, and it points at the wrong component: the flag appears once on the ordinary subprocess path (packages/subprocess/subprocess-local/src/spawn.ts:472, windowsHide: platform === 'win32'), which starts the runner rather than the confined child, and the restricted spawn defines no CREATE_NO_WINDOW constant at all. It is not set in dsh-jobs β€” a search there finds nothing, and every windowsHide in the tree sits on a host-side or ordinary spawn. And where it is set, [#8208] measured it both ways on a host that works: with and without windowsHide, the confined pwsh reached exit 0 with its own stdout intact. The reason is the rule again β€” a windowless console is still a console, and that is what the child inherits; what decides is whether the host owns a console object, not whether it owns a window.

    One arm was applied, measured, and rejected. Putting DETACHED_PROCESS on the restricted child removes its console request and does stop the crash β€” but pwsh then exits 0 with zero bytes on stdout and stderr, while cmd.exe keeps its output ([#8208]). That trades a loud failure for a silent one on the interpreter most likely to be used, so the advisory names it as a non-remedy instead of offering it; the runner-side remedies keep the output.

  3. A capability SID inside the restricted token's own restricting list ([#9186], added in 0.16.0). The two producers above are properties of the program and of the host; this one is a property of the token, and that is why it is the hardest to see: the reporter drove the sandbox API directly, with the runner hosted by a real node binary and the DACLs left untouched, and held everything fixed but one input β€” the restricting list the restricted token is built from. With the workspace-write list β€” [logon SID, EVERYONE] plus one capability SID β€” every program died this way, whoami.exe and cmd.exe as well as pwsh; with the read-only list, which carries no capability SID, the same program started normally in both stdio shapes (piped and inherited). The capability SIDs are derived per workspace and per private temp directory and join the list only under workspace-write (sandbox-windows-acl/src/token.ts: createRestrictedToken() is verbatim read-only ? [logonSid, world] : [logonSid, world, ...writeSids]), so read-only carries none by design.

    Since 0.20.0 that input has a rule, and the rule is what makes the producer structural rather than tunable. [#9467] changed only the third entry of the restricting list inside one host and got the same child, sixteen times over: it dies with 0xC0000142 exactly when the list carries a SID that is not an enabled group of the token being restricted. EVERYONE, S-1-5-4, S-1-5-11 and S-1-5-32-545 are groups of the token and start it; the write SIDs (S-1-4-*), S-1-0-0, S-1-3-*, S-1-5-32-544 and S-1-16-4096 are not, and kill it. Two consequences follow, and both are stated in the advisory:

    • No setting repairs it. The write SIDs are derived from a path β€” S-1-4-x-y, with the private temp one at S-1-4-x-y-1 (sandbox-windows-acl/src/workspace-sid.ts) β€” and a path-derived SID can never be an enabled group of a token. So under workspace-write the token that may write is the token that cannot start the child, while read-only, carrying no such SID, starts it and simply does not write.
    • Self-rescue is refused. Adding the capability SID to the token's own groups fails: SetTokenInformation(TokenGroups) β†’ Win32 error 87.

    The build number is not the variable here: this family had only ever been measured on Windows 11 26200, and [#9467] reproduced the whole structure on Windows 10 19044.

    The check is a mode switch, and since 0.18.0 it is the FIRST of two steps rather than the whole answer. Hold the command and the tool fixed and change only the mode: if read-only starts the command that workspace-write kills, the sandbox is the difference. That is one setting rather than a debugging session β€” and it is the answer to the one thing 0xC0000142 cannot say, because the same code runs in both directions: the restricted-token layer's own docstring records that a token built without the logon-SID + EVERYONE keep-alive pair also dies in early DLL init with exactly this status (and pwsh earlier still, in its CNG path, as 0xE0434352). Too few entries in the restricting list and too many land on the same 0xC0000142, so the code cannot name its own direction; only a comparison can.

    What the switch cannot do is name the mechanism inside the sandbox, and [#9336] is the report that proves it. That reporter ran the packaged desktop's chain and the ordinary explorer β†’ cmd β†’ pwsh chain against the same runner with the same argv: from the desktop chain cmd, PowerShell 7 and PowerShell 5.1 all died under workspace-write and all three reached exit 0 under read-only, while the console-bearing chain returned 0 in both modes (and still enforced the sandbox β€” an out-of-workspace write exited 1 with no file created). That is exactly what producer 3 looks like from the outside, and it is producer 2: the failing combination is a console-less host AND a confining token, and a mode switch made inside a console-less host separates for both reasons at once. So the second step is "does the runner own a console", it is answered by producer 2's own arm (start the runner from a DETACHED_PROCESS chain β€” no desktop, no particular machine), and this producer is the answer only when the runner did own one β€” the shape [#9186] measured, with a real node host and the DACLs untouched.

    A second input rides the same token, and since 0.23.0 it is measured rather than carried as a candidate. [#9336] reasoned that adding SIDs to a restricting list only widens write access, so the more suspicious input is the ACE the backend merges into the token's default DACL. That reading was right about the asymmetry and unmeasured about the effect; the measurement came as a supply of data rather than a defect report, through this plugin's own issue tracker, and it is quoted here as its reporter's. setTokenDefaultDaclGrant (sandbox-windows-acl/src/token.ts) merges one full-access restricting-SID ACE into the existing default DACL β€” SetEntriesInAclW, GRANT_ACCESS, FILE_ALL_ACCESS, extended rather than replaced β€” and the SID it names is tempWriteSid ?? writeSid ?? Everyone (src/index.ts:304): the private temp SID under workspace-write when a temp directory exists, otherwise the workspace SID (a sha256 of the canonical workspace path, S-1-4-x-y), and Everybody under read-only. So workspace-write is the only mode whose default DACL names a synthetic identity at all β€” a real second difference between the modes.

    What the measurement rewrote was the trustee, not the ACE. A Node module-loading hook over the shipped runner changed that single call and nothing else β€” the restricting list, the integrity lowering, the write grants, TEMP handling, the command line and the workspace all held fixed β€” so the ACE stayed where it was and only the SID it names changed. On Windows 10 19045:

    • naming the capability SID fails (0xC0000142) with the restricting list as shipped, with the two capability SIDs removed from it, and with the integrity-level lowering removed;
    • naming Everyone in that slot succeeds;
    • and the arm that decides which half is the input names both β€” the capability ACE left exactly where it is with Everyone added beside it also reaches exit 0.

    So the failing input is not "an ACE with a synthetic name was merged" but the absence of a trustee the restricted token itself holds, which is what the mode asymmetry actually forbids. The same measurement dumps GetTokenInformation(TokenDefaultDacl) from inside the confined child and finds four entries β€” Everyone at full access, merged by the backend, beside Administrators, SYSTEM and the logon SID at 0x10000000, all three ambient and identical to the unrestricted process's β€” so the merge extends this default DACL; the "merge dropped the ambient half" reading does not apply on that machine. The merge stays load-bearing in a way that forbids the obvious experiment, and the working arm is the one that respects it: every new object the confined process creates takes its DACL from there, so an ACE removed to test the theory takes every piped grandchild spawn down with it (spawn EPERM). The advisory still says not to delete it, and now says why the arm that succeeded did not have to.

    Two boundaries ride with the rule. First, the console is a conjunction with it: on that machine the failure is (no console) AND (no universally-held trustee in the default DACL), and either one alone recovers β€” the same pair [#9336] filed, read from the token's side rather than from the host's. Second, the build axis is open, and the measurement's own reporter asked for it to be recorded that way rather than folded into the rule: it is one build (Windows 10 19045), while this family also has a Windows 11 26200 report of the same packaged-desktop shape ([#8721]) whose arms vary no default-DACL input at all. So the advisory reads the outcome as (mode Γ— host Γ— build) and marks the third dimension not measured to be identical.

    And the switch costs something, which the backend's README states. Under read-only PowerShell cannot create its AppLocker probe files in temp and conservatively starts in ConstrainedLanguage, where Add-Type, non-core .NET static calls, COM and reflection fail β€” [#9336] adds [System.IO.File]::* and Get-CimInstance to that list β€” while the shipped workspace-write path lets the probe complete and keeps FullLanguage unless the machine carries a host-wide WDAC/AppLocker policy (packages/sandbox/sandbox-windows-acl/README.md:192). A command that "works" after the switch may therefore be a command that no longer runs at all: the switch is a diagnostic, not a repair. One thing this producer is not: .NET. Pure-native programs die here identically, which retires the "self-contained .NET runtime" cause the earlier reports converged on β€” that is the report's own control arm, not an assertion of ours.

Why this family is read from a successful result. The producer never marks it an error, and that is a fact about upstream rather than a choice here: RUNNER_FAILURE_RULES['windows-acl'] admits exactly one code β€” [{ allowedExitCodes: [127], fatalSignatures: ['windows-acl-run: '] }] (packages/sandbox/sandbox-local/src/index.ts) β€” and classifyRunnerFailure skips any other code before it looks at stderr (packages/sandbox/sandbox/src/diagnostics.ts), so 0xC0000142 is never a runner failure and SandboxUnavailableError is never thrown. The renderer then reports it the way it reports any finished command β€” "Non-zero exits are reported, not errored … only infrastructure failures (spawn errors, aborts) surface as isError results" (packages/shell/tool-pwsh/src/render.ts) β€” as [exit code: …]. Every version of this plugin before 0.6.0 read error results only and was structurally blind to it, which is exactly why the model retries a command that can never start.

The read is ToolExecutionSuccess.value β€” the tool's own canonical output, documented as "Execution-local canonical value; deliberately omitted from durable events" and not carried on failure results at all β€” so the code arrives structurally rather than as a line of text. A command that prints [exit code: -1073741502] is not this failure, and neither is a value some other tool happens to build with an exitCode field: the classifier requires the shipped shell projection (kind: 'foreground' plus an integer exitCode).

The advisory that follows:

Sandboxed command never started β€” the process died while its native libraries were loading.

What was reported:
  [exit code: -1073741502]        (0xC0000142 STATUS_DLL_INIT_FAILED)
The call ran under sandbox mode `workspace-write`, where the harness starts every command through its
restricted-token runner.

0xC0000142 is STATUS_DLL_INIT_FAILED: ... this is BEFORE the program's entry point. ...
Nothing in the code says "sandbox" by itself; what makes the sandbox a candidate is the mode above ...

Three producers have been measured under a confining Windows mode. Check which one this is:
  1. An MSYS2 / Git-Bash program ... (#7877)
     If that is what could not start: write the same work as a PowerShell or `cmd` command instead
  2. The packaged desktop application's sandbox runner ... (#8193, #8208)
     This process is NOT an Electron binary (`process.versions.electron` is unset), so producer 2 does not apply here.
  3. A capability SID inside the restricted token's own restricting list ... (#9186)
     CHECK, in TWO STEPS β€” the first alone does not settle it. Step 1: ... change only the MODE ...
     Step 2: establish whether the runner OWNS A CONSOLE, because producer 2 fails the very same switch ...

     Since 0.20.0 there is a RULE for which input does it: the child dies exactly when the restricting list
     carries a SID that is NOT an enabled group of the token (16 of 16 arms, #9467, on Windows 10 19044).
     The write SIDs are DERIVED FROM A PATH, and a path-derived SID is never an enabled group of any token β€”
     so this producer cannot be tuned away, and adding the SID to the token's groups is refused (Win32 87).

     A second candidate sits on the SAME token and is NOT the restricting list: the token's DEFAULT DACL ...
     it is a CANDIDATE and not an answer β€” nobody has measured it ...

Do not retry this call: the environment has not changed, and the identical call produces the identical code.

A `read-only` session is NOT a substitute for a working `workspace-write` one β€” the mode switch above is a
diagnostic, not a repair. ... under `read-only`, PowerShell ... starts in ConstrainedLanguage ...

Honest boundary β€” 0xC0000142 has producers this list does not have: a program that cannot load one of
its own DLLs dies this way too, and the sandbox backend's own source records that a child started with
a hidden console window does as well ... This is not a claim that the sandbox caused the failure.

What it does not claim. The code is a loader status, and the loader reports the same status for causes that have nothing to do with the sandbox (a missing DLL, a program's own initialization failure, the hidden-console child the backend's own source avoids CREATE_NO_WINDOW for). So the advisory diagnoses the class ("the process never reached its entry point") and enumerates the producers measured under a confining mode, each with the check that separates them β€” one of which the reader answers (what program failed to start) and one of which the plugin answers (is this host the packaged desktop binary). Where a producer has more than one measured shape, the advisory names all of them and records which check separates them outside the session, rather than asserting the single shape that happened to be measured first. It never offers danger-full-access as a fix and never suggests a sandbox setting be relaxed.

4. Denied inside the workspace (workspace-denial, added in 0.11.0; the branches it forks into in 0.13.0 and 0.14.0, the breadth of the second in 0.22.0)

[#423] is one report and its own follow-up, and it is the other end of the backend Β§1 is about. There the workspace grant could not be applied at all and every command died before it ran; here the grant was applied β€” on the workspace root, once β€” and part of the tree still refuses writes, forever. The report's shape: under workspace-write on Windows, a command writing into a subdirectory that was created or moved in from outside the session (an installer, an editor, another harness running under its own account) is denied, while the same command against a directory the harness itself created succeeds.

[exit code: 1]  sandbox: { mode: "workspace-write", denied: true }

The signature is a value, not a message, and this family is invisible from the error path for the same structural reason Β§3 is: a denied command exits nonzero, and the shipped shell tools report a nonzero exit as a finished run rather than as isError (packages/shell/tool-pwsh/src/render.ts reports [exit code: N] and drops a denial marker). The fact therefore arrives in ToolExecutionSuccess.value, where tool-bash / tool-pwsh project what the sandbox executor stamped (packages/shell/bash-sandbox/src/index.ts, pwsh-sandbox): the mode the call actually ran under, whether the backend's own refusal dialect appears in the captured stderr, and the enforcement that applied. Reading the executors' own stamp rather than a sentence means this plugin is reporting the harness's reading of its own sandbox, not a gue