[Bug] persistent bash mis-expands ! in commands via history expansion; bash 3.2.57 (incl. macOS default) hangs until timeout (shebangs in heredocs) - one-line fix #5046
Replies: 4 comments 2 replies
|
Source-verified end to end at 1. The wrapper chain is exactly as described. 2. Fix location — the init send is the right scope; please keep it there. Appending to the init text at 3. macOS aggravator confirmed as structural, not a probe bug. 4. Design fragility worth a code comment. The deeper issue is that the tool's completion contract is "two markers on one physical line" — any input-level line discard (histexpand today, something else tomorrow) silently converts a 300 ms command into a 300 s timeout with a shell reset. The one-line fix removes the current discard source, but the marker contract itself is what makes the failure mode so expensive. A one-line comment in 5. Regression fixture. For a test that actually pins this: the failure requires the The consolidation of #3373 + #3634 + this thread into one diagnosis with repro + fix + test is exactly what the maintainers need; this is the strongest candidate in the shell-tool queue for when PRs reopen. |
|
This report adds a particularly important failure class to the persistent-terminal diagnosis: the wrapped physical input line can be discarded before Bash reaches the command, so the missing completion marker does not prove that the intended child process is still running. That distinction matters for recovery. First capture the exact wrapper shape, Bash version, shell mode, start/end markers, and whether a child PID or process group exists. Then classify separately:
Do not press Enter repeatedly or resubmit a side-effecting command while the marker state is unknown. Confirm the process outcome and durable tool/turn close before retrying. A scoped init change for the persistent tool can be safer than changing shared interactive Bash defaults, but it still needs a real PTY regression test for the escaped-quote-plus-bang shape on the supported platforms. The handbook terminal runbook has the lane classifier, marker evidence, process ownership and duplicate-side-effect recovery sequence: https://sandbaseai.github.io/deepseek-harness-handbook/long-running-terminal.html It treats the 300-second display as a symptom, not proof of a live foreground process, and keeps the Bash workaround separate from approval and sandbox policy. The upstream report and the handbook are independent community evidence; neither should be read as proof that every Desktop wrapper uses the same shell path. |
|
更新说明 / Update note: 已在正文补充 WSL2/Ubuntu 26.04 的验证:bash 5.3.9 不复现;编译 bash 3.2.57 复现且一行修复有效。据此把结论从“macOS 问题”修正为“bash 3.2.57 低版本适配问题”,并补上了超时路径的代码级解释(Linux 上同样会等到工具 deadline 后 reset)。同时新增一节,讨论默认关闭 histexpand 对该工具本身是否合适。 Added WSL2/Ubuntu 26.04 verification (bash 5.3.9 does not reproduce; compiled 3.2.57 reproduces and the one-line fix works), reworded the conclusion as a bash-3.2.57 compatibility issue rather than a macOS issue, corrected the timeout-path explanation (Linux also waits to the tool deadline), and added a section on whether defaulting histexpand off is appropriate for this tool. |
|
Confirmed on macOS with Harness In a real review run, Reduced reproduction: submit these as two separate persistent Bash tool calls in the same shell: printf '%s\n' seedprintf '%s\n' ':!*.md'Using Harness's actual wrapper with Bash 3.2.57 reproduces the history substitution and continuation prompt. Replaying the original Git command through the real Harness PTY also timed out with history expansion enabled; sending |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Summary
The persistent bash tool wraps each command into a single physical line and sends it to an interactive bash in a PTY. On bash 3.2.57 (macOS default; reproduced on Linux with a compiled 3.2.57), history expansion runs before parsing, and its quote scanner does not understand ANSI-C
$'...'. The\'escapes produced by the tool's own quoting terminate the scanner's single-quote region, so a later!in the command body (most commonly#!/bin/bashinside a heredoc, or JS negations likeif (!m)) is read as an event designator. Bash printsevent not foundand discards the entire physical line, including both completion markers; the tool then polls until its timeout (default 300 s on macOS) and resets the shell, looking like a hang. Not reproduced on Linux bash 5.3.9 (its history scanner protects$'...', so the tool's wrapping is unaffected). Fix: appendset +o histexpandto the shell init.中文摘要:bash 3.2.57 的历史展开在解析前误读工具包装用的
$'…',把命令里的!当事件指示符并丢弃整行,工具等到超时后 reset;修复为初始化时追加set +o histexpand。详见下文验证。Environment
cd5ef81481(tagdsh-v0.1.2-alpha.1, merged 2026-08-28; still the tip as of 2026-08-30) andb150a551b8(dsh-v0.1.1-rc.2);packages/shell/tool-bash-persistent/src/index.tsis identical in both, and line 268 readstext: 'stty -echo',./bin/bash3.2.57(1)-release (the default); the shell is spawned with--noprofile --norc -i(packages/terminal/terminal-bash/src/config.ts).bash-3.2.57.tar.gz—GNU bash, version 3.2.57(2)-release (x86_64-unknown-linux-gnu); build dir and tarball removed afterwards.!(version boundary is claimed.Trigger surface
On affected bash 3.2.57, a command fires when wrapping leaves a
!outside history expansion's quote tracking. Observed:cat > script.sh <<'EOF' ... EOFwith#!/bin/bashor#!/usr/bin/env node— the common agent-authored shape; simple heredocs work, script-writing heredocs hang.if (!m) { ... },if (!res.ok) ...,!line.startsWith(...).echo 'hello!world',rg --files -g '!node_modules'(as in 极简模式grep等命令卡死问题复现以及解决方案 #3634).!followed by space / tab / newline /=(! cmp -s a b,a != b).!(is not exempt on the tested 3.2.57.!!,!$, or a!wordprefix of an earlier command), the line is rewritten instead of erroring; reproduced!!injecting the previous command.Reproduction (no DSH install, no PTY needed)
Paste this into affected bash 3.2.57; it feeds the same physical-line shape
wrapCommand()emits into an interactive bash:Observed on bash 3.2.57 (prompt echo trimmed):
Neither marker appears — the whole line was discarded; same on the Linux-compiled 3.2.57. On bash 5.3.9 the same block prints both markers plus the heredoc body with
__DSH_END__:0. Withset +o histexpandsent first (the fix):Smallest scanner illustration (the only difference is the
\'before the!; also reproduces on Linux-compiled 3.2.57; on 5.3.9 neither line errors):Root cause
wrapCommand()(index.ts:77-82) puts the whole command on one physical line:printf START; eval -- $'<escaped>'; status=$?; printf END status.quoteForBash()(index.ts:69-75) escapes\as\\,'as\', newlines as\ninside$'...'— safe for the parser.\and'but not$'...'. The'inside\'terminates the scanner's single-quote region, so the next!is read as an event designator. (bash 5.3.9 protects$'...'here — mechanism inferred from behavior; see Linux verification.)commandOutput()never matches and the poll loop (index.ts:309-377) runs to the deadline (index.ts:301).Why it waits the full timeout
stdin_read— the one wait reason that returns a fast partial (index.ts:372-375). Since it never settles (below), the loop polls until the tool's owntimeoutMs(index.ts:301), then resets.stdin_readnever settles:MacProcessInspector.isStdinWaiting()returnsfalseunconditionally,packages/subprocess/subprocess-local/src/process-inspector.ts), and the prompt-based path (session.ts:492-496) needspromptSeen/promptTextSeen, which never become true because after the error bash reprintsdsh>without first emittingOSC 133;D(verified with a real PTY using the samePS1/PROMPT_COMMAND; after a normal command the marker returns).LinuxProcessInspector.isStdinWaitingreads/proc/${pid}/task/${tid}/syscall), but it has a pre-write memory gate this failure cannot pass.setInitialForeground()samples the foreground group before the write (session.ts:138-140,313);acceptsStdinWait()accepts a stdin wait only after a poll has observed the shell leave stdin wait post-write (session.ts:143-149). A histexpand discard returns toreadlinewithin the same poll cycle, so that departure is never observed and the exact-probe settle (session.ts:499-501) never fires. This matches the Linux 3.2.57 tool-level test: no quick partial, wait to the tool deadline (5 s/10 s), then reset.Linux verification (WSL2/Ubuntu 26.04)
Measured 2026-08-30 at
cd5ef81481with a WSL2 DSH.bash 5.3.9 (distro default)
histexpandis on by default, but 5.3.9 protects$'...'during history expansion: the non-PTY repro prints both markers plus the heredoc body with__DSH_END__:0, andeval -- $'echo \'hi\'\n#!/bin/bash\n'does not error. Double-quoted!(echo "!/bin/bash") still errors, so history expansion is active — it just does not break the tool's$'...'wrapping.bash 3.2.57 (compiled from source on Linux)
histexpand on, same failure as macOS —bash: !/bin/bash\nhello\nEOF': event not found, neither marker; withset +o histexpandfirst, it prints__DSH_START__/#!/bin/bash/hello/__DSH_END__:0. The\'-vs-no-\'pair reproduces identically.shellPathpointed at the compiled 3.2.57): with the fix, the heredoc shebang returns#!/bin/bash\nhelloin ~24 ms. Without the fix, it fails; withtimeoutMsset to 5 s/10 s it ran to the tool deadline and reset — a quickstdin_readpartial was not observed, the same failure shape as macOS.Suggested fix (one line)
(
set +His the equivalent short form.)histexpandoff); the init is re-sent after every shell reset.await setup.done,index.ts:272); putting+HinDEFAULT_BASH_ARGSwould affect every defaultshellbackend session, so not there.request.text.startsWith('stty -echo')(packages/shell/tool-bash-persistent/tests/tools.spec.ts), so it stays green.--noprofile --norc -i; on affected 3.2.57 both fail with the current init, on 5.3.9 they pass even without the fix, so the regression must run on affected 3.2.57):Regression tests (TypeScript,
loader-composition.spec.ts)The fixtures use a quoted heredoc delimiter (
<<'EOF') soquoteForBashemits a\'before the!; a bare!with no preceding escaped quote stays inside the scanner's single-quote region and passes even with the bug. On 5.3.9 neither shape fails.Alternatives considered
!inquoteForBash(): not reliable — on the tested 3.2.57$'\!'yields a literal\!(measured); 5.3.9 was not tested for this escape. Turning histexpand off removes the whole failure class.eval --: larger change; would also address theeval --bashism noted in Bug: persistent-bash 的 eval -- 是 bashism,非 bash 持久 shell 下每条命令 exit 127 | eval -- breaks every non-bash persistent shell #2271.Is disabling histexpand by default appropriate for this tool?
histexpandoff; the tool uses-ifor prompt markers/session lifecycle, not for history expansion, so this aligns with non-interactive semantics.!can sendset -o histexpandas a separate first command; it just should not be the default.Verdict: appropriate, and the one-line init is the right place.
Relationship to earlier reports
This consolidates and extends two earlier reports of the same root cause:
isStdinWaiting()aggravator credited above.set +H).!!/!$), regression tests, verification against current mastercd5ef81481, and a Linux-compiled bash 3.2.57 run that isolates the bash version.eval --bashism), Persistent Bash timeout message should not diagnose OOM #1186 (timeout message claims OOM).Workaround today
Send
set +o histexpandas its own first command in the persistent shell; re-send after a timeout-triggered reset. Do not put it on the same line as a command containing!— history expansion runs when the whole physical line is read, before the setting takes effect. Or avoid!inside single-quoted strings and heredoc bodies on affected 3.2.57.All reactions