You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
History hygiene for shell commands and spawned shells (spike)
Motivation
Complete command lines submitted to an interactive shell through this MCP server are eligible for that shell's history according to the user's shell configuration. This can pollute interactive recall and persist command text to disk. Raw send_keys input is broader than shell commands: it also carries control keys, partial text, and TUI input, so it cannot safely inherit a policy that prepends characters.
The repository already has a per-call mitigation: suppress_history: bool = False on send_keys, send_keys_batch operations, and run_command (#73). It prepends one ASCII space and is effective only when the receiving shell is already configured to ignore space-prefixed commands.
This spike proposes a startup default for semantic shell-command surfaces, best-effort persistent-history controls for shells spawned by MCP tools, explicit per-call overrides, honest capability reporting, and functional coverage.
Guarantee and non-goals
The feature requests best-effort shell-history suppression; it does not promise zero trace or secret transport.
The first target is avoiding persistent shell-history files where the active shell honors the selected mechanism.
Bash may omit a line entirely when ignorespace is active; Zsh and Fish may retain a suppressed line temporarily in memory.
Shell startup files can override environment values or history options after spawn.
Pane echo, tmux scrollback, capture_pane, pipe_pane, attached-terminal scrollback, shell hooks, process visibility, MCP/client transcripts, and application logs are separate surfaces.
Raw terminal input, paste paths, arbitrary multiline input, and direct tmux process-launch commands must never be modified implicitly.
If a workflow needs to pass a secret, prefer a credential helper, pre-existing environment reference, file descriptor, or already-hidden prompt instead of injecting the literal through terminal input.
Verified shell behavior
Shell
Leading-space behavior
Stronger spawn-time control
Important caveat
Bash
Works only when HISTCONTROL contains ignorespace or ignoreboth; upstream Bash otherwise saves the line.
Empty or unset HISTFILE prevents history-file persistence; merging ignorespace into HISTCONTROL enables the prefix convention.
History is processed before execution, so a same-line HISTCONTROL=... command or set +o history; command wrapper cannot protect itself. See the GNU Bash history documentation.
Zsh
Works only when HIST_IGNORE_SPACE is enabled; the ignored event remains temporarily recallable until another event replaces it.
Empty or unset HISTFILE prevents history-file persistence, subject to startup files resetting it.
fc -p "" 0 0; command; fc -P on one submitted line does not work: Zsh adds the complete outer line before executing fc -p. A disposable history context requires separate accepted inputs or a pre-execution hook. See Zsh 5.9 options, zshaddhistory, and execution order.
Fish
Without a custom fish_should_add_to_history, a leading space creates an ephemeral entry that is removed after the next command.
fish_private_mode=1 and an empty fish_history avoid normal disk persistence for a spawned Fish shell.
Defining fish_should_add_to_history replaces the default leading-space rule, and Fish's bracketed-paste handling can remove leading whitespace. See the Fish 4.7.0 hook contract and reader implementation.
Dash
Dash itself has no persistent interactive history facility.
None required for Dash itself.
Do not generalize this to every shell invoked as sh; the implementation behind sh is platform-dependent. See dash(1).
PowerShell
No leading-space convention.
PSReadLine exposes AddToHistoryHandler, including memory-only and skip outcomes.
Shell-specific integration is outside the first POSIX-shell implementation. See Set-PSReadLineOption.
Nushell
Leading-space exclusion is supported and configurable.
Nushell-specific configuration is outside the first implementation.
Preserve as documented follow-up coverage rather than claiming generic shell support. See nushell#9371.
Constraints established by QA
Raw send_keys cannot inherit the server default
Current send_keys and both send_keys_batch paths implement suppression by prepending a literal space. Upstream Pane.send_keys does the same unconditionally when requested.
An isolated tmux probe demonstrated the failure mode: while sleep was running, sending the suppressed form of C-c inserted literal C-c and did not interrupt the process; sending plain C-c did. Checking only enter is insufficient because TUI searches and application actions commonly submit input with Enter.
send_keys and send_keys_batch must therefore keep explicit, default-off suppression. A server default may apply only to an operation that is semantically known to submit a shell command, such as run_command or a future send_shell_command.
One prefix does not protect arbitrary multiline input
Isolated Bash, Zsh, and Fish probes submitted a space-prefixed first line followed by an unprefixed second line. Each shell suppressed only the first accepted event and persisted the second. A semantic command surface must reject ambiguous raw multiline input, define one accepted shell event, or use a separate mechanism. Paste tools remain outside this feature.
Spawn scope differs by tool
create_session(environment=...) stores values in the tmux session environment, as demonstrated by the repository's functional test and tmux's session construction. Later panes created in that session inherit those values. This is intentionally session-wide behavior, not per-process-only behavior, and must be documented as such.
Environment supplied to create_window, split_window, or respawn_pane is scoped to the process spawned by that operation. Direct shell=, window_shell=, or equivalent launch strings are tmux process commands, not lines typed into an interactive shell, so they must never receive a leading-space prefix.
Current repository state
The current source baseline is d717818; the v0.1.0a16 tag points to an earlier commit, so current-code links in this issue use the seven-character commit reference.
libtmux 0.61.0 supports environment forwarding on Session.new_window, Pane.split, and Pane.respawn; tmux has exposed -e on these commands since the supported 3.2a floor.
paste_text and paste_buffer have no history handling and remain out of scope.
Only run_command currently has an end-to-end suppression test. The send_keys and send_keys_batch paths lack direct suppression coverage.
The current history gotcha omits send_keys_batch and incorrectly implies that Bash enables ignorespace by default.
Proposed MCP-native design
Startup setting and precedence
Add LIBTMUX_SUPPRESS_HISTORY=0|1, resolved once when the server starts and defaulting to 0. Invalid explicit values should fail startup with an actionable configuration error because silently disabling a requested privacy control is unsafe.
For semantically eligible tools, expose the effective non-nullable boolean as the JSON Schema default. Precedence is explicit per-call value, then the startup setting, then backward-compatible False. This keeps tools/list honest and allows an explicit false to override a server configured with suppression enabled.
Do not add a mutable global MCP toggle tool. Startup configuration plus per-call overrides avoid cross-client races and surprising changes during concurrent use.
Tool behavior
Surface
Inherit LIBTMUX_SUPPRESS_HISTORY?
Behavior
run_command
Yes
It is the existing semantic shell-command surface; an explicit boolean overrides the startup default.
Future send_shell_command
Yes
Add only if persistent-shell state needs a dedicated semantic command tool; it should reject unknown/TUI contexts and ambiguous multiline input.
Apply the spawn environment policy when enabled, with an explicit per-call override.
Direct tmux launch commands
No prefix
Launch the requested process unchanged.
No safety-tier changes are required: all affected operations are already mutating. MCP has no standard history/privacy ToolAnnotation, so the implementation should not invent a privacyHint; use tool schemas, parameter descriptions, and a short server instruction describing the active default and the raw-input exclusion. Standard annotations remain advisory hints for properties such as read-only, destructive, and idempotent behavior (MCP tools specification).
Spawn environment policy
Add the same client-compatible environment: dict[str, str] | JSON string | None input shape used by create_session to create_window and split_window, and keep environment normalization in one helper.
When spawn suppression is enabled, build a copied environment mapping and apply best-effort controls without mutating caller data: empty HISTFILE for Bash/Zsh, merge ignorespace into HISTCONTROL without discarding existing values, and set Fish private-history variables for Fish startup. Do not send a post-startup setup line in the first implementation; that requires shell detection, readiness synchronization, restoration semantics, and interruption-safe cleanup.
If caller-supplied environment values conflict with enabled suppression, return an actionable tool error rather than silently weakening the requested policy or retrying without it. Document that create_session applies the resulting history environment to the session and future panes, while the other spawn operations affect only their spawned process.
Discoverability and logging
The effective JSON Schema default is authoritative. Parameter descriptions should state the mechanism, precedence, shell dependency, and best-effort guarantee. Server instructions may summarize the active default, but they supplement rather than replace schemas and must remain within the existing instruction budget.
History suppression does not itself make command text safe for logs. libtmux 0.61.0 includes shell-joined tmux arguments in DEBUG and exception records (source). If the implementation or documentation claims secret hygiene, add an upstream-safe logging change or local filtering and prove with sentinel tests that command and history-environment values do not appear in captured records. Otherwise state this logging boundary explicitly as a non-goal.
Implementation slices
Semantic command default: add the startup resolver, bind the effective schema default to run_command, preserve explicit overrides, update instructions and docs, and add resolver/schema/behavior tests.
Spawn persistence policy: add environment parity to create_window and split_window, normalize environment input, apply the policy to all four spawn/respawn tools, document create_session scope, and test merge/conflict behavior.
Raw-input regression coverage: retain explicit-only suppression on send_keys and send_keys_batch, cover both batch paths, and prove that a server-enabled policy does not alter C-c, partial input, or TUI text.
Optional follow-ups: consider a dedicated send_shell_command; shell detection and post-startup setup; paste semantics; PowerShell/Nushell integration; and stronger no-memory or scrub modes. These should not block the first no-disk, best-effort implementation.
Acceptance criteria
LIBTMUX_SUPPRESS_HISTORY resolves once at startup, defaults off, and rejects invalid explicit values.
run_command advertises the effective boolean default through tools/list; explicit true and false override it.
send_keys and every SendKeysOperation continue to advertise false and never inherit the startup setting.
A regression test proves that server-enabled suppression does not turn C-c or TUI text into space-prefixed literal input.
Explicit suppression is covered for send_keys, untimed send_keys_batch, and timed send_keys_batch.
Semantic command handling either rejects ambiguous multiline input or proves that the complete payload is one protected shell-history event.
create_window and split_window accept normalized environment input with the same client-compatible shape as create_session.
Spawn policy and explicit overrides are covered for create_session, create_window, split_window, and respawn_pane.
Tests prove environment mappings are copied, compatible values are merged, conflicts fail clearly, and no unsuppressed retry occurs.
Controlled Bash, Zsh, and Fish tests distinguish mechanism application, disk persistence, and possible in-memory recall; unavailable shells skip with a documented reason.
FastMCP in-process tests inspect list_tools under both startup settings and exercise representative calls; uv run fastmcp inspect fastmcp.json confirms the production surface.
Documentation updates configuration, tool pages, shell limitations, spawn scope, logging/privacy boundaries, and CHANGES without claiming zero trace.
The full repository format, lint, type-check, test, documentation, and supported-tmux CI gates pass.
Prior art and references
tmuxp applies suppress_history to values already modeled as shell commands rather than arbitrary terminal input (v1.74.0 builder, documentation).
FastMCP recommends in-process client tests for inspecting and calling registered tools (v3.4.2 testing guide).
MCP clients may display tool inputs before execution, and annotations are hints rather than security controls (MCP tools specification).
History hygiene for shell commands and spawned shells (spike)
Motivation
Complete command lines submitted to an interactive shell through this MCP server are eligible for that shell's history according to the user's shell configuration. This can pollute interactive recall and persist command text to disk. Raw
send_keysinput is broader than shell commands: it also carries control keys, partial text, and TUI input, so it cannot safely inherit a policy that prepends characters.The repository already has a per-call mitigation:
suppress_history: bool = Falseonsend_keys,send_keys_batchoperations, andrun_command(#73). It prepends one ASCII space and is effective only when the receiving shell is already configured to ignore space-prefixed commands.This spike proposes a startup default for semantic shell-command surfaces, best-effort persistent-history controls for shells spawned by MCP tools, explicit per-call overrides, honest capability reporting, and functional coverage.
Guarantee and non-goals
The feature requests best-effort shell-history suppression; it does not promise zero trace or secret transport.
ignorespaceis active; Zsh and Fish may retain a suppressed line temporarily in memory.capture_pane,pipe_pane, attached-terminal scrollback, shell hooks, process visibility, MCP/client transcripts, and application logs are separate surfaces.If a workflow needs to pass a secret, prefer a credential helper, pre-existing environment reference, file descriptor, or already-hidden prompt instead of injecting the literal through terminal input.
Verified shell behavior
HISTCONTROLcontainsignorespaceorignoreboth; upstream Bash otherwise saves the line.HISTFILEprevents history-file persistence; mergingignorespaceintoHISTCONTROLenables the prefix convention.HISTCONTROL=... commandorset +o history; commandwrapper cannot protect itself. See the GNU Bash history documentation.HIST_IGNORE_SPACEis enabled; the ignored event remains temporarily recallable until another event replaces it.HISTFILEprevents history-file persistence, subject to startup files resetting it.fc -p "" 0 0; command; fc -Pon one submitted line does not work: Zsh adds the complete outer line before executingfc -p. A disposable history context requires separate accepted inputs or a pre-execution hook. See Zsh 5.9 options,zshaddhistory, and execution order.fish_should_add_to_history, a leading space creates an ephemeral entry that is removed after the next command.fish_private_mode=1and an emptyfish_historyavoid normal disk persistence for a spawned Fish shell.fish_should_add_to_historyreplaces the default leading-space rule, and Fish's bracketed-paste handling can remove leading whitespace. See the Fish 4.7.0 hook contract and reader implementation.sh; the implementation behindshis platform-dependent. See dash(1).AddToHistoryHandler, including memory-only and skip outcomes.Set-PSReadLineOption.Constraints established by QA
Raw
send_keyscannot inherit the server defaultCurrent
send_keysand bothsend_keys_batchpaths implement suppression by prepending a literal space. UpstreamPane.send_keysdoes the same unconditionally when requested.An isolated tmux probe demonstrated the failure mode: while
sleepwas running, sending the suppressed form ofC-cinserted literalC-cand did not interrupt the process; sending plainC-cdid. Checking onlyenteris insufficient because TUI searches and application actions commonly submit input with Enter.send_keysandsend_keys_batchmust therefore keep explicit, default-off suppression. A server default may apply only to an operation that is semantically known to submit a shell command, such asrun_commandor a futuresend_shell_command.One prefix does not protect arbitrary multiline input
Isolated Bash, Zsh, and Fish probes submitted a space-prefixed first line followed by an unprefixed second line. Each shell suppressed only the first accepted event and persisted the second. A semantic command surface must reject ambiguous raw multiline input, define one accepted shell event, or use a separate mechanism. Paste tools remain outside this feature.
Spawn scope differs by tool
create_session(environment=...)stores values in the tmux session environment, as demonstrated by the repository's functional test and tmux's session construction. Later panes created in that session inherit those values. This is intentionally session-wide behavior, not per-process-only behavior, and must be documented as such.Environment supplied to
create_window,split_window, orrespawn_paneis scoped to the process spawned by that operation. Directshell=,window_shell=, or equivalent launch strings are tmux process commands, not lines typed into an interactive shell, so they must never receive a leading-space prefix.Current repository state
The current source baseline is
d717818; thev0.1.0a16tag points to an earlier commit, so current-code links in this issue use the seven-character commit reference.send_keysdelegates suppression to libtmux.send_keys_batchstores suppression perSendKeysOperation; the timed path applies the prefix itself.run_commandapplies the prefix to its authored shell-command payload.LIBTMUX_SAFETYprovides the existing startup-resolution precedent.create_sessionandrespawn_panealready acceptenvironment;create_windowandsplit_windowdo not.Session.new_window,Pane.split, andPane.respawn; tmux has exposed-eon these commands since the supported 3.2a floor.paste_textandpaste_bufferhave no history handling and remain out of scope.run_commandcurrently has an end-to-end suppression test. Thesend_keysandsend_keys_batchpaths lack direct suppression coverage.send_keys_batchand incorrectly implies that Bash enablesignorespaceby default.Proposed MCP-native design
Startup setting and precedence
Add
LIBTMUX_SUPPRESS_HISTORY=0|1, resolved once when the server starts and defaulting to0. Invalid explicit values should fail startup with an actionable configuration error because silently disabling a requested privacy control is unsafe.For semantically eligible tools, expose the effective non-nullable boolean as the JSON Schema default. Precedence is explicit per-call value, then the startup setting, then backward-compatible
False. This keepstools/listhonest and allows an explicitfalseto override a server configured with suppression enabled.Do not add a mutable global MCP toggle tool. Startup configuration plus per-call overrides avoid cross-client races and surprising changes during concurrent use.
Tool behavior
LIBTMUX_SUPPRESS_HISTORY?run_commandsend_shell_commandsend_keyssuppress_history: bool = False; explicit opt-in retains the existing behavior.send_keys_batchSendKeysOperation.suppress_historyexplicit and default-off in both timed and untimed paths.paste_text/paste_buffercreate_session/create_window/split_window/respawn_paneNo safety-tier changes are required: all affected operations are already mutating. MCP has no standard history/privacy
ToolAnnotation, so the implementation should not invent aprivacyHint; use tool schemas, parameter descriptions, and a short server instruction describing the active default and the raw-input exclusion. Standard annotations remain advisory hints for properties such as read-only, destructive, and idempotent behavior (MCP tools specification).Spawn environment policy
Add the same client-compatible
environment: dict[str, str] | JSON string | Noneinput shape used bycreate_sessiontocreate_windowandsplit_window, and keep environment normalization in one helper.When spawn suppression is enabled, build a copied environment mapping and apply best-effort controls without mutating caller data: empty
HISTFILEfor Bash/Zsh, mergeignorespaceintoHISTCONTROLwithout discarding existing values, and set Fish private-history variables for Fish startup. Do not send a post-startup setup line in the first implementation; that requires shell detection, readiness synchronization, restoration semantics, and interruption-safe cleanup.If caller-supplied environment values conflict with enabled suppression, return an actionable tool error rather than silently weakening the requested policy or retrying without it. Document that
create_sessionapplies the resulting history environment to the session and future panes, while the other spawn operations affect only their spawned process.Discoverability and logging
The effective JSON Schema default is authoritative. Parameter descriptions should state the mechanism, precedence, shell dependency, and best-effort guarantee. Server instructions may summarize the active default, but they supplement rather than replace schemas and must remain within the existing instruction budget.
History suppression does not itself make command text safe for logs. libtmux 0.61.0 includes shell-joined tmux arguments in DEBUG and exception records (source). If the implementation or documentation claims secret hygiene, add an upstream-safe logging change or local filtering and prove with sentinel tests that command and history-environment values do not appear in captured records. Otherwise state this logging boundary explicitly as a non-goal.
Implementation slices
run_command, preserve explicit overrides, update instructions and docs, and add resolver/schema/behavior tests.create_windowandsplit_window, normalize environment input, apply the policy to all four spawn/respawn tools, documentcreate_sessionscope, and test merge/conflict behavior.send_keysandsend_keys_batch, cover both batch paths, and prove that a server-enabled policy does not alterC-c, partial input, or TUI text.send_shell_command; shell detection and post-startup setup; paste semantics; PowerShell/Nushell integration; and stronger no-memory or scrub modes. These should not block the first no-disk, best-effort implementation.Acceptance criteria
LIBTMUX_SUPPRESS_HISTORYresolves once at startup, defaults off, and rejects invalid explicit values.run_commandadvertises the effective boolean default throughtools/list; explicittrueandfalseoverride it.send_keysand everySendKeysOperationcontinue to advertisefalseand never inherit the startup setting.C-cor TUI text into space-prefixed literal input.send_keys, untimedsend_keys_batch, and timedsend_keys_batch.create_windowandsplit_windowaccept normalized environment input with the same client-compatible shape ascreate_session.create_session,create_window,split_window, andrespawn_pane.list_toolsunder both startup settings and exercise representative calls;uv run fastmcp inspect fastmcp.jsonconfirms the production surface.Prior art and references
suppress_historyto values already modeled as shell commands rather than arbitrary terminal input (v1.74.0 builder, documentation).suppress_historyonrun_command.