DeepSeek Harness bash hangs 300s on !: history expansion
One printf 'p4-start %s p4-end\n' '!' runs a full 300 seconds in the bash tool and returns not a result but a screen of dsh> prompts. The command itself is flawless and returns instantly in a terminal; the hang comes from it being wrapped in $'...' when delivered, while the interactive bash performs history expansion on ! at read-line time and discards the whole line (#10636). What follows reproduces and verifies it in experiment order.
DeepSeek Harness: Experiment 1: minimal reproduction, change one character
The only controlled variable is whether the command contains !.
- First run an equivalent command without the exclamation mark:
printf 'p4-start %s p4-end\n' 'x', which returns normally. - Then run
printf 'p4-start %s p4-end\n' '!', which hangs. - Look at the delivery shape: the tool layer wraps the command into a string like the one below (with START/END markers and
eval --). - Feed that string to a
bash --noprofile --norc -imatching production, and the output is:
bash: !\: event not found
dsh>
- Note two things: the START printf, the eval, and the END printf never ran; and the prompt after the error has no completion marker in front of it.
- Conclusion: the command is not wrong; the shell discarded the whole line at the read-line stage.
DSH plugin: Experiment 2: it is the quoting form, not the command
$'...' and single quotes treat ! differently: history expansion exempts content inside single quotes, not content inside ANSI-C quoting.
- Look at
quoteForBash: it escapes backslashes, single quotes,\r, and\n, but never!. - Look at the shell launch parameters:
--noprofile --norc -iis interactive mode, where histexpand is on by default. - Look at the initialization sequence: only
stty -echo, with no history expansion disable. - So a
!followed by an escaped single quote is read as the event identifier!\; the lookup fails,event not foundis printed, and the whole line is discarded. - This narrows the scope to the "quoting form": any change to the command content is only a bypass, not a fix.
DeepSeek Harness: Three layers of amplification: why a dropped line burns 300 seconds
The trigger is only the first layer; the other two are what actually burn the 300 seconds — the prompt lacks a completion marker, and the tool loop does not recognize that state.
- Layer one (trigger): history expansion discards the line, taking both START and END marker printfs with it.
- Layer two (amplification): completion detection relies on
PROMPT_COMMANDprintingOSC 133;D, but after a discarded line the prompt bash reprints does not runPROMPT_COMMAND, so only a bare prompt appears. - Layer three (spinning): the tool loop's return branches recognize only "END marker seen", "session exited", and "wait reason is stdin_read";
inferred_idleis not among them, so it spins every 25 ms until the 300-second deadline. - On timeout it takes the timeout branch and calls
shells.reset()to kill the entire shell. - What you see: a screen full of
dsh>, theevent not foundcut off by the scrollback limit, and a notice that the beginning was discarded.
DSH Plugin: Experiment 3: test two fixes
One changes the quoting form, one turns off history expansion; both make the command return normally, but only the latter reached master.
| Option | Layer | Change | Community result |
|---|---|---|---|
| B | layer one | replace $'...' with traditional single-quote quoting | full chain outputs normally |
| C | layer one | append set +H to initialization | full chain outputs normally |
| D | layer three | exit early with a notice when spinning without output growth | resilience hardening |
| E | layer two | stop relying only on PROMPT_COMMAND for completion | resilience hardening |
Option B must keep handling literal \r and \n; option C is one line and is the root-cause fix. D and E do not remove the trigger but make future "line discarded" cases fail fast instead of burning 300 seconds.
DeepSeek Harness: Version status: fixed on master, not backported to rc.3
Commit 967ba4c4b8 (2026-10-21) changes initialization from stty -echo to stty -echo; set +H and adds a regression test, but it is not an ancestor of the rc.3 tag.
- Use
git merge-base --is-ancestor 967ba4c4b8 <rc.3 commit>to check containment yourself. - Community conclusion: it is an ancestor of master (
22ddee3), not ofdsh-v0.2.1-rc.3(1f81c04). - So rc.3 still reproduces it 100% of the time.
- Even with that fix merged, layer one is unchanged:
quoteForBashstill escapes only backslashes, single quotes, and newlines, andset +His the only thing standing in front of!. - In other words, if the backend or the wrapper layer is rewritten later, this quoting defect returns immediately.
DSH plugin: Today's workaround: keep bare ! out of commands
Until the fix reaches your version, the safest move is to keep the exclamation mark from ever reaching history expansion.
- Avoid a bare
!in commands; use an equivalent form such asprintfto generate the literal. - If you must include it, make sure it sits inside traditional single-quote quoting rather than ANSI-C quoting.
- Do not put
set +Hat the start of a command line expecting it to work — expansion happens at read-line time, before that line executes. - Split long tasks into several short commands so a hang can be traced to a specific one quickly.
- After a hang, do not retry the same character repeatedly; switch to an equivalent form first to verify the link itself works.
Cautions
Avoid the trigger instead of fighting the timeout.
- Do not read the 300 seconds as a slow network. The timeout here is a local shell spinning, not a request timeout.
- Do not look only at the last screen.
event not foundmay already be cut off by scrollback. - Do not expect the session to recover. The timeout resets the shell and loses the previous interaction state.
- Record the bash version. The community reproduced it on macOS's bundled 3.2.57; 5.x behavior was not verified locally, and the fix should not depend on the version.
- Do not treat B and C as alternatives. One is correct quoting, the other disables expansion; defense in depth should do both.
If your command contains no ! at all and still hangs to timeout, it is not this class — check completion detection and shell initialization.
One last diagnostic trick: failures where "the shell discarded the command" usually end as "markers never appear + output is empty + it hits exactly the timeout ceiling". Remember that combination and you can skip a lot of guessing.
Likewise, a ! inside a heredoc takes the same path — expansion happens when the whole line is read, regardless of whether the command spans multiple lines.See DeepSeek Harness terminal: PSReadLine ghosts in context for the related case.
FAQ
DeepSeek Harness hangs because its command delivery layer wraps commands in $'...', while the controlled shell is an interactive bash with history expansion on; $'...' does not exempt !, so bash discards the whole line with event not found and never prints the completion marker, leaving the tool spinning until timeout.
You only see prompts because, after the line is discarded in DeepSeek Harness, the START marker printf never runs either, so on timeout the tool cannot find the start marker and falls back to the scrollback tail; the real event not found is cut by the scrollback limit, leaving only bare prompts.
No, set +H in the command does not fix it on the spot in DeepSeek Harness: history expansion happens in the read-line stage, before the in-line statement executes; a set +H from a previous call is also wiped by the shell reset after a timeout. The only effective approach is to avoid a bare !.
The fix is master's commit 967ba4c4b8 in DeepSeek Harness, which changes initialization from stty -echo to stty -echo; set +H, but it is an ancestor of master and not of the rc.3 tag, so rc.3 still reproduces it.
Until the fix ships, the DeepSeek Harness rc.3 workaround is to put ! inside traditional single quotes and make sure the whole command is single-quoted, or use an equivalent form without ! (for example, printf to generate a literal). Splitting long commands into shorter ones also exposes the failure faster.
Related Terms
- history expansion
- History expansion is bash's mechanism that replaces !-prefixed text with a history event while reading a line. It runs before parsing and execution, so disabling it inside the same line comes too late.— https://github.com/deepseek-ai/deepseek-harness/discussions/10636
- quoteForBash
- quoteForBash is the tool-layer function that wraps a command into a deliverable string using ANSI-C quoting `$'...'`. It escapes backslashes, single quotes, and newlines, but does not handle the exclamation mark.— https://github.com/deepseek-ai/deepseek-harness/discussions/10636
- OSC 133;D
- OSC 133;D is the marker printed by PROMPT_COMMAND in the terminal completion-detection protocol, indicating a command has ended. After a line is discarded, the prompt bash reprints lacks it, so completion detection fails.— https://github.com/deepseek-ai/deepseek-harness/discussions/10636
- inferred_idle
- inferred_idle is the fallback state of completion detection meaning the terminal looks idle but no completion marker arrived. The current tool loop does not treat it as an early-exit condition, so it waits until timeout.— https://github.com/deepseek-ai/deepseek-harness/discussions/10636
Sources
- #10636 — heredoc or single-line commands containing ! hang the whole command line· deepseek-ai (GitHub Discussions)
- deepseek-ai/deepseek-harness (source repository)· GitHub