DeepSeek Harness terminal: PSReadLine ghosts in context
If a persistent terminal on Windows returns terminal_send viewports containing old commands you never typed, that is not a cross-session leak; PSReadLine prediction completion is being captured as input in the model context. According to the reproduction in Discussion #10070, the ghost text comes from the system-wide ConsoleHost_history.txt, and Set-PSReadLineOption -PredictionSource None makes it disappear immediately. The sections below separate the symptoms from the outbound path, then give three interchangeable ways to turn the source off.
Where untyped commands in a DeepSeek Harness terminal come from
Separate three things first: prediction ghosts, per-character redraw, and real input. All three look like text, but they have completely different sources, and mixing them up leads to a false diagnosis of session leakage.
- Send
terminal_send "echo AAA". Expected: the viewport shows a line such asecho athat was never entered, which is a prediction ghost from prefix matching. - Look at the line shape. Expected:
e,ec,ech,echois a per-character redraw staircase, not model or user input. - Compare
terminal_readhistory and runGet-History. Expected: only this session's entries appear, which rules out cross-session leakage. - In the same session run
Set-PSReadLineOption -PredictionSource None, then sendecho ZZZ. Expected: the ghost disappears and the staircase remains, which pins the cause to the prediction source. - Try a Windows account with no history file. Expected: the ghost disappears, confirming the source is
ConsoleHost_history.txt.
How DeepSeek Harness terminal bytes reach model context
The model does not see the screen; it sees the concatenated text stream produced by the sanitizer, so a preview in the terminal also becomes a prompt. The chain has four steps:
terminal_openstarts a new pwsh process with-NoLogo -NoProfile. Expected: no custom profile is loaded.- PSReadLine reads
ConsoleHost_history.txt. Expected: prefix-matched history is rendered as a faint or italic ghost. TerminalSanitizerremoves all ANSI, including the SGR that marks the ghost style. Expected: the ghost becomes ordinary text.terminal_sendreturns the concatenated result as the viewport. Expected: the next LLM request sends it as-is.
Once a ghost is in a viewport it is no longer a local display issue; it is sent again with every request. According to the discussion, exposure has four layers:
- Every LLM request resends the viewports already in context. Expected: the ghost is transmitted repeatedly.
submit=falseonly removes the trailing Enter. Expected: a prefix such assshorexportcan deliberately fish out matching history.- This channel executes no command, reads no file, and leaves no file-read audit record. Expected: nothing in the audit log points at the disclosure.
- Ghost content can include credential commands, internal addresses, and private paths. Expected: the default prediction source plus one unsubmitted send is enough to trigger it.
DeepSeek Harness root cause: predictions on, sanitizer only filters
According to the community's source review in the thread, nothing in the terminal-bash package calls Set-PSReadLineOption, and the default arguments are only -NoLogo -NoProfile. For the subsystem internals, see terminal subsystem notes; two layers explain the behavior:
- Interactive pwsh defaults
PredictionSourcetoHistoryAndPlugin; a non-interactive-Commandprobe reportsNone. Expected: the probe gives a misleading result and makes prediction look disabled. TerminalSanitizerfilters the stream rather than emulating a screen: its CSI branch skips parameters and drops cursor positioning, and\\x08passes through unchanged. Expected: redraw frames concatenate on one line.- Erasing in a terminal is really "draw again", not retract. Expected: no filter rewrite can remove the ghost, so the prediction source itself must be disabled.
- With both layers present, any cleanup that only touches the display path recurs. Expected: the ghost returns on the next request.
Four misdiagnoses in DSH plugin troubleshooting
All four send the investigation in the wrong direction; checking them one by one saves time. They are wrong about the process, the probe method, the filter layer, and the noise source respectively:
- Treating old commands as a cross-session leak. Expected:
Get-Historyshows only this session's records, because everyterminal_openis a new process. - Probing
PredictionSourcewith-Commandand gettingNone. Expected: the result is not representative, because non-interactive mode does not load prediction. - Assuming the sanitizer can erase ghosts. Expected: the ghost stays in the viewport, because deciding "what remains on screen" requires screen state.
- Assuming disabling prediction removes all repeated text. Expected: the per-character redraw frames remain, because they come from another path.
Three ways to disable prediction in a DeepSeek Harness terminal
All three paths remove the history half; they differ in whether you change configuration, the packaged default, or the shell. If you wrap terminal_send in your own DSH plugin, use the same three layers.
Path 1: add -Command through shellArgs
This is a stopgap that needs no code change and works immediately, at the cost of replacing the dialect defaults wholesale. According to the discussion, when shellArgs is non-empty the default -NoLogo -NoProfile no longer applies, so it must be written out in full.
- Open the persistent terminal plugin configuration in DSH and find
shellArgs. Expected: the current argument array is visible. - Enter the arguments below, keeping
-NoExitso the process stays interactive. Expected: the saved configuration contains four arguments. - Save the configuration and restart the app. Expected: no old pwsh process remains.
- Reopen the terminal and send
echo AAA. Expected: the viewport no longer shows old history commands, only the redraw staircase.
{
"shellArgs": ["-NoLogo", "-NoProfile", "-NoExit", "-Command", "Set-PSReadLineOption -PredictionSource None"]
}
Path 2: change the packaged default so every session inherits it
If every profile and every new session should start with prediction off, put the line in the package's startup path. According to the discussion, PWSH_PROMPT_SETUP currently defines only prompt, which is the cheapest landing point; changing DEFAULT_PWSH_ARGS works as well.
- In
terminal-bash'sconfig.ts, findDEFAULT_PWSH_ARGSor wherePWSH_PROMPT_SETUPis assembled. Expected: the default argument list or the prompt setup script is located. - Append
-NoExit -Command "Set-PSReadLineOption -PredictionSource None", or add the same command to the prompt script. Expected: every new pwsh process receives the prediction command. - Rebuild and reload the plugin, then call
terminal_openonce. Expected: the new package is loaded. - Run
Get-PSReadLineOption | Select-Object PredictionSource. Expected: output isNone; reapply the change after a package upgrade because the upstream fix has not landed.
Path 3: switch the session to a bash terminal
Avoiding PowerShell's prediction mechanism entirely by switching that session to the bash dialect is the simplest option. It suits projects already working in Git Bash or WSL.
- In the persistent terminal configuration, change the dialect or backend from pwsh to bash. Expected: the configuration points at the bash dialect.
- Reopen the terminal. Expected: the prompt is bash, not PowerShell, and PSReadLine is not loaded.
- Send the same command and inspect the viewport. Expected: no prediction ghost appears.
- Do not carry over the pwsh
-Commandarguments. Expected: bash fails at startup on unknown arguments; Windows also needs a working bash environment, and path separators and escaping rules change with it.
Verification and trade-offs after the DeepSeek Harness fix
All three paths remove only the history half; per-character redraw frames remain in the viewport. According to the discussion, the structural solution is a screen snapshot, not another filter change.
- Use
terminal_sendto enter only the prefixsshwithout submitting. Expected: no history is echoed for the prefix. - Inspect the
terminal_openmotd andterminal_readhistory. Expected: no ghost text remains. - If multiple frames still appear on one line, that is the sanitizer discarding CSI. Expected: only the screen-snapshot approach based on
@xterm/headlesscan address it. - Repeat the three checks under a brand-new profile. Expected: the configuration applies per profile, and the distinction between local display and model context stays clear.
- Restart the app and send a prefix again. Expected: the fix survives the restart instead of disappearing with the current process.
Source: Discussion #10070.
FAQ
In DeepSeek Harness this is not a cross-session leak: PSReadLine inline prediction renders historical commands as an input preview. According to Discussion #10070, every terminal_open starts a new pwsh process whose Get-History contains only this session's entries, and the PTY is not reused; the old commands come from the system-wide ConsoleHost_history.txt. Running Set-PSReadLineOption -PredictionSource None makes the ghosts disappear.
A DSH plugin captures the ghost text as input in the model context. A user sees one faint preview line, while the model receives multiple redraw frames concatenated after the sanitizer strips ANSI, and that is sent to the provider with the next LLM request. These are not the same disclosure, and with submit=false a prefix can deliberately fish for history.
In DeepSeek Harness, a non-empty shellArgs replaces the dialect defaults instead of appending to them. According to the community's source review in the thread, the defaults are only -NoLogo -NoProfile, so a custom value must spell out -NoLogo -NoProfile -NoExit -Command plus the command that disables prediction; otherwise the no-logo and no-profile behavior is lost.
In DeepSeek Harness, that noise comes from how TerminalSanitizer filters the stream, not from the prediction source. The sanitizer discards CSI sequences, so redraw frames concatenate on one line, and \r is rewritten to \n, so per-key frames cannot be retracted. The ghosts can be turned off; redraw frames need the screen-snapshot approach, and the package already depends on @xterm/headless.
Related Terms
- PSReadLine inline prediction
- PSReadLine inline prediction draws suggestions from history or plugins after the cursor in a faint or italic style. Interactive pwsh defaults `PredictionSource` to `HistoryAndPlugin`, so even a new process reads the system-wide history file and shows ghost text.— https://github.com/deepseek-ai/deepseek-harness/discussions/10070
- TerminalSanitizer
- TerminalSanitizer is the filter layer that turns raw PTY bytes into model-readable text in the DSH persistent terminal. It does not emulate a screen; it removes ANSI control sequences and normalizes newlines, so it cannot know what remains visible on screen.— https://github.com/deepseek-ai/deepseek-harness/discussions/10070
- viewport
- A viewport is the visible terminal text that `terminal_send` returns to the model. It is assembled from sanitizer output and becomes part of the model context in later LLM requests.— https://github.com/deepseek-ai/deepseek-harness/discussions/10070
Sources
- deepseek-harness Discussion #10070: persistent terminal leaks PSReadLine prediction ghosts (shell history) into the model context· deepseek-ai (GitHub Discussions)