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 1 week ago9 min readpage 36 / 37

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 in-terminal workspace switcher (Jump To, the session tree) reads /v1/workspaces from the host gateway, which is the daemon.

So if the picker is empty, moshi-hook status, the daemon log, and the multiplexers (daemon) section tell you nothing about it. 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 and herdr. 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. 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.

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 and Herdr 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.

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.
  • "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 Jump To or the workspace switcher is empty — that surface does use the daemon. Go to Debugging the gateway.