Diagnostics
August 29, 2026 ยท View on GitHub
psmux runs its server detached, with no visible stderr. When something goes wrong there is nowhere for an error message to land on screen, so psmux writes what it knows to files instead. This page lists every diagnostic file psmux can produce, the environment variables that turn the optional ones on, and what to attach when you file a bug report.
Everything on this page is read only observation, with one exception called out where it appears:
PSMUX_FAKE_WIN_BUILD moves psmux onto a different branch on purpose, so that a platform specific
path can be reproduced on a machine that is not that platform.
Debug Logging
psmux has fourteen independent debug loggers. All of them are off by default and cost nothing
when disabled (one atomic load per call site). Each writes a timestamped line per event, and the
loggers under ~/.psmux/ are capped at a fixed number of entries so an enabled log can never fill
your disk.
| Variable | Log file | What it records |
|---|---|---|
PSMUX_CLIENT_DEBUG=1 | ~/.psmux/client_debug.log | Client frame receive, JSON parse, draw lifecycle, status bar rendering |
PSMUX_STYLE_DEBUG=1 | ~/.psmux/style_debug.log | Style and theme parsing, inline #[...] directives |
PSMUX_INPUT_DEBUG=1 | ~/.psmux/input_debug.log | Every input event plus the console mode in effect when it arrived |
PSMUX_SERVER_DEBUG=1 | ~/.psmux/server_debug.log | Server side request tracing and session switching |
PSMUX_SESSION_DEBUG=1 | ~/.psmux/session_debug.log | Session registry scans and stale port cleanup |
PSMUX_MOUSE_DEBUG=1 | ~/.psmux/mouse_debug.log | Mouse injection, screen to pane coordinate mapping, and the wheel gate's decision per notch (which mouse protocol the pane holds and who enabled it) |
PSMUX_SSH_DEBUG=1 | ~/.psmux/ssh_input.log | SSH escape sequence decoding into Win32 input records |
PSMUX_LATENCY_LOG=1 | ~/.psmux/latency.log | Keypress to render latency, measured client side |
PSMUX_PANE_RAW=1 | ~/.psmux/pane_raw.bin | The raw byte stream a pane's child wrote, before psmux's VT parser touched it. The file to read when a colour, a mode switch or a mouse DECSET seems to be missing: it shows what the program (and conhost, which echoes console mode changes into the same stream as ESC [ ? 1003 ; 1006 h / l) actually emitted |
PSMUX_AUTORENAME_DEBUG=1 | ~/.psmux/autorename.log | The process tree walk behind automatic-rename and #{pane_current_command} |
PSMUX_ROUTE_DEBUG=1 | stderr of the CLI process | Which session a bare psmux <command> (no -t, no $TMUX) was routed to and why |
PSMUX_POPUP_DEBUG=1 | %TEMP%\psmux_popup_debug.log | Popup creation, resize, and teardown |
PSMUX_WARM_DEBUG=1 | %TEMP%\psmux_warm_debug.log | Warm server and warm pane lifecycle |
PSMUX_AUTH_DEBUG=1 | %TEMP%\psmux_auth_debug.log | The .port and .key handshake between client and server |
Three of these deliberately live in %TEMP% rather than ~/.psmux/. Popup, warm pool and auth
diagnostics are all about races between concurrent psmux processes, so those three logs are
append-only and per-machine rather than truncate-on-start, and no process can clobber another's
evidence. PSMUX_AUTH_DEBUG additionally echoes the client side of the handshake to stderr, which
is visible when you run the client in a normal console.
Two more details worth knowing:
PSMUX_SESSION_DEBUGappends rather than truncates. Registry cleanup runs in every short-livedpsmuxCLI process, so truncating on open would erase the log before you could read it.PSMUX_LATENCY_LOG=1also enables a server side companion at%USERPROFILE%\psmux_server_latency.logholding dump-state build times. Read the two together to tell a slow server from a slow client.
Enabling a Log
Set the variable in the shell you launch psmux from, then reproduce the problem:
$env:PSMUX_INPUT_DEBUG = "1" # turn the input logger on for this shell only
psmux kill-server # make sure the next launch is a fresh server
psmux new-session -s repro # reproduce the problem here
Get-Content "$env:USERPROFILE\.psmux\input_debug.log" -Tail 40
The variable must be set before the process you want to trace starts. A logger that traces the
server (PSMUX_SERVER_DEBUG, PSMUX_SESSION_DEBUG, PSMUX_WARM_DEBUG) only takes effect on a
server started after the variable was set, which is why the example kills the old server first.
Warm servers are pre-spawned, so add $env:PSMUX_NO_WARM = "1" when you need the very first server
process to carry your setting. See warm-sessions.md.
To turn a logger back off, clear the variable and restart:
Remove-Item Env:\PSMUX_INPUT_DEBUG
psmux kill-server
The Windows Build Seam
| Variable | What it does |
|---|---|
PSMUX_FAKE_WIN_BUILD=19045 | Makes psmux report that build number instead of the real one |
Several mouse code paths branch on the Windows build number, because conhost handles VT mouse
input differently below build 22523 (see mouse-ssh.md and
faq.md). Those branches are unreachable on a modern host, so this variable exists to
exercise them: set it to 19045 to see what a Windows 10 pane does, or to 26100 to see the
modern path from an older machine.
This is diagnostic only. It changes nothing about the machine and nothing about the console, it
only changes which branch psmux picks, so a build set here that does not match reality gives you
a mouse path your conhost was not built for. Set it while reproducing a report, then clear it. It
must be set on the server process to affect a pane, so kill the server first and add
$env:PSMUX_NO_WARM = "1" so that a warm server spawned earlier, without your setting, does not
answer instead.
Always On Diagnostic Files
These three are written without any variable being set. They exist because the failures they describe happen where you cannot see them.
~/.psmux/server-startup.log
Written when the server fails during startup, typically when the first pane cannot be spawned.
The classic case is a default-shell pointing at a path that does not exist or cannot be executed,
where all the user sees is psmux flashing and returning to the prompt. The log records:
- the real error text, including the raw Windows error (for example
CreateProcessW err 87), - the exact path psmux tried to spawn,
- the psmux build that produced the failure,
- the size of the inherited environment block and its largest single variable, because an
oversized environment is itself a common cause of
err 87on profiles where OneDrive and WindowsApps entries have inflated it toward the 32 KB Windows limit.
The attaching client reads this file back and echoes a fresh failure to your terminal rather than leaving it buried, so in most cases you will see the real cause on screen. The file is the fuller record. Only a log written during the current startup attempt is surfaced. An older one is treated as stale and ignored.
~/.psmux/config-warnings.log
Non-fatal config parse problems, such as an unknown option name, a malformed value, or an unknown command in your config file. The detached server has no stderr to complain to, so it records the warnings here and the next client to attach echoes them to your terminal. As with the startup log, only warnings from the current startup are shown, and the file is removed once a config parses cleanly, so a fixed config stops reporting.
This is the file to check first when a config line appears to have been silently ignored.
~/.psmux/crash.log
Written by the server's panic handler: the panic message and a full backtrace. The handler also
removes that session's .port and .key files, so a crashed server does not leave stale discovery
files behind for the next client to trip over.
If this file exists and its timestamp matches your problem, attach it. It is the single most useful artifact in a bug report.
Runtime State and Bookkeeping
psmux keeps all of its runtime bookkeeping in one directory, %USERPROFILE%\.psmux\. The home
directory is resolved from USERPROFILE first, then the Win32 profile API, then
HOMEDRIVE plus HOMEPATH, then HOME. Setting PSMUX_DATA_DIR to an absolute path moves the
whole directory, which gives you a second, fully independent registry (its own sessions, its own
warm server, its own last_session) without touching the default one. See
configuration.md.
Per-session files are named after the session:
| File | Purpose |
|---|---|
<session>.port | The TCP port that session's server is listening on |
<session>.key | The auth key a client must present to that server |
<session>.sid | The session's stable id, the $N used in targets |
<session>.pid | Liveness anchor, stored as pid:creation_filetime |
<session>.act | Last activity stamp, Unix epoch microseconds. Written on client attach and on real keystrokes from an attached client (throttled to one write per second), the same activity_time tmux keeps. It is what a bare psmux <command> with no -t uses to pick the session you are actually working in (#603). A detached session that was never attached has none and ranks by its .port file's age |
<session>.spawnlock | Warm pool spawn lock, prevents two racing spawns |
The .pid file is worth understanding, because it is what makes psmux safe to clean up after
itself. A bare pid can be recycled by Windows and reused by an unrelated process. Storing the
process creation time alongside it means psmux can prove a pid is still the same process it wrote
down before it acts on it, so the orphan server reaper and kill-server can never terminate
something that merely inherited the number.
The warm server uses the reserved session name __warm__, so it owns a full set of the files above
named __warm__.port, __warm__.key and so on. That is why psmux ls does not show it.
Socket namespaces created with -L <name> prefix the session name, so the files become
<namespace>__<session>.port and so on. A namespaced warm server is therefore
<namespace>____warm__.port, with the doubled underscore coming from the namespace separator plus
the warm name.
Three files have fixed names and are not tied to any session:
| File | Purpose |
|---|---|
latency.log | Client latency trace, only written under PSMUX_LATENCY_LOG=1 |
last_session | The session name written on the most recent attach. A bare psmux invocation no longer trusts it outright: routing ranks sessions by their .act stamp first, and this file only breaks a tie between sessions with identical activity, which in practice means a registry written by an older psmux that nobody has attached to since |
next_session_id | Counter that hands out the next stable session id |
A stale .port (a server that is gone but whose registry files survived) is reaped by the next
client that trips over it. psmux attach -t name against one prints tmux's
can't find session: name and exits 1, and a bare psmux attach that finds nothing left prints
no sessions. Neither surfaces the underlying winsock error any more
(#605). If you see a raw
No connection could be made because the target machine actively refused it you are on an older
build.
The same directory also holds plugins\ (see plugins.md) and, in control mode,
cc_debug.log (see iterm2-control-mode.md).
Inspecting the Directory
Get-ChildItem "$env:USERPROFILE\.psmux" | Sort-Object LastWriteTime -Descending
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 7/27/2026 9:14 AM plugins
-a--- 7/27/2026 9:14 AM 21 work.port
-a--- 7/27/2026 9:14 AM 44 work.key
-a--- 7/27/2026 9:14 AM 26 work.pid
-a--- 7/27/2026 9:12 AM 21 __warm__.port
-a--- 7/27/2026 9:12 AM 4 last_session
Deleting a stale .port or .key file for a session whose server is gone is safe. psmux also
cleans these up on its own, both at startup and when a server exits or panics. Never delete files
belonging to a session you still have attached.
What to Attach to a Bug Report
Collect these before opening an issue at GitHub Issues. The first four are almost always enough.
- Version and build.
psmux -Vprints the version, the commit hash, and the build date. - Your config, or the smallest config that still reproduces it. If you are unsure whether your
config is involved, test with an empty one:
psmux -f NUL new-session. - The always on logs, if they exist and are recent:
~/.psmux/crash.log,~/.psmux/server-startup.log,~/.psmux/config-warnings.log. - Exact reproduction steps, including which shell you launched psmux from and which terminal emulator you are running in. Windows Terminal, conhost and ConEmu behave differently.
- A relevant debug log, if you can guess the subsystem. Keys or modifiers not arriving,
use
PSMUX_INPUT_DEBUG. Mouse misbehaving, usePSMUX_MOUSE_DEBUG. Anything over SSH, usePSMUX_SSH_DEBUG. Wrong colors or a broken theme, usePSMUX_STYLE_DEBUG. A client that will not attach, usePSMUX_AUTH_DEBUG. A program inside a pane that seems to lose a colour, a screen mode or its mouse registration, usePSMUX_PANE_RAWand look at what the program really wrote. A bare command landing in the wrong session, usePSMUX_ROUTE_DEBUG. - A directory listing of
%USERPROFILE%\.psmux\when the problem is about sessions not being found, servers not starting, or stale sessions appearing inpsmux ls. - Your Windows version.
[System.Environment]::OSVersion.Versionprints it. Several subsystems, notably mouse over SSH, are gated on the Windows build number. See mouse-ssh.md.
A one-liner that gathers the common set:
psmux -V
Get-ChildItem "$env:USERPROFILE\.psmux" -Filter "*.log" | ForEach-Object {
"===== $($_.Name) ====="
Get-Content $_.FullName -Tail 50
}
Debug logs can contain the text you typed and the output of your panes. Read them before you paste them into a public issue.
Note: psmux also reads a small number of
PSMUX_TEST_*variables and one build-number override. These are internal test seams used by the test suite to force timing races and are not supported knobs. Do not set them.
See Also
- configuration.md for the supported options and user facing environment variables
- warm-sessions.md for the
__warm__server and how to disable it - performance.md for what the latency numbers should look like
- faq.md for the common questions these logs usually answer