Summary
dmux installs a session hook client-resized → run-shell "kill -USR1 <dmux pid>", but its only SIGUSR1 listener is registered inside a React effect that unregisters itself whenever a dialog opens or closes. In that window the process has no SIGUSR1 handler, so the default action — terminate — applies, and a terminal resize kills the dmux control pane.
I hit this on three separate dmux sessions on one machine within a few hours.
Symptoms
It presents as three seemingly unrelated problems, which is what made it hard to place:
- The control pane silently becomes a bare shell. The death line is easy to miss because it scrolls away:
zsh: user-defined signal 1 node "/opt/homebrew/lib/node_modules/dmux/dist/index.js"
- That pane then floods with garbage. The dying process never got to disable mouse reporting, so tmux still reports
mouse_any_flag=YES for a pane now running a plain shell and keeps forwarding SGR mouse sequences into it:
zsh: command not found: 73M64
zsh: command not found: 17
Typing is impossible; mouse movement inserts characters mid-command.
- The hooks outlive the process.
tmux show-hooks -t <session> still points kill -USR1 at a dead PID. Since PIDs wrap, an unrelated process that later lands on that PID gets SIGUSR1 on every resize. after-select-pane / after-split-window have the same issue with SIGUSR2.
Cause
At HEAD:
src/hooks/useLayoutManagement.ts:98 — process.on('SIGUSR1', handleResize) inside useEffect
src/hooks/useLayoutManagement.ts:104 — process.off('SIGUSR1', handleResize) in the cleanup
src/hooks/useLayoutManagement.ts:109 — deps [controlPaneId, hasActiveDialog], so the effect re-runs on every dialog toggle
Meanwhile src/index.ts registers SIGINT (:1455), SIGTERM (:1458) and SIGUSR2 (:1464) at process level, permanently. SIGUSR1 is the only one that isn't — which looks like an oversight rather than a design choice, since the tmux hook that sends it is installed by dmux itself in setupResizeHook().
Reproduction
Standalone, no tmux and no dmux needed. It models the exact sequence: effect mounts, effect cleans up, hook fires.
// node repro.mjs unpatched -> exit 158 (128 + 30; SIGUSR1 is signal 30 on darwin)
// node repro.mjs patched -> SURVIVED
if (process.argv[2] === 'patched') process.on('SIGUSR1', () => {})
const effectHandler = () => {}
process.on('SIGUSR1', effectHandler) // effect mounts
process.off('SIGUSR1', effectHandler) // dialog toggled -> cleanup runs
process.kill(process.pid, 'SIGUSR1') // client-resized hook fires
setTimeout(() => { console.log('SURVIVED'); process.exit(0) }, 300)
Suggested fix
Register a permanent SIGUSR1 listener next to the existing SIGUSR2 one in src/index.ts:
process.on('SIGUSR1', () => {
LogService.getInstance().debug('Received SIGUSR1 from tmux client-resized hook', 'ResizeDebug');
});
Node invokes every registered listener, and the effect's process.off('SIGUSR1', handleResize) removes only its own reference — so resize behaviour is unchanged and useLayoutManagement.ts needs no edit. The permanent listener only closes the window in which the default terminate action applies.
I've been running this patch locally and the control pane has been stable since. Happy to open a PR.
Two things possibly worth handling separately:
- Hook cleanup on exit. Removing the session hooks when dmux shuts down would stop dangling
kill -USR1/-USR2 commands from targeting recycled PIDs.
- Mouse reporting reset.
cleanTerminalExit handles the graceful paths, but a signal death leaves the pane in mouse-reporting mode. A trap-style reset, or having the hooks target a PID file that is cleaned up, would avoid the unusable-shell symptom.
Environment
|
|
| dmux |
5.10.0 (npm global) |
| node |
v26.5.0 |
| tmux |
3.7b |
| OS |
macOS 15.7.7 (24G720), arm64 |
Ruled out while diagnosing: memory pressure (no jetsam kills in the kernel log), auto-update races (package mtime unchanged), and crashes (SIGUSR1 leaves no crash report — it isn't a crash, which is why nothing showed up in ~/Library/Logs/DiagnosticReports).
Summary
dmux installs a session hook
client-resized → run-shell "kill -USR1 <dmux pid>", but its onlySIGUSR1listener is registered inside a React effect that unregisters itself whenever a dialog opens or closes. In that window the process has noSIGUSR1handler, so the default action — terminate — applies, and a terminal resize kills the dmux control pane.I hit this on three separate dmux sessions on one machine within a few hours.
Symptoms
It presents as three seemingly unrelated problems, which is what made it hard to place:
mouse_any_flag=YESfor a pane now running a plain shell and keeps forwarding SGR mouse sequences into it:tmux show-hooks -t <session>still pointskill -USR1at a dead PID. Since PIDs wrap, an unrelated process that later lands on that PID getsSIGUSR1on every resize.after-select-pane/after-split-windowhave the same issue withSIGUSR2.Cause
At HEAD:
src/hooks/useLayoutManagement.ts:98—process.on('SIGUSR1', handleResize)insideuseEffectsrc/hooks/useLayoutManagement.ts:104—process.off('SIGUSR1', handleResize)in the cleanupsrc/hooks/useLayoutManagement.ts:109— deps[controlPaneId, hasActiveDialog], so the effect re-runs on every dialog toggleMeanwhile
src/index.tsregistersSIGINT(:1455),SIGTERM(:1458) andSIGUSR2(:1464) at process level, permanently.SIGUSR1is the only one that isn't — which looks like an oversight rather than a design choice, since the tmux hook that sends it is installed by dmux itself insetupResizeHook().Reproduction
Standalone, no tmux and no dmux needed. It models the exact sequence: effect mounts, effect cleans up, hook fires.
Suggested fix
Register a permanent
SIGUSR1listener next to the existingSIGUSR2one insrc/index.ts:Node invokes every registered listener, and the effect's
process.off('SIGUSR1', handleResize)removes only its own reference — so resize behaviour is unchanged anduseLayoutManagement.tsneeds no edit. The permanent listener only closes the window in which the default terminate action applies.I've been running this patch locally and the control pane has been stable since. Happy to open a PR.
Two things possibly worth handling separately:
kill -USR1/-USR2commands from targeting recycled PIDs.cleanTerminalExithandles the graceful paths, but a signal death leaves the pane in mouse-reporting mode. Atrap-style reset, or having the hooks target a PID file that is cleaned up, would avoid the unusable-shell symptom.Environment
Ruled out while diagnosing: memory pressure (no jetsam kills in the kernel log), auto-update races (package mtime unchanged), and crashes (
SIGUSR1leaves no crash report — it isn't a crash, which is why nothing showed up in~/Library/Logs/DiagnosticReports).