Get Moshi
deep dive

Debugging the session picker

The session picker is built from one command Moshi runs over SSH at connect time. When your sessions are missing, run that same command by hand — it fails in a handful of specific, fixable ways.

updated today12 min readpage 45 / 47

You have tmux sessions running on the host. tmux ls inside the terminal lists them. But when you tap a saved connection, Moshi drops you straight into a shell — or opens the picker with only Recent directories and no tmux tab.

This page walks through why, in the order worth checking.

The picker does not use the daemon

This is the most common wrong turn. Moshi has two different multiplexer surfaces, and they get their data from completely different places:

  • The connect-time session picker runs one shell command over your SSH connection, before any terminal exists. It never talks to moshi-hook.
  • The Sidebar (the in-terminal workspace tree) reads /v1/workspaces from the host gateway, which is the daemon.

Rex and Herdr Core are the exceptions: neither has a terminal client for Moshi to attach with, so the picker's script asks moshi-hook for them. See Rex and Herdr Core specifics.

So if a tmux, Zellij or Herdr tab is missing, moshi-hook status, the daemon log, and the multiplexer paths the daemon resolves tell you nothing about it. (moshi-hook doctor is the exception: it also reproduces the picker's view of tmux and Herdr on the host — see Two tmux installations.) A host with the daemon stopped entirely still shows a full session picker. Conversely, a working picker does not prove the gateway is healthy — for that, see Debugging the gateway.

Reproduce the probe by hand

At connect time Moshi runs a single POSIX shell script over SSH that checks each multiplexer and lists its sessions. This is the short version — enough to reproduce every failure below:

what the picker asks your host
$ssh myhost "sh -lc 'command -v tmux; tmux list-sessions'"
/usr/bin/tmux
main: 2 windows (created Wed Aug 5 06:13:22 2026) (attached)

Two details matter and are easy to miss:

  • It runs sh -lc, not your login shell. On most Linux distributions /bin/sh is dash, not bash.
  • Moshi prepends a fixed list of common install directories to PATH — ~/.local/bin, ~/bin, linuxbrew, nix profiles, /opt/homebrew/bin, /usr/local/bin, /opt/local/bin, /usr/bin, /bin — and only then appends whatever PATH that shell already had.
warn

That prepend is the part people leave out, and it changes the answer. Your own PATH comes last, so a tmux in /usr/bin wins over one in ~/miniforge3/bin — the opposite of what your shell does. A test without the prepend can pass while the picker fails.

So the faithful reproduction sets the same PATH the app does:

the probe, with the app's PATH order
$ssh myhost "sh -lc 'export PATH=\$HOME/.local/bin:\$HOME/bin:/usr/local/bin:/usr/bin:/bin:\$PATH; command -v tmux; tmux -V; tmux list-sessions'"
/usr/bin/tmux
tmux 3.2a

If that prints your sessions, detection is fine and you should skip to Timing and Channel limits. If it prints a different tmux than the one your sessions run under, go to Two tmux installations. If it prints nothing at all, keep reading.

PATH: the probe shell is not your shell

warn

Testing with ssh myhost 'command -v tmux' is not the same test. That runs your login shell — and bash sources ~/.bashrc for non-interactive SSH commands. The picker's sh -lc does not.

A login sh reads /etc/profile and ~/.profile. It does not read ~/.bashrc, and on Ubuntu the stock ~/.profile only sources ~/.bashrc when the shell is bash. So any tool installed somewhere that only ~/.bashrc puts on PATH is invisible to the probe.

This bites hardest with per-user package managers that write their init block into ~/.bashrc:

  • conda, miniforge, mamba (~/miniforge3/bin, ~/anaconda3/bin)
  • nvm-managed Node prefixes
  • asdf, mise, pyenv shims
  • a manually built tmux in a custom prefix

Compare the two shells to confirm:

is it a PATH problem?
$ssh myhost 'command -v tmux'
/home/you/miniforge3/bin/tmux
$ssh myhost "sh -lc 'command -v tmux'"
# no output → the picker cannot see your tmux either

The fix is to put the binary somewhere the probe already looks. Either install the distro package so it lands in /usr/bin, or symlink it into ~/.local/bin, which is first on the probe's PATH:

make tmux visible to the probe
$mkdir -p ~/.local/bin
$ln -sf "$(command -v tmux)" ~/.local/bin/tmux
$ssh myhost "sh -lc 'command -v tmux'"
# should now print /home/you/.local/bin/tmux

The same applies to zellij, herdr, and, for Rex, moshi-hook. Reconnect after the change — the probe runs once per connect.

Two tmux installations

A second copy of tmux is easy to end up with — Homebrew alongside the distro package, or conda's tmux alongside apt's. Because the probe searches its own prepended PATH before yours, it resolves a different binary than your interactive shell whenever the second copy sits in one of those directories. A conda tmux is the common case: it lives at the end of your PATH, so /usr/bin/tmux beats it for the probe and loses to it for you.

That by itself would be harmless. The problem is that tmux refuses to talk to a server started by a different version, so the probe asks the wrong tmux and gets an error rather than your sessions:

two tmux, two answers
$ssh myhost 'tmux -V'
tmux 3.4
$ssh myhost "sh -lc 'tmux -V; tmux list-sessions'"
tmux 3.2a
protocol version mismatch (client 8, server 7)

The picker treats that error the same as an empty list, so the tmux tab simply does not appear.

moshi-hook doctor (moshi-hook 0.4.3 or newer) checks for this on the host: it mirrors the picker's PATH order and flags when the tmux or Herdr the app — or the daemon — would run differs in version from the one running your server, then prints the symlink or removal fix. To check by hand, list every copy on the host:

find duplicate installs
$ssh myhost "sh -lc 'command -v -a tmux'"
/usr/bin/tmux
/opt/homebrew/bin/tmux

Two or more lines means this is your problem. Either remove the copy you do not want, or point the probe at the one you actually use:

pin the probe to your tmux
$mkdir -p ~/.local/bin
$ln -sf /opt/homebrew/bin/tmux ~/.local/bin/tmux
warn

Sessions already running under the other binary stay unreachable until that server exits — the version mismatch cuts both ways. Restart those sessions, or make this change before starting the ones you care about.

Timing

The picker probe has a time budget. If it expires, Moshi continues without multiplexer sessions and shows a Session check timed out warning toast.

The budget adapts per saved connection: a first-time connection gets a cold ceiling of about four seconds, a host with history gets roughly two and a half times its recent average, and repeated timeouts back off further, up to ten seconds. So a host that times out once usually recovers by itself on the next attempt.

Persistent timeouts almost always mean a slow login shell rather than a slow network. Measure it without the SSH handshake, which the app does not pay — it reuses one authenticated connection:

isolate the login shell cost
$ssh -o ControlMaster=auto -o ControlPath=/tmp/moshi-cm -o ControlPersist=60 myhost true
$time ssh -o ControlPath=/tmp/moshi-cm myhost "sh -lc 'command -v tmux >/dev/null'"
# under ~0.5s is healthy; multiple seconds means your profile is doing heavy work

If that is slow, something in /etc/profile, /etc/profile.d/*.sh, or ~/.profile is expensive — a conda hook that spawns Python, a version-manager init, an NFS-backed home directory, or a corporate login script. Moving heavy work behind an interactive-shell guard speeds up the picker and every other remote tool.

Channel limits

Moshi opens one SSH connection and runs its connect-time probes over it concurrently — the multiplexer snapshot, the recent-directories lookup, and the moshi-hook status check. If your host limits how many sessions a single connection may open, the extra channels are refused, and a refused probe looks exactly like "no multiplexer installed".

A one-command-at-a-time test never reproduces this. Test the concurrent case explicitly:

three probes over one connection
$ssh -o ControlMaster=auto -o ControlPath=/tmp/moshi-cm -o ControlPersist=60 myhost true
$for i in 1 2 3; do ssh -o ControlPath=/tmp/moshi-cm myhost "sh -lc 'command -v tmux'" & done; wait
# expect three paths; fewer means channels are being refused

Then check the server-side cap:

sshd session limits
$ssh myhost 'sshd -T 2>/dev/null | grep -iE "maxsessions|maxstartups"'
maxsessions 10

maxsessions 10 is the default and is fine. A hardened MaxSessions 1 is not — raise it in /etc/sshd_config, or in /etc/ssh/sshd_config.d/, and reload sshd. If you have a host where the picker works and one where it does not, comparing this value between them is often the fastest way to settle it.

Native Windows hosts

On a native Windows host (the experimental moshi-hook preview, not WSL), Moshi does not run the sh -lc script above. It runs a PowerShell probe that resolves herdr.exe and lists its sessions. Herdr is the only multiplexer the picker checks there.

warn

An sh -lc '...' test can succeed on Windows — Git for Windows puts an sh on PATH — while the picker still fails. It is not the probe the app runs, so it proves nothing about the picker.

Reproduce the actual probe from another machine, over the same sshd:

the Windows picker probe
$ssh you@winhost '$h = Get-Command herdr.exe | Select-Object -First 1 -ExpandProperty Source; $h; & $h session list --json; "exit=$LASTEXITCODE"'
C:\Users\you\.local\bin\herdr.exe
# JSON session list here
exit=0

A path, JSON, and exit=0 means detection is fine — check for a Session check timed out toast and see Timing. Anything else tells you where it falls over:

  • '$h' is not recognized as an internal or external command, or other cmd.exe-style errors — OpenSSH for Windows is running remote commands through cmd.exe, so the PowerShell probe cannot even parse. This is the most common cause. See below.
  • No path printed — herdr.exe is not on the PATH that sshd sessions get. Add its directory to the system or user PATH and restart the sshd service so new sessions pick it up.
  • A path, then an error instead of JSON — Herdr itself failed to run from the SSH session; the error text is what to chase.

Set OpenSSH's default shell to PowerShell

OpenSSH for Windows uses cmd.exe for remote commands unless HKLM\SOFTWARE\OpenSSH\DefaultShell is set. Interactive SSH can still look fine — you type powershell or your profile launches it — so this goes unnoticed until the picker silently falls back to a plain shell.

From an elevated PowerShell on the Windows host:

make PowerShell the sshd default shell
$New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -PropertyType String -Force
$Restart-Service sshd

Rerun the probe above to confirm, then reconnect from Moshi. The Herdr picker and the Chat View opened from Herdr notifications both depend on this.

Sessions are listed but greyed out

That is entitlement, not detection. Selecting a multiplexer session is a Pro feature with a limited free trial; when the trial is used up the picker still shows every session, with a lock banner reading Multiplexer sessions are Pro features. See Subscription.

Detection failures look different: the tab for that multiplexer is missing entirely, or the picker never opens.

The picker never opens at all

Moshi only shows the picker when there is something to pick. With no multiplexer sessions and no recent directories, it connects straight to a plain shell — the same behavior as a host with nothing running on it.

So an empty picker and a skipped picker are the same underlying result: the probe came back with nothing. Two settings also shape what appears, under Settings -> Shell -> Recent directories:

Detect on connect
Scans Claude/Codex history on the host for one-tap directory entries. Off means no Recent directories tab.
Open in tmux
Picking a recent directory creates a tmux session there instead of a plain shell.

Recent directories come from moshi-hook cwd-list --json when the daemon is installed, with a POSIX shell fallback when it is not — so that tab, unlike the multiplexer tabs, is affected by the hook.

Zellij, Herdr and Rex specifics

  • Zellij is listed through zellij list-sessions, tried in three flag variants for version compatibility. If zellij ls works interactively but the picker is empty, run the probe form: ssh myhost "sh -lc 'zellij list-sessions --short'".
  • Herdr sessions come from herdr session list --json, and only sessions whose server is actually running are shown — a stopped default session is deliberately hidden, matching tmux semantics. When exactly one Herdr session exists, the picker collapses it and lists that session's workspaces directly.
  • Rex sessions come from moshi-hook rex ls --json, which the probe runs only when it finds moshi-hook on its PATH. A missing Rex tab has three likely causes: moshi-hook isn't on the probe's PATH, it's older than 0.4.38 and doesn't list Rex, or no Rex server is running for the SSH user, which shows as "running": false. Reproduce it with ssh myhost "sh -lc 'moshi-hook rex ls --json'". Rex isn't checked on native Windows hosts.
  • Herdr Core (hc, used by the Herdr desktop app) is listed by moshi-hook too, never by herdr session list. Without moshi-hook 0.4.45 or newer on the probe's PATH, the picker shows only Recent and the card says No multiplexer. Reproduce it with ssh myhost "sh -lc 'moshi-hook mux ls --json'" and check the hc section.

Common failures

  • tmux works by hand, nothing in the picker — compare ssh host 'command -v tmux' against ssh host "sh -lc 'command -v tmux'". Different results mean PATH; see above.
  • Both shells find tmux, still nothing in the picker — check for a second install with command -v -a tmux. Two copies means a protocol version mismatch against the running server; see Two tmux installations.
  • Works on one host, not another, same app and account — compare sshd -T | grep maxsessions and the login-shell timing between the two hosts.
  • Native Windows host always opens a plain PowerShell shell — OpenSSH is running remote commands through cmd.exe. Set DefaultShell to PowerShell; see Native Windows hosts.
  • "Session check timed out" toast — slow login shell. Reconnect once (the budget adapts upward), then fix the profile.
  • Sessions appear, then vanish on a later connect — the tmux server exited. Confirm with tmux ls on the host; sessions do not survive a host reboot unless something recreates them.
  • Sessions listed but not selectable — Pro trial exhausted, not a detection problem.
  • Picker fine, but the Sidebar is empty — that surface does use the daemon. Go to Debugging the gateway.