# Setup

Wire murmur after `npm install -g @mu-crew/murmur`. Selling and surface
overview: [README](../README.md). Hard ssh cases: [SSH.md](../SSH.md).

## Per-node init

On every node that runs agents:

```bash
murmur init                      # this node's identity, and hook entries
pi install npm:@mu-crew/murmur   # the agent-side extension, as a pi package
```

`murmur init` also writes the `murmur hook` entries for each hook harness set
up on the node (see [below](#claude-code-codex-cursor-cli-and-muse-code-murmur-hook)).
Run it again after an upgrade; a second run changes nothing.

pi loads the extension from the package's `pi` manifest. The CLI and the
extension are two installs of the same package, so upgrade them together:

```bash
npm i -g @mu-crew/murmur@latest
pi update npm:@mu-crew/murmur    # then /reload running agents
```

Both share one store, in `~/.local/state/murmur2` (`$XDG_STATE_HOME/murmur2`
when that is set; `MURMUR_STATE_DIR` moves it). When the schema changes, the
newer side rebuilds the store and the older side refuses it rather than
rebuilding it back: a CLI verb prints `written by a newer murmur` and exits 4
(`notify` and `clear`, which run from hooks, stay silent), the dash shows the
message, and an older extension records nothing until it is upgraded. An
extension that already had the store open when a newer CLI rebuilt it goes
stale (its agent drops out of the view) until it reloads. 1.x builds keep their
own directory (below).

### Coming from murmur 1.x

2.x keeps its state in `~/.local/state/murmur2`, not 1.x's
`~/.local/state/murmur`: 1.x refuses the store the hook harnesses need, so the
two cannot share one. The first 2.x command copies the node's `identity.json`
(the same `host_id`, so peers still know it) and the old store's peers. It
never writes to the old directory, so a 1.x murmur keeps working there, but
the two do not see each other's agents: upgrade the CLI and the pi package
together, then `/reload` running pi agents. Nothing is migrated under
`MURMUR_STATE_DIR`. Once nothing runs 1.x, remove `~/.local/state/murmur`.

### Coming from `murmur link pi`

The old `link pi` wrote a shim at `~/.pi/agent/extensions/murmur.ts`. Left in
place next to the package, pi loads the extension twice and every event is
reported twice. In this order:

```bash
pi install npm:@mu-crew/murmur   # once: registers the package
murmur pi migrate                # report only
murmur pi migrate --apply        # move the shim aside; nothing is deleted
```

Only a file byte for byte equal to a shim `link pi` generated (its two generated
paths aside) is moved, into `~/.pi/agent/disabled/`, which pi does not read. A
`link pi --copy` install, the inlined copy v0.1 `link pi` wrote, or an edited
file is reported and left for you: move it aside yourself.
`MURMUR_PI_HOME` changes the home directory it starts from.

Running agents keep the old extension code until they restart. Order matters:
without `murmur init` the extension loads and records nothing. Agents must run
**inside tmux**. A pane is the address; without one there is nothing to jump to.

## tmux focus hooks

Looking at a finished agent clears its badge. On every node, in `.tmux.conf`:

```tmux
# Run murmur only when the pane, its window or its session shows a badge.
%hidden MURMUR_ATTN="#{||:#{m/r:^(done|blocked|error|crashed)$,#{@murmur_pane_state}},#{||:#{m/r:^(done|blocked|error|crashed)$,#{@murmur_window_state}},#{m/r:^(done|blocked|error|crashed)$,#{@murmur_session_state}}}}"
%hidden MURMUR_CLEAR="run-shell -b 'murmur clear --pane #{pane_id}'"
set-hook -g after-select-pane      "if -F '$MURMUR_ATTN' \"$MURMUR_CLEAR\""
set-hook -g after-select-window    "if -F '$MURMUR_ATTN' \"$MURMUR_CLEAR\""
set-hook -g client-session-changed "if -F '$MURMUR_ATTN' \"$MURMUR_CLEAR\""
```

The gate skips a Node start on switches with nothing to acknowledge. Window and
session states count too, so orphan badges (from a pane that is gone) still
clear.

These are per node and not optional. `murmur clear` is what acknowledges an
attention request. A node's own snapshot is what every peer reads — without
these hooks, finished agents stay `done` in every peer's list.

Verify with `tmux show-hooks -g`. `set-hook` accepts a name your tmux does not
have and exits 0, so a wrong name fails silently.

Pass the pane id explicitly: hooks run in the tmux server, where `$TMUX_PANE` is
unset. Without it, a window holding an agent and a shell clears when you focus
the shell.

Focus can only cancel attention. It cannot stop a running agent or alter what
the agent reported about itself.

A focus hook needs no socket: it runs inside the server whose pane it names.
From outside that server, name it as `murmur pane` does, with
`murmur clear --pane %N -L <name>` or `-S <path>`; mu does this to acknowledge
a failed turn on its own tmux server before retrying it. A hook harness's
next prompt clears that pane's `error` and `done` by itself.

## Side panel

Bind the side panel in `.tmux.conf`:

```tmux
bind C-m run-shell "murmur sidepanel"
```

Then press `prefix + C-m` to toggle it for the current window. This key is free
in tmux's default prefix table. `prefix + m` and `prefix + M` are not free: tmux
uses them to set and clear a pane mark.

The panel opens on the left at 25 percent of the window width clamped to
25–40 columns, then shrinks to fit its widest line (never below 16 columns). It
spans the full viewport even when the existing layout is nested, and takes focus. Toggling it again removes only that window's panel.

Press `j` or `Down` to select the next agent, and `k` or `Up` to select the
previous agent. Press `g` or `Home` to select the first agent, and `G` or `End`
to select the last agent. Press `?` to show all panel keys. Press `?` or
`Escape` to close the help panel. Press `c` to toggle compact one-line rows. With
the mouse, click a row to select it, double-click it to jump to that agent, and
scroll the wheel to move the selection. The side panel and dashboard share this compact setting, along with the saved sort
and visibility settings.

## Harnesses other than pi

pi reports in-process through the extension. Claude Code, Codex, the Cursor
CLI (`cursor-agent`) and Muse Code report through `murmur hook`, which gives
them ownership of their pane, `running` and `idle`, `blocked`, and the last
turn's text. The rest — opencode, and Codex or the Cursor CLI set up the older
way — use `murmur notify`.

### Claude Code, Codex, Cursor CLI and Muse Code: `murmur hook`

`murmur init` writes these entries for you:

```bash
murmur init                          # every harness whose config directory exists
murmur init --hooks cursor-agent     # also one not set up yet (or: all)
murmur init --no-hooks               # identity only
```

It writes `~/.claude/settings.json`, `~/.codex/hooks.json`,
`~/.cursor/hooks.json` and the `hooks` key of `~/.config/muse/settings.json`,
for each harness whose directory (`~/.claude`, `~/.codex`, `~/.cursor`,
`~/.config/muse`) exists. Those files are shared with mu, museum, muzzle and
your own hooks, so murmur touches only handlers whose command starts with
`murmur`: it adds the missing ones, sets Muse's `Interrupt` to `async`, and
removes its own stale ones (a duplicate, a `murmur` handler under a filtering
matcher or on an event it does not hook, the Cursor CLI's older
`murmur notify` entry). Everything else stays where it was. A file already
right is not rewritten, a symlinked file keeps its link, and a file that is
not valid JSON is reported and left alone. A command that runs murmur by
absolute path is yours, not murmur's: replace it with `murmur` on `PATH`, or
`init` adds a second one. `CLAUDE_CONFIG_DIR` and `CODEX_HOME`, when set, move
Claude Code's and codex's files (`$CLAUDE_CONFIG_DIR/settings.json`,
`$CODEX_HOME/hooks.json` and `config.toml`) for `init` and `doctor` as they do
for the harnesses, muzzle and museum. Muse's and the Cursor CLI's stay under
the home directory.

`murmur doctor` checks the same entries: drift (a missing event, a stray or
non-async handler, an unreadable file) is a problem, and a harness set up with
no murmur entries is an observation. Both name `murmur init` as the fix.

Once codex has these hooks, an older `notify` line in `~/.codex/config.toml`
that runs murmur reports every finished turn a second time. murmur does not
edit that file: `init` warns and `doctor` reports a problem until you remove
the line.

By hand, run `murmur hook claude` (or `murmur hook codex`) for each hooked
event. The payload arrives on stdin and names its own event, so one command
serves them all. Claude Code, in `~/.claude/settings.json`:

```json
{
  "hooks": {
    "SessionStart": [{ "hooks": [{ "type": "command", "command": "murmur hook claude" }] }],
    "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "murmur hook claude" }] }],
    "PermissionRequest": [{ "hooks": [{ "type": "command", "command": "murmur hook claude" }] }],
    "Notification": [{ "hooks": [{ "type": "command", "command": "murmur hook claude" }] }],
    "Stop": [{ "hooks": [{ "type": "command", "command": "murmur hook claude" }] }],
    "StopFailure": [{ "hooks": [{ "type": "command", "command": "murmur hook claude" }] }],
    "SessionEnd": [{ "hooks": [{ "type": "command", "command": "murmur hook claude" }] }]
  }
}
```

Codex takes the same shape in `~/.codex/hooks.json`, with `murmur hook codex`,
for `SessionStart`, `UserPromptSubmit`, `PermissionRequest`, `Stop`,
`Interrupt` and `SessionEnd`. Codex asks you to trust new or changed hooks
once, including the ones `murmur init` writes. Drop the `notify` line from `~/.codex/config.toml` when you add these,
or a finished turn is reported twice.

The Cursor CLI, in `~/.cursor/hooks.json` (or a project's `.cursor/hooks.json`),
with `murmur hook cursor-agent`:

```json
{
  "version": 1,
  "hooks": {
    "sessionStart": [{ "command": "murmur hook cursor-agent" }],
    "beforeSubmitPrompt": [{ "command": "murmur hook cursor-agent" }],
    "afterAgentResponse": [{ "command": "murmur hook cursor-agent" }],
    "stop": [{ "command": "murmur hook cursor-agent" }],
    "sessionEnd": [{ "command": "murmur hook cursor-agent" }]
  }
}
```

`afterAgentResponse` carries the turn's text and may land after `stop`; murmur
pairs the two by the turn's `generation_id`. An Escape reads `idle`, and the
`error` stop cursor-agent sends after it is dropped. The Cursor CLI has no
permission hook, so its pane never reads `blocked`. `murmur hook cursor-agent`
replies `{}` on stdout: the CLI treats empty stdout as a failed hook
(`empty_stdout`) and tracking never sticks. Replace the
`murmur notify --source cursor` entry below when you add these.

Muse Code, under the `hooks` key of `~/.config/muse/settings.json` (or a
project's `.muse/hooks.json`), in Claude Code's shape with
`murmur hook muse`, for `SessionStart`, `UserPromptSubmit`,
`PermissionRequest`, `Stop`, `StopFailure`, `Interrupt` and `SessionEnd`.
`Interrupt` must be declared asynchronous, and only this settings route
delivers it; a Claude-format plugin cannot subscribe to it:

```json
{
  "hooks": {
    "Interrupt": [
      { "hooks": [{ "type": "command", "command": "murmur hook muse", "async": true }] }
    ]
  }
}
```

Muse runs its hooks with a cleared environment (no `$TMUX`, none of mu's
variables), which murmur does not need: it places the event by walking the
hook's process chain to the tmux server, and names a mu worker from mu's
anchor file.

Do not hook `PreToolUse` or `PostToolUse`: they fire on every tool call and
carry no state murmur uses. The hook prints nothing and always exits 0, so a
murmur problem never blocks a turn. The agent must run in a tmux pane; a hook
whose process chain does not reach a tmux server writes nothing. Events wait in
a per-pane inbox under `~/.local/state/murmur2/inbox` (the home path whatever
`XDG_STATE_HOME` says, since Muse's hooks never see it) until a reader
(`murmur status`, the dash, `murmur pane`) applies them. An agent counts as
crew when mu started it (its anchor file), or when the hook sees
`MU_MANAGED_AGENT=1` or `MU_AGENT_NAME`.

A worker mu starts on a remote host for a pane on this one carries a remote
key (in its anchor, or `MU_REMOTE_KEY`). Its hooks write to that host's remote
area instead of its inbox, so it never appears in the host's own `murmur
status`; mu reads it with `murmur pane --remote <key> --json` on that host.

### pi on mu's `hooks` channel

With `MU_PI_CHANNEL=hooks` in its environment, murmur's pi extension stops
using the store: it writes inbox events like the hooks above and never claims
the pane or loads a native module. Set it only for a pi build that cannot load
murmur's store; mu then drives that pi like the harnesses above.

### `murmur notify`

Harnesses with no hook murmur maps run a command when something happens, and
`murmur notify` records an attention request for the pane it runs in.

This path is attention only: no ownership, no `running`/`idle`, no crash
detection. Unrecognised events become `blocked`, so a new harness still puts a
row in front of you.

A request counts as crew, not yours, when the hook's environment has
`MU_MANAGED_AGENT=1` or `MU_AGENT_NAME`, which mu sets on the agents it places.
The hook inherits them from the harness, so a wrapper that clears the
environment (`env -i`) makes a mu worker count as yours.

### Codex

```toml
# ~/.codex/config.toml
notify = ["murmur", "notify", "--source", "codex"]
```

Do not wrap with `sh -lc` alone. Codex appends the event JSON as one more
argument, and `sh -lc '<script>' <arg>` puts that argument in `$0` rather than
`$1` — the wrapper swallows the payload. Exec murmur directly, or forward
explicitly:

```toml
notify = ["/bin/sh", "-lc", "exec murmur notify --source codex \"$@\"", "codex-notify"]
```

Codex fires `agent-turn-complete` (turn ended, waiting for you) — murmur records
`done`. Drop `--title Codex` if you have it; the message falls back to the
payload's `last-assistant-message`.

The same fields may arrive as JSON on stdin (opencode's plugin form). argv wins
when both are present; flags still beat the payload, so `--event-type` can pin
an event murmur does not know.

### opencode

OpenCode 2 has no notify hook, so a plugin pipes JSON to `murmur notify`. The
[mu-crew dotfiles](https://github.com/mu-crew/dotfiles) ship one at
`opencode/plugins/murmur/tui.ts`. Install it as
`~/.config/opencode/plugins/murmur/tui.ts`. opencode loads only a directory
with a `tui` entry, and skips a bare file in `plugins/`.

It must be a TUI plugin. A server plugin runs in opencode's shared background
service, which keeps the environment of the pane that first started it. Every
notification from every opencode window would then land on that one pane. A
TUI plugin runs in the terminal, so `murmur notify` gets the right
`$TMUX_PANE`. Every TUI sees every session's events, so the plugin only
reports sessions it has shown, either as the open session or as a tab.

A finished turn arrives as `session.execution.succeeded`. The plugin sends it
as `session.idle`, which murmur records as `done`. `permission.asked` is not in
murmur's table, so it becomes `blocked`.

### Cursor CLI

The attention-only form; `murmur hook cursor-agent` above supersedes it.

```json
{
  "version": 1,
  "hooks": {
    "stop": [
      {
        "command": "murmur notify --source cursor"
      }
    ]
  }
}
```

`~/.cursor/hooks.json` (user) or `.cursor/hooks.json` (project). `status:
completed` → `done`; `error` → `error`; `aborted` → `blocked`. The agent must run inside
tmux. Interactive `agent` sessions are the target; non-interactive `agent -p`
has been observed to omit `stop`.

### PATH in hooks

A notify hook inherits the PATH of whatever launched the harness. `/bin/sh -l`
does not fix a missing npm prefix. Verify from inside the harness:

```bash
murmur notify --source probe --message reachable && murmur status
murmur clear --pane "$TMUX_PANE"
```

If `murmur` is missing there, use the absolute path from `command -v murmur`.

The probe is a real attention request: no event name → `blocked`, so the window
badges. `murmur clear` (or focus, with the hooks above) takes it back.

`notify` outside tmux, or against a store from a newer murmur, records nothing
and exits 0, so it cannot break the caller's exit code. A pane reached only via notify is still a full jumpable
row.

## Notifications

murmur runs `~/.config/murmur/on-attention` (or `$MURMUR_CONFIG_DIR/on-attention`)
once for each new `done`, `blocked`, `error` or `crashed`, on any node. Make it
executable; nothing else is configured. It runs detached, with its output
discarded, and these variables set:

| Variable | Value |
| --- | --- |
| `MURMUR_KIND` | `done`, `blocked`, `error` or `crashed` |
| `MURMUR_AGENT` | the agent name, as the picker shows it |
| `MURMUR_HOST` | the peer name, or this node's display name |
| `MURMUR_LOCAL` | `1` for this node, `0` for a peer |
| `MURMUR_PANE` | the pane id on its node |
| `MURMUR_MESSAGE` | the request text; for `error`, pi's error message |
| `MURMUR_HOST_COLOR` | the host's accent hex, as the dash colors it |
| `MURMUR_GLYPH` | the kind's Nerd Font glyph, as every murmur surface shows it |
| `MURMUR_KIND_COLOR` | the kind's hex color, as every murmur surface shows it |
| `MURMUR_EVENT` | all of the above and more, as JSON |

```sh
#!/bin/sh
# ~/.config/murmur/on-attention
case "$MURMUR_KIND" in
  done) urgency=low ;;
  *) urgency=critical ;;
esac
notify-send -u "$urgency" "murmur: $MURMUR_AGENT $MURMUR_KIND" \
  "$MURMUR_HOST${MURMUR_MESSAGE:+ — $MURMUR_MESSAGE}"
```

A host's color is a crc32 of its short name over eight Catppuccin accents, the
same hash the mu-crew dotfiles publish as `@mu_crew_host_color`, so one host is one
color in the status bar, the dash, the side panel, the picker and your
notifications. For a peer the name
is the one given to `peer add`.

The [mu-crew dotfiles](https://github.com/mu-crew/dotfiles#notifications)
ship a ready hook that turns these into notifications with an icon: a frame in
the host color around the kind's glyph.

On macOS, use `osascript -e "display notification \"$MURMUR_MESSAGE\" with title \"$MURMUR_AGENT $MURMUR_KIND\""`.
For a phone, `curl -d "$MURMUR_AGENT $MURMUR_KIND" ntfy.sh/<topic>`.

The hook fires from `murmur collect`, which `murmur status` runs on each
status-bar tick, as do the dash and side panel. So an event arrives within one
tick, and only on a node that runs one of those. Run the hook on the machine
you sit at; peers need no hook. Each event fires once even when several
surfaces collect at the same time. Events older than 15 minutes do not fire, so
installing the hook does not replay the backlog. A crew agent's `done` fires
too; filter on `MURMUR_EVENT`'s `driver` if you do not want it.

## Peers and doctor

On the machine you watch from:

```bash
murmur peer list         # configured peers, last seen
murmur peer list --all   # also ssh hosts that could become peers
murmur peer add devbox
murmur doctor            # survey each peer over ssh
murmur doctor --topology # also probe who can reach whom
```

`peer list` is local. `doctor` dials out, and also checks this node's own
hook entries ([above](#claude-code-codex-cursor-cli-and-muse-code-murmur-hook)). Membership is per node: peering a
machine does not mean it peers you — if it does not peer back, its list cannot
see your agents. `doctor` writes nothing and repairs nothing; it prints the
commands. Exit 0 for observations, 1 only for a real problem. Suggestions name
this node as it calls itself; that name may not resolve from the peer's side.
`peer add` accepts a target that does not answer yet and discovers identity on
the first collect.

`--topology` costs one dial per ordered pair. Plain `doctor` answers "is my
fleet mutual?"; `--topology` answers "what shapes are possible here?". A
recommended hub is arithmetic on that matrix. Spokes see the hub and the hub
sees every spoke; **spokes do not see each other** — `export` publishes local
panes only. A pair that was not demonstrably up reads `unknown`, not
unreachable.

Asleep or powered-off nodes are normal. `status`, `pick`, `dash`, and
`sidepanel` stay silent about that. `murmur peer list` has LAST SEEN; `murmur collect` (run on purpose)
prints one line per peer it could not reach.

Bind a popup picker:

```tmux
bind -N "agent state picker" a display-popup -E -w 80% -h 60% "murmur pick"
```

Rows use the shared picker prefix `marker · 30-column agent · state · context`, matching the `tms` session picker while retaining Murmur's state word, stream, host, age, and flags.

Typing in pick matches agent name, workstream or tmux session, host, and the
state word as literal substrings — so `blocked` narrows without a dedicated
binding.

## One key to the dash and back

After a jump, getting back depends on where the agent lives: a local agent is
an ordinary tmux move, a remote one hands the keyboard to the remote server.
Bind one key that covers both:

```tmux
bind -N "toggle murmur dash" G run-shell -b "murmur dash --goto"
```

Add it on every machine you jump to, not just the one you sit at — on a remote
host it is the remote tmux that reads the key.

`murmur dash --goto` decides in this order:

1. **In a murmur-controlled remote session** it detaches. The wrapper's own
   restore command then returns the local client to where the jump started.
2. **In the dash** it goes back to the pane you pressed the key in last. If that
   pane is gone it goes to its window. If the window is gone too, it stays in
   the dash and says `nothing to go back to`.
3. **With a dash running** it records where you are and switches to the dash.
   This includes an ordinary `ssh` login to a machine that has its own dash.
4. **With no dash running** it records where you are, opens `murmur dash` in a
   window of the dedicated `murmur-dash` session, and switches to it. On a node
   without `murmur init` it refuses instead, because the dash would exit at once.

The side panel is an ordinary pane, so the key works there and the split is
left alone: back returns you to the panel as you left it.

A dash marks its pane for as long as it runs, and murmur marks the one client
its own jump attaches. The way back is one option per client,
`@murmur_return_<client>`, so two terminals toggling at once do not trade
places. All three are tmux options, so nothing is inferred from process
ancestry or history. A dash killed with `SIGKILL` cannot clear its marker, so
`--goto` checks the pane is still alive before switching and opens a new dash
if it is not.

The `murmur-dash` session has `detach-on-destroy off`: quitting the dash with `q`
moves your client to another session instead of detaching the terminal.

`run-shell -b` backgrounds the call, so the key never blocks the server. The
cost is that a failure message goes nowhere; run `murmur dash --goto` by hand
to see it.

The client mark applies to the next client that attaches to the remote tmux
server. A jump command override that does not itself attach a tmux client, or
attaches after some other client does, leaves the jump unmarked — then `--goto`
switches to the remote host's own dash instead of coming home.

## Rex

[Rex](https://www.superlogical.com/rex/) shows terminal program status
([OSC 7501](https://www.superlogical.com/rex/docs/build/program-status)) on
its tabs and in its session picker, and pi already reports its own pane that
way. `murmur rex-feed` adds the rest of the fleet: run it in a tab of its own,
and that tab carries one record per agent the status pill counts, with the
most urgent state on its root record.

```sh
rex window new crew -- murmur rex-feed
```

- Which agents: human-driven agents that are not idle, and crew agents only
  when `blocked`, `error` or `crashed`, as in the pill.
- States map onto the protocol's five: `crashed` and `error` are `error`,
  `blocked` is `blocked` (kind `question`), `done` is `done`, `running` and
  `waiting` are `working`. Idle agents get no record.
- Each record's title is the agent's label, prefixed with its host for a
  remote agent; its message is the dash card's one-liner.
- It re-reads the store every 2 s and fetches peers on the collect floor,
  as `murmur status` does. Ctrl-C clears every record it set.

The records belong to the feed's pane, so Rex lists them there; selecting one
does not jump to the agent. Use `murmur pick` or the dash for that.

### pick and dash in a Rex pane

Both run in a Rex pane outside tmux. A jump opens the agent in a new tab of
the current Rex session, named after the agent (`linuxpc · dotfiles` for a
remote one): `tmux attach -t <pane>` for a local agent, the peer's jump
command for a remote one. The picker or dash stays where it was. Closing the
tab, or detaching, ends the attach.

That tab shows no program status of its own: the agent runs inside tmux, and
tmux drops OSC 7501 rather than pass it to the outer terminal (pi does not
wrap it in DCS passthrough). Keep `rex-feed` running for the fleet's state.
An agent started directly in a Rex pane, not in tmux, reports its own (pi
does), but murmur cannot see it.

The dash's glance and prompt input (`i`) work as in tmux: both address the
agent's pane, not a client. Still tmux-only: `murmur sidepanel` (it lays out
a tmux window) and `murmur dash --goto` (a tmux key binding). In a plain
terminal outside both, the picker attaches in place and the dash refuses to
start.

## Jump command override

`target` is always ssh (collector). The jump command is for a human and need not
be ssh:

```bash
murmur peer set dev --jump-command 'et dev -c "tmux attach -t {pane}"'
```

murmur substitutes `{pane}` and runs the rest unparsed. Default is
`ssh -t <target> env LC_CTYPE=C.UTF-8 tmux attach`. The explicit UTF-8 locale
keeps Nerd Font glyphs intact when the remote SSH login has no locale. Override
the command when sshd sets `MaxSessions 1` — see [SSH.md](../SSH.md).

Remote jump from inside tmux opens a local wrapper session named after the peer
with a trailing `~`:

- `status off` — the remote's bar is the only one on screen
- `prefix None` — `^b` goes straight to the remote

Both are per-session. Leaving the remote returns you to the window you jumped
from and destroys the wrapper. Jumping to the same host twice reuses one
session. Outside tmux, pick/dash run ssh directly and you land back at your
shell on exit.

Inside the wrapper the local tmux has no prefix. Escape hatch (root table —
not `M-b` / other Alt letters that would eat picker's own filters):

```tmux
bind -n M-Escape if-shell -F '#{m:*~,#{session_name}}' detach-client
```

## Snapshot versions

All nodes must speak the same snapshot format. A mismatch is rejected rather
than guessed — the peer shows as reachable-but-broken with the reason.
`murmur peer list` shows each peer's version. Patch versions that share a
snapshot major interoperate.
