Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@
_Notes on upcoming releases will be added here_
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->

### Breaking changes

**History suppression now defaults on for run commands**

MCP clients now see the effective server default for `suppress_history` on {tooliconl}`run-command`, and calls that omit the argument inherit it. The default requests best-effort history suppression. To run multiline input while suppression is enabled, pass `suppress_history=false`; to retain the previous omitted-call behavior, set {envvar}`LIBTMUX_SUPPRESS_HISTORY` to `0` and restart the server. Direct Python calls remain unchanged. See {ref}`configuration` for details. (#91)

### What's new

**History controls for spawned shells**

{tooliconl}`create-session`, {tooliconl}`create-window`, {tooliconl}`split-window`, and {tooliconl}`respawn-pane` now let callers opt into best-effort no-disk shell-history controls with `suppress_persistent_history=true`. Session controls reach the initial and future panes, while the other tools limit the setting to the process launched by that call.

History suppression reduces accidental persistence, but it is not secret transport: shells can override the controls, and terminal output and other observation surfaces remain visible. See {ref}`history-hygiene` for shell behavior and {ref}`safety` for the remaining boundaries. (#91)

**Per-process environments for windows and panes**

{tooliconl}`create-window` and {tooliconl}`split-window` now accept per-process `environment` mappings or JSON object strings, so callers can configure new windows and panes without changing the tmux session environment. {tooliconl}`respawn-pane` now accepts the same JSON object form for its existing environment input. Values can still surface in host process inspection and child environments; use credential references, not literal credentials. See {ref}`safety` for details. (#91)

### Documentation

**Grok CLI and Antigravity in the MCP install picker**
Expand Down
22 changes: 21 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,25 @@ Safety tier controlling which tools are available. See {ref}`safety`.
- **Default:** `mutating`
- **Values:** `readonly`, `mutating`, `destructive`

```{envvar} LIBTMUX_SUPPRESS_HISTORY
```

Controls the MCP default for lightweight, best-effort command-history suppression. This setting applies only when an MCP caller omits `suppress_history` from {tooliconl}`run-command`.

- **Type:** string flag
- **Default:** `1` (enabled)
- **Values:** `0`, `1`

Unset and `1` enable suppression; `0` disables it. Any other value fails server startup with `LIBTMUX_SUPPRESS_HISTORY must be unset, '0', or '1'`, without echoing the rejected value. An explicit `suppress_history` value wins for each MCP call. Direct Python calls default to `False`.

{toolref}`run-command` prefixes one space to the grouped event that carries your single-line command. When suppression is effective, a command containing a carriage return or line feed fails before tmux receives input; set `suppress_history=false` for intentional multiline input.

Process creation uses a separate control. {toolref}`create-session`, {toolref}`create-window`, {toolref}`split-window`, and {toolref}`respawn-pane` expose `suppress_persistent_history`, which defaults to `false` for MCP and direct Python calls and never inherits this startup setting. Setting it to `true` copies and merges best-effort no-disk history controls into the spawned environment. A conflicting caller-supplied history value fails the call, names the environment variable without including the conflicting value, and is never retried without suppression.

Leaving it `false` adds no history controls. That choice cannot remove inherited, session, or startup-file controls; the process can still receive them from tmux, your supplied `environment`, or a shell startup file. The startup default never changes the raw-input behavior of {toolref}`send-keys`, {toolref}`send-keys-batch`, {toolref}`paste-text`, or {toolref}`paste-buffer`.

The server resolves {envvar}`LIBTMUX_SUPPRESS_HISTORY` once during startup. Restart the MCP server only after changing this startup setting, usually by reconnecting or restarting the MCP client. Per-call arguments take effect without a restart. See {ref}`history-hygiene` for shell-specific limits and {ref}`safety` for surfaces that history suppression does not hide.

## Setting environment variables

Set environment variables in your MCP client config:
Expand All @@ -51,7 +70,8 @@ Set environment variables in your MCP client config:
"args": ["libtmux-mcp"],
"env": {
"LIBTMUX_SOCKET": "ai_workspace",
"LIBTMUX_SAFETY": "readonly"
"LIBTMUX_SAFETY": "readonly",
"LIBTMUX_SUPPRESS_HISTORY": "1"
}
}
}
Expand Down
26 changes: 26 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,32 @@ Tear down tmux objects. Not reversible.

{toolref}`kill-session` · {toolref}`kill-window` · {toolref}`kill-pane` · {toolref}`kill-server` · {toolref}`call-destructive-tools-batch`

### Example: keep test runs out of persistent history

libtmux-mcp provides best-effort history suppression for Bash, Zsh, and Fish.
MCP calls to {tooliconl}`run-command` use lightweight command suppression by
default. When you create a new shell, opt into stronger no-disk controls with
`suppress_persistent_history=true`.

```{admonition} Prompt
:class: prompt

Create a tmux session called "checks" with best-effort no-disk shell-history
controls, run `pytest -q` in its initial pane, and show me the result.
```

The agent calls {tooliconl}`create-session` with
`suppress_persistent_history=true`, reuses `active_pane_id` from the returned
{class}`~libtmux_mcp.models.SessionInfo`, and calls
{tooliconl}`run-command` without an override. The same spawn option on
{toolref}`create-window`, {toolref}`split-window`, or
{toolref}`respawn-pane` applies only to the process that call starts.

These controls reduce history noise; they do not make commands secret. See
{ref}`history-suppression` for shell-specific behavior,
{ref}`configuration` for the server default, and {ref}`safety` for other
observation surfaces.

[Browse all tools →](tools/index)

---
Expand Down
12 changes: 9 additions & 3 deletions docs/prompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ for completion, and inspect exit status plus output.

**Why use this instead of {tooliconl}`send-keys` + {tooliconl}`capture-pane` polling?**
{tooliconl}`run-command` sends the command, waits through a private
tmux signal, captures tail-preserved output, and returns exit status
in one typed result. That removes the manual channel plumbing from
the common authored-command workflow.
tmux signal, captures tail-preserved output, and returns a
{class}`~libtmux_mcp.models.RunCommandResult`. That removes the manual
channel plumbing from the common authored-command workflow.

```{fastmcp-prompt-input} run_and_wait
```
Expand Down Expand Up @@ -90,6 +90,12 @@ a one-shot shell command, use `send_keys` or `send_keys_batch`, then
observe later output with `capture_since`.
````

Single-line renders omit `suppress_history`, so MCP calls use the server's
enabled-by-default setting. If `command` contains a carriage return or line
feed, the prompt instead renders `suppress_history=False` and warns that the
shell may record the multiline command. See {ref}`history-suppression` for
the Bash, Zsh, and Fish behavior behind both cases.

For custom shell composition that falls outside {tooliconl}`run-command`,
compose ``tmux wait-for -S <channel>`` yourself and call
{tooliconl}`wait-for-channel`. Keep that as the low-level escape hatch,
Expand Down
10 changes: 8 additions & 2 deletions docs/tools/pane/respawn-pane.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ the layout: `respawn-pane` preserves the pane in place.
starts a new one. **The `pane_id` is preserved** — that's the whole
point of the tool. `pane_pid` updates to the new process.

`suppress_persistent_history` defaults to `false` for MCP and direct Python calls. It does not inherit {envvar}`LIBTMUX_SUPPRESS_HISTORY`. Leave it `false` to add no history controls for this call. That choice cannot remove inherited, session, or startup-file controls.

Set it to `true` and {tooliconl}`respawn-pane` copies and merges best-effort no-disk history controls for only the spawned process. It does not change the tmux session environment or affect later panes. The shell can retain in-memory history, and a startup file can override these controls after the process starts.

When you enable it, tmux environment arguments are added, but the spawned process command text is not prefixed or rewritten. The `shell` text is passed through unchanged. If you also pass `environment`, any history-control values must agree with the policy. A conflict fails the call, names the variable without including the conflicting value, and is never retried without suppression. See {ref}`history-hygiene` for shell behavior and {ref}`safety` for output, scrollback, process, transcript, hook, and logging boundaries.

**Tip:** Call {tooliconl}`get-pane-info` first if you need to capture
`pane_current_command` before respawn — the new process loses its argv.
Omitting `shell` makes tmux replay the original argv (good default for
Expand Down Expand Up @@ -65,9 +71,9 @@ time).
}
```

The audit log redacts each `environment` *value* via `{len, sha256_prefix}` digests but keeps the keys visible (env var names like `DATABASE_URL` are operator-debug-useful, while their values are the secret). Note that values may still appear briefly in the OS process table while tmux spawns the new process — see {ref}`safety` for details.
Mapping input keeps the keys visible in the audit log but replaces each `environment` *value* with a `{len, sha256_prefix}` digest. A JSON object string is redacted as one scalar digest, so its keys are not retained in the audit record. Values may still appear briefly in the OS process table while tmux spawns the new process — see {ref}`safety` for details.

Response (PaneInfo):
Response ({class}`~libtmux_mcp.models.PaneInfo`):

```json5
{
Expand Down
22 changes: 13 additions & 9 deletions docs/tools/pane/run-command.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@
```{fastmcp-tool} pane_tools.run_command
```

**Use when** you need to run a shell command in a pane and get a typed
result with exit status, timeout state, and captured pane output.
**Use when** you need to run a shell command in a pane and get a
{class}`~libtmux_mcp.models.RunCommandResult` with exit status, timeout state,
and captured pane output.

**Avoid when** you need raw interactive key driving — use
{tooliconl}`send-keys` or {tooliconl}`send-keys-batch` for TUIs, key
names, and partial commands.

**Side effects:** Sends a command to the pane's interactive shell. The
command may read or write files, start processes, or access the network
depending on what the shell command does. Each command runs in a subshell,
so directory or environment changes do not persist across calls.
Set `suppress_history=true` for secret-bearing commands on shells that
honor leading-space history suppression.
**Side effects:** Sends a command to the pane's interactive shell. The command may read or write files, start processes, or access the network depending on what the shell command does. Each command runs in a subshell, so directory or environment changes do not persist across calls.

For MCP calls, lightweight suppression is enabled by default. The {ref}`configuration <configuration>` setting {envvar}`LIBTMUX_SUPPRESS_HISTORY` controls only omitted MCP `suppress_history` arguments, and an explicit `suppress_history` value wins. Direct Python calls default to `False`. `suppress_history=false` permits intentional multiline input.

Suppression is best effort: {tooliconl}`run-command` prefixes one space to the grouped event that carries your single-line command, but the existing shell must be configured to ignore space-prefixed commands. When suppression is effective, a command containing a carriage return or line feed is rejected before tmux receives input because one prefix cannot protect multiple shell events. This control does not change the shell's environment or startup configuration, clear its memory or pane scrollback, or hide the command from other observers. See {ref}`history-hygiene` for shell behavior and {ref}`safety` before handling credentials.

**Example:**

Expand All @@ -30,7 +30,7 @@ honor leading-space history suppression.
}
```

Response:
Response ({class}`~libtmux_mcp.models.RunCommandResult`):

```json
{
Expand All @@ -44,5 +44,9 @@ Response:
}
```

```{note}
The generated parameter table below reflects the direct Python signature, so it shows `suppress_history=False`. MCP `tools/list` advertises the effective suppression default instead: `true` unless {envvar}`LIBTMUX_SUPPRESS_HISTORY` is `0`. An MCP call that omits the argument uses that advertised default.
```

```{fastmcp-tool-input} pane_tools.run_command
```
13 changes: 12 additions & 1 deletion docs/tools/server/create-session.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ container — create one before creating windows or panes.

**Side effects:** Creates a new tmux session with one window and one pane.

**Do not pass credentials directly in `environment`.** Values persist in the
new session, can be inspected with {tooliconl}`show-environment`, and reach
its initial pane and future panes. Pass credential references instead; see
{ref}`safety` for details.

`suppress_persistent_history` defaults to `false` for MCP and direct Python calls. It does not inherit {envvar}`LIBTMUX_SUPPRESS_HISTORY`. Leave it `false` to add no history controls for this call. That choice cannot remove inherited, session, or startup-file controls.

Set it to `true` and {tooliconl}`create-session` copies and merges best-effort no-disk history controls into the tmux session environment. They reach the initial pane and future panes in that session. The shell can retain in-memory history, and a startup file can override these controls after the process starts.

When you enable it, tmux environment arguments are added, but the spawned process command text is not prefixed or rewritten. If you also pass `environment`, any history-control values must agree with the policy. A conflict fails the call, names the variable without including the conflicting value, and is never retried without suppression. See {ref}`history-hygiene` for shell behavior and {ref}`safety` for output, scrollback, process, transcript, hook, and logging boundaries.

**Example:**

```json
Expand All @@ -22,7 +33,7 @@ container — create one before creating windows or panes.
}
```

Response:
Response ({class}`~libtmux_mcp.models.SessionInfo`):

```json
{
Expand Down
30 changes: 29 additions & 1 deletion docs/tools/session/create-window.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@

**Side effects:** Creates a new window. Attaches to it if `attach` is true.

`environment` accepts a mapping or JSON object string and applies only to the
process started in the new window; it does not change the tmux session
environment. Values can remain visible in the tmux client argv and child
environment even though the MCP audit record is redacted. Pass credential
references, not literal credentials; see {ref}`safety`.

`suppress_persistent_history` defaults to `false` for MCP and direct Python calls. It does not inherit {envvar}`LIBTMUX_SUPPRESS_HISTORY`. Leave it `false` to add no history controls for this call. That choice cannot remove inherited, session, or startup-file controls.

Set it to `true` and {tooliconl}`create-window` copies and merges best-effort no-disk history controls for only the spawned process. It does not change the tmux session environment, so future windows and panes do not receive the controls from this call. The shell can retain in-memory history, and a startup file can override these controls after the process starts.

When you enable it, tmux environment arguments are added, but the spawned process command text is not prefixed or rewritten. If you also pass `environment`, any history-control values must agree with the policy. A conflict fails the call, names the variable without including the conflicting value, and is never retried without suppression. See {ref}`history-hygiene` for shell behavior and {ref}`safety` for output, scrollback, process, transcript, hook, and logging boundaries.

**Example:**

```json
Expand All @@ -19,7 +31,23 @@
}
```

Response:
**Example — launch a test window with scoped settings:**

```json
{
"tool": "create_window",
"arguments": {
"session_name": "dev",
"window_name": "tests",
"environment": {
"PYTHONPATH": "src"
},
"suppress_persistent_history": true
}
}
```

Response ({class}`~libtmux_mcp.models.WindowInfo`):

```json
{
Expand Down
30 changes: 29 additions & 1 deletion docs/tools/window/split-window.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ window.

**Side effects:** Creates a new pane by splitting an existing one.

`environment` accepts a mapping or JSON object string and applies only to the
process started in the new pane; it does not change the tmux session
environment. Values can remain visible in the tmux client argv and child
environment even though the MCP audit record is redacted. Pass credential
references, not literal credentials; see {ref}`safety`.

`suppress_persistent_history` defaults to `false` for MCP and direct Python calls. It does not inherit {envvar}`LIBTMUX_SUPPRESS_HISTORY`. Leave it `false` to add no history controls for this call. That choice cannot remove inherited, session, or startup-file controls.

Set it to `true` and {tooliconl}`split-window` copies and merges best-effort no-disk history controls for only the spawned process. It does not change the tmux session environment, so later panes do not receive the controls from this call. The shell can retain in-memory history, and a startup file can override these controls after the process starts.

When you enable it, tmux environment arguments are added, but the spawned process command text is not prefixed or rewritten. The `shell` text is passed through unchanged. If you also pass `environment`, any history-control values must agree with the policy. A conflict fails the call, names the variable without including the conflicting value, and is never retried without suppression. See {ref}`history-hygiene` for shell behavior and {ref}`safety` for output, scrollback, process, transcript, hook, and logging boundaries.

**Example:**

```json
Expand All @@ -20,7 +32,23 @@ window.
}
```

Response:
**Example — split off a test pane with scoped settings:**

```json
{
"tool": "split_window",
"arguments": {
"session_name": "dev",
"direction": "right",
"environment": {
"APP_ENV": "test"
},
"suppress_persistent_history": true
}
}
```

Response ({class}`~libtmux_mcp.models.PaneInfo`):

```json
{
Expand Down
15 changes: 9 additions & 6 deletions docs/topics/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,16 @@ Pane IDs like `%0`, `%5`, `%12` are unique across all sessions and windows withi

However, they reset when the tmux **server** restarts. Do not cache pane IDs across server restarts. After killing and recreating a session, re-discover pane IDs with {tooliconl}`list-panes`.

## `suppress_history` requires shell support
## Shell-history suppression is best effort

The `suppress_history` parameter on {tooliconl}`send-keys` and
{tooliconl}`run-command` prepends a space before the command, which prevents it
from being saved in shell history. This only works if the shell's `HISTCONTROL`
variable includes `ignorespace` (the default for bash, but not universal across
all shells).
MCP calls to {tooliconl}`run-command` request lightweight suppression by
default, but Bash, Zsh, and Fish each decide whether and how a command reaches
history. Stronger no-disk controls for newly spawned shells are available only
when you opt into `suppress_persistent_history=true`.

Neither control makes commands secret, and raw TUI or paste input stays
outside the default. See {ref}`history-suppression` for the two-level model,
shell-specific behavior, multiline commands, scope, and remaining visibility.

## Gemini CLI injects `wait_for_previous` into tool arguments

Expand Down
Loading