dsh-win-bash
IdentifiedGit Bash shell provider for DeepSeek Harness on Windows: ctx.shell + bash tool composed beside the pwsh stack
dsh-win-bash
English | 中文
Git Bash command execution for DeepSeek Harness on Windows.
What it does
The shipped DSH composition gates its two shell stacks by platform. On Windows
the host composes @deepseek-ai/dsh-pwsh-sandbox (PowerShell) and every agent
preset mounts @deepseek-ai/dsh-tool-pwsh; the bash stack stays disabled
because dsh-bash-local spawns a bare bash resolved through PATH — which
on Windows is C:\Windows\system32\bash.exe, the WSL launcher, not Git
Bash. A WSL shell has a different filesystem view and no C:\-style paths, so
it is not a drop-in Windows shell.
This plugin adds the missing piece:
- a
ctx.shellprovider that spawns an explicitly resolved Git Bash executable (<git>\bin\bash.exe -c <command>), inheriting every upstream mechanic: managed process groups, timeout/abort classification, bounded output with spill files, the model-friendly environment (NO_COLOR,TERM=dumb,PAGER=cat), background jobs, and the file sandbox; - the shipped model-facing
bashtool wired to that executor.
It composes beside the pwsh stack rather than replacing it: the loader row
declares isolate: { shell: 'win-bash' }, which puts the executor and the tool
into a private service realm. The host's ctx.shell (pwsh) is neither
replaced nor duplicated, so the preset's pwsh tool keeps working untouched.
Install
dsh plugin --profile desktop add dsh-win-bash
From a local checkout, point at the directory instead:
dsh plugin --profile desktop add link:/path/to/dsh-win-bash
Either form writes the profile dependency and appends dsh-win-bash to
dsh.profile.bundles; the plugin's own cordis.patch.yml supplies the loader
row. The host picks the new bundle up on the next start (the profile
reconciliation hot-reloads the row when the app is running).
To use it in another profile, repeat the command with that profile name.
Configuration
All knobs live on the win-bash row (override from the profile's
cordis.patch.yml with a bare - id: win-bash / config: {...} entry — a
patch replaces the row's whole config):
| Key | Default | Meaning |
|---|---|---|
bashPath | '' | Absolute path to bash.exe. Empty = auto-detect. |
promptHint | true | Add a one-line system-prompt section preferring bash over pwsh. |
sandboxFallback | auto | What a proven confined-spawn failure means on this host — see below. |
timeoutMs | 120000 | Default foreground timeout (upstream default). |
maxTimeoutMs | 600000 | Cap for per-call timeout overrides. |
maxOutputBytes | 64000 | Per-stream in-memory cap; overflow spills to disk. |
maxSpillBytes | 67108864 | Per-stream full-output spill cap. |
graceMs | 3000 | Kill escalation and post-exit pipe-drain grace. |
cwd | (unset) | Default working directory; the session workspace wins per call. |
Confined Git Bash cannot initialize today (sandboxFallback)
On Windows, the upstream ACL runner (@deepseek-ai/dsh-sandbox-windows-acl)
spawns confined children under a WRITE_RESTRICTED token, and Git Bash
cannot initialize under it. Two routes to the same death —
STATUS_DLL_INIT_FAILED, exit 0xC0000142, reported by the tool as
(no output) [exit code: 3221225794] — are known:
- Console-less host. The runner spawns console children that inherit the
host console (its own docs: "Console isolation is unavailable"; the
CREATE_NO_WINDOW/CREATE_NEW_CONSOLEvariants die under the restricted token). A DSH desktop app launched from Explorer/Start menu is a GUI process with no console anywhere in its tree, so console children die at DLL init. Reported from a real desktop install, where the shippedpwshtool died the same silent way. - msys shared object (every host). The msys runtime behind Git Bash
creates
\BaseNamedObjects\msys-*during init; the restricted token denies it (0xC0000022) and bash dies with a fatal-error stack trace on stderr. Reproduced with the real shipped runner on a dev machine:cmd.exeandpowershell.exesurvive the identical spawn (console or not) — only bash dies. Launching DSH from a console parent does NOT avoid this route.
The same command under danger-full-access works, because it never enters the
sandbox path. So on Windows the fallback below is not a stopgap — it is the
configuration that makes the bash tool work at all under a file-sandboxed
session.
sandboxFallback decides what a proven init death means:
| Value | Behavior |
|---|---|
fail | Explicit, actionable error (root cause + remedies). Never runs unconfined. |
unconfined | Never calls the sandbox: every command runs Git Bash directly, stamped enforcement: 'none'. No prompt — this is the operator's standing consent, for approver-less hosts (CI, headless). |
auto (default) | A read-only confined probe runs when the row activates: on a proven 0xC0000142 death every confined call — the first included — is answered with a sandbox denial and the command does not run. The shipped tool renders [sandbox: file access denied under <mode> mode] plus the escalation hint; the model retries the same command once with sandbox_permissions + a one-sentence justification, and the harness approval channel asks the user. Only an allowed-once outcome executes — unconfined, under the approved wider mode. If the probe is inconclusive, the first live confined call that carries the proven death latches the same denial behavior. |
auto never bypasses a working sandbox: the denial only appears after a
positive 0xC0000142 signature, which a confined child that actually ran
cannot produce (cmd and pwsh survive the identical spawn; a command that
crashes this way unconfined fails the same way, so asking the user is a safe
diagnostic rather than a false positive). A terminal-launched dsh/dsh web
keeps whatever works upstream — but per route 2 above, confined Git Bash is
broken on Windows regardless of how the host was launched, so auto will
legitimately deny there too.
What the user sees under auto: the first confined bash call comes back
(no output) with the denial marker and the hint to retry the exact command
once with sandbox_permissions (the narrowest wider mode that suffices) plus
a justification — "the approval prompt asks the user". The retry raises an
approval request (Allow this operation with <mode> permissions: …); on
approval the command runs unconfined and the result's sandbox fact reads
{ mode: 'danger-full-access', denied: false } — an honest stamp, no bypass
flag. A rejection throws "the user rejected escalating this command … it stays
denied, so stop and explain instead of working around it". The host log
carries a one-time win-bash: … warning naming the root cause. When the probe
proved the death at row activation, the system-prompt section says so too, so
the model asks for danger-full-access directly instead of stepping through
the ladder one rung at a time.
Upstream (not this plugin): route 1 needs the runner to use
DETACHED_PROCESS (or branch on GetConsoleWindow() === NULL) when the host
has no console — the harness repo has no such branch today. Route 2 is not
fixable by the runner at all: a WRITE_RESTRICTED token cannot grant the
msys runtime object creation in \BaseNamedObjects, so confined Git Bash on
Windows needs either this plugin's fallback or a different sandbox mechanism
upstream.
Git Bash discovery order
bashPathfrom the row config;- the
DSH_GIT_BASH_PATHenvironment variable; - the registry (
HKLM/HKCU/WOW6432NodeSOFTWARE\GitForWindows→InstallPath) — this is how a version-stamped install likeF:\ssdpath\Git\2331is found; - well-known install roots (
%ProgramFiles%\Git,%ProgramFiles(x86)%\Git,%LOCALAPPDATA%\Programs\Git, Chocolatey, scoop, MSYS2); PATH, in order, skipping the WSL launcher underC:\Windows\System32.
An explicit bashPath (or the env var) is tried first without the WSL
filter, so an operator who really wants WSL bash can point at it; it still has
to exist, and an explicit path that does not exist falls through to the rest
of the order (the dsh-pwsh-local behavior). Auto-detection never picks the
WSL launcher, and resolves to nothing rather than guessing: the row then
fails with an actionable message naming the fix.
Verify
In a session, the model (or you, through the tool list) can run:
bash: echo "$BASH_VERSION"; uname -s
MINGW64_NT-* means Git Bash; Linux would mean WSL. The Plugins page lists
the row as win-bash (dsh-win-bash), and the host log prints
win-bash: using Git Bash at <path> at startup.
Rollback
dsh plugin --profile desktop remove dsh-win-bash
or disable the row (plugin_manager → win-bash, or disabled: true in the
profile patch). Either way the host returns to exactly its shipped state: the
pwsh executor, the presets' pwsh tool, and no bash tool. Nothing else in the
composition is touched, because the plugin never writes outside its own realm.
Known limitations
- Windows only. The row is gated
process.platform !== 'win32'; on POSIX the shipped bash stack already runs and a secondbashtool would collide. pwshremains available. Removing it would mean restating an agent preset's whole plugin list, which silently drifts on every DSH upgrade (a renamed package there fails the preset mount for every session). ThepromptHintsection steers the model tobashinstead; sessions that should never see pwsh can use a preset without it.- Git Bash path semantics are MSYS semantics.
/f/...paths, and Windows paths with backslashes may need quoting — ordinary Git Bash behavior, the same model-facing surface as any Git Bash user. - The confined sandbox path is inherited, not re-implemented. In
workspace-write/read-onlymodes the upstream windows-acl runner wraps our argv; only the wrapped executable changes. - No Windows host can sandbox Git Bash today. Two independent routes kill
the confined child before the command runs: a console-less GUI host (the
runner's own documented limitation — launching DSH from a terminal avoids
only this one), and the msys runtime's denied
\BaseNamedObjects\msys-*object under the restricted token, which hits every host regardless of how DSH was launched. Both are upstream limitations of the ACL runner, not something the plugin can fix by choosing a different shell —pwshdies the same way on the first route. UndersandboxFallback: autothe plugin answers such a call with a denial and lets the user authorize a wider mode. - Confined Git Bash is broken on every Windows host, not just desktops.
Under the runner's restricted token the msys runtime cannot create its
\BaseNamedObjects\msys-*shared object (0xC0000022→ bash dies0xC0000142); reproduced with the real shipped runner, where cmd and pwsh survive the identical spawn.sandboxFallbackis therefore the working configuration for Windows — underautoit routes every such call through user authorization instead of quietly running unconfined.
Comments
Loading…