DeepSeek Harness terminal: PSReadLine ghosts in context

TroubleshootingPublished 2026-10-20Author: DeepSeek Plugin Market
DeepSeek HarnessPSReadLinepersistent terminalmodel contextterminal_send
In DeepSeek Harness, untyped commands appear in terminal_send because PSReadLine predictions enter model context. Disable them via shellArgs, defaults, or bash.

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.

  1. Send terminal_send "echo AAA". Expected: the viewport shows a line such as echo a that was never entered, which is a prediction ghost from prefix matching.
  2. Look at the line shape. Expected: e, ec, ech, echo is a per-character redraw staircase, not model or user input.
  3. Compare terminal_read history and run Get-History. Expected: only this session's entries appear, which rules out cross-session leakage.
  4. In the same session run Set-PSReadLineOption -PredictionSource None, then send echo ZZZ. Expected: the ghost disappears and the staircase remains, which pins the cause to the prediction source.
  5. 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:

  1. terminal_open starts a new pwsh process with -NoLogo -NoProfile. Expected: no custom profile is loaded.
  2. PSReadLine reads ConsoleHost_history.txt. Expected: prefix-matched history is rendered as a faint or italic ghost.
  3. TerminalSanitizer removes all ANSI, including the SGR that marks the ghost style. Expected: the ghost becomes ordinary text.
  4. terminal_send returns 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:

  1. Every LLM request resends the viewports already in context. Expected: the ghost is transmitted repeatedly.
  2. submit=false only removes the trailing Enter. Expected: a prefix such as ssh or export can deliberately fish out matching history.
  3. 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.
  4. 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:

  1. Interactive pwsh defaults PredictionSource to HistoryAndPlugin; a non-interactive -Command probe reports None. Expected: the probe gives a misleading result and makes prediction look disabled.
  2. TerminalSanitizer filters the stream rather than emulating a screen: its CSI branch skips parameters and drops cursor positioning, and \\x08 passes through unchanged. Expected: redraw frames concatenate on one line.
  3. 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.
  4. 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:

  1. Treating old commands as a cross-session leak. Expected: Get-History shows only this session's records, because every terminal_open is a new process.
  2. Probing PredictionSource with -Command and getting None. Expected: the result is not representative, because non-interactive mode does not load prediction.
  3. Assuming the sanitizer can erase ghosts. Expected: the ghost stays in the viewport, because deciding "what remains on screen" requires screen state.
  4. 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.

  1. Open the persistent terminal plugin configuration in DSH and find shellArgs. Expected: the current argument array is visible.
  2. Enter the arguments below, keeping -NoExit so the process stays interactive. Expected: the saved configuration contains four arguments.
  3. Save the configuration and restart the app. Expected: no old pwsh process remains.
  4. Reopen the terminal and send echo AAA. Expected: the viewport no longer shows old history commands, only the redraw staircase.
json
{
  "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.

  1. In terminal-bash's config.ts, find DEFAULT_PWSH_ARGS or where PWSH_PROMPT_SETUP is assembled. Expected: the default argument list or the prompt setup script is located.
  2. 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.
  3. Rebuild and reload the plugin, then call terminal_open once. Expected: the new package is loaded.
  4. Run Get-PSReadLineOption | Select-Object PredictionSource. Expected: output is None; 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.

  1. In the persistent terminal configuration, change the dialect or backend from pwsh to bash. Expected: the configuration points at the bash dialect.
  2. Reopen the terminal. Expected: the prompt is bash, not PowerShell, and PSReadLine is not loaded.
  3. Send the same command and inspect the viewport. Expected: no prediction ghost appears.
  4. Do not carry over the pwsh -Command arguments. 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.

  1. Use terminal_send to enter only the prefix ssh without submitting. Expected: no history is echoed for the prefix.
  2. Inspect the terminal_open motd and terminal_read history. Expected: no ghost text remains.
  3. If multiple frames still appear on one line, that is the sanitizer discarding CSI. Expected: only the screen-snapshot approach based on @xterm/headless can address it.
  4. 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.
  5. 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

A terminal_send viewport in DeepSeek Harness shows old commands I never typed. Is this a cross-session leak?

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.

Why is this data exposure specific to a DSH plugin when users see prediction completions in their own terminals too?

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.

Why must shellArgs for the DeepSeek Harness terminal repeat -NoLogo -NoProfile when disabling prediction?

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.

After disabling PSReadLine prediction, why does a DeepSeek Harness terminal still show staircase text?

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