Skip to content

fix(config): keep wt config show from suspending on the zsh compinit probe#3327

Merged
max-sixty merged 3 commits into
mainfrom
fix/issue-3322
Jul 9, 2026
Merged

fix(config): keep wt config show from suspending on the zsh compinit probe#3327
max-sixty merged 3 commits into
mainfrom
fix/issue-3322

Conversation

@worktrunk-bot

Copy link
Copy Markdown
Collaborator

Problem

After upgrading 0.56.0 → 0.63.0, wt config show suspends instead of running:

$ wt config show
[1]  + 3071417 suspended (tty output)

suspended (tty output) is SIGTTOU: a process in a background process group touched the controlling terminal. The reporter's wt -vv config show shows the suspend landing right after the log-path header and before any config content — i.e. the pager (less) is being suspended on its first terminal operation.

Root cause

wt config show runs an interactive zsh probe to detect whether compinit is configured (check_zsh_compinit_missing, mirrored by shell::detect_zsh_compinit). An interactive zsh -ic with a controlling terminal enables job control: it setpgids itself into a new process group and tcsetpgrps to claim the terminal foreground.

#3165 (shipped in 0.62.0, inside the reporter's upgrade window) added a 2s kill-on-timeout to that probe. When the user's interactive rc is slow to start or blocks on a prompt (e.g. compinit's insecure-directories prompt), the probe is SIGKILLed before it restores the foreground process group. SIGKILL can't be caught, so zsh's terminal-restore-on-exit never runs — the terminal foreground is left pointing at the dead probe's group, and wt is now a background process group. The pager wt config show spawns moments later then raises SIGTTOU on its first tcsetattr, suspending the command.

This is environment-specific: it only fires when the probe actually times out (slow/prompting interactive rc), which is why it doesn't reproduce for everyone.

Solution

Pass +m (disable job control) to both interactive probes. With job control off, the probe never grabs wt's controlling terminal, so a timeout-kill can't strand wt in a background process group. +m doesn't affect compinit/compdef detection — it only disables terminal foreground management the probe never needed.

Testing

Verified at the OS level under a PTY, replicating the probe's exact spawn (interactive zsh -ic, stdin/stdout/stderr redirected, slow rc, SIGKILL on timeout):

  • Unpatched: tcgetpgrp of the controlling terminal moves to the probe's process group and stays there after the kill (foreground stolen) — the next terminal op suspends with SIGTTOU.
  • With +m: the terminal foreground is intact after the kill, and wt config show reaches the pager normally.

Added regression guards (test_compinit_probe_disables_job_control, test_zsh_probe_disables_job_control) asserting both probes pass +m before -ic. Full terminal-suspend behavior depends on a real controlling TTY + a slow zsh rc, so it isn't unit-testable in CI without flakiness; the guards lock in the load-bearing flag.


Closes #3322 — automated triage

…t probe

The interactive `zsh -ic` compinit probe enables job control, puts itself in
a new process group, and `tcsetpgrp`s to claim the terminal foreground. The 2s
kill-on-timeout added in #3165 SIGKILLs the probe before it can restore the
foreground group, so `wt` is left in a *background* process group. The pager
`wt config show` spawns moments later then raises SIGTTOU on its first terminal
operation, suspending the command (`suspended (tty output)`, #3322).

Pass `+m` to disable job control in both probes (`check_zsh_compinit_missing`
and `detect_zsh_compinit`), so the interactive shell never grabs wt's
controlling terminal and a timeout-kill can't strand wt in the background.

Verified at the OS level under a PTY: the unpatched probe steals the terminal
foreground (`tcgetpgrp` moves to the dead probe's group and stays); with `+m`
the foreground is intact and the pager runs normally.

Closes #3322

Co-Authored-By: Claude <noreply@anthropic.com>
@max-sixty

Copy link
Copy Markdown
Owner

I see we're repeating things a bit — can we unify? if not easy, put a TODO and we'll come back to it

Separately: how could we have caught this initially? A PTY test?

Remove an accidentally-committed `-l` file (captured `wt config show`
output) and cross-link the two duplicated interactive zsh compinit probes
(`check_zsh_compinit_missing` / `shell::detect_zsh_compinit`) with a shared
TODO. Both had to receive the #3322 `+m` job-control fix; they diverge on
purpose (`--no-globalrcs`, result type, runner), so unification is deferred
behind one parameterized helper rather than done mechanically here.

Co-Authored-By: Claude <noreply@anthropic.com>
@worktrunk-bot

Copy link
Copy Markdown
Collaborator Author

Done in 57f57ad.

Repetition. The two interactive zsh compinit probes — check_zsh_compinit_missing (config show) and shell::detect_zsh_compinit (shell setup) — are indeed near-duplicates, and this PR made it worse by patching +m into both. They diverge on purpose, though, so I went with the TODO rather than a risky mechanical merge:

  • check_zsh_compinit_missing passes --no-globalrcs (we only nudge when the user's config lacks compinit, so /etc/zshrc enabling it must not suppress the warning).
  • detect_zsh_compinit deliberately sources global rcs (shell setup shouldn't re-add what the system already enables).
  • They also differ in return type (bool vs Option<bool>) and runner (Cmd vs raw Command).

I left a shared // TODO(zsh-compinit-probe-unify) on both, pointing at a single probe_zsh_compdef(no_globalrcs: bool) helper as the unification shape. Happy to do that here if you'd prefer it landed now.

Also dropped a stray -l file that snuck into the branch — it was captured wt config show output (cargo run -- config show > -l style), not part of the fix.

Catching it initially — yes, a PTY test would. And the deterministic signal is not the downstream pager suspend (that's the env-dependent part the unit guards punt on) — it's the controlling-terminal foreground process group right after the timeout-kill. I reproduced it under a PTY, replicating the probe's exact spawn (interactive zsh -ic, slow rc, SIGKILL at 2s):

WITHOUT +m: my_pgrp=12847 terminal_fg=12848 -> STOLEN(stranded)
WITH    +m: my_pgrp=12854 terminal_fg=12854 -> INTACT

Without +m, tcgetpgrp of the slave moves to the dead probe's group and stays there → the next tty op (the pager) gets SIGTTOU. With +m the foreground never moves. That tcgetpgrp == own pgrp assertion is fully deterministic — no dependence on whether less happens to page — so it sidesteps the flakiness the PR notes.

The repo already has the harness for it (portable-pty, e.g. config_update_pty.rs). The one wrinkle: asserting on tcgetpgrp is clean when the test drives the probe mechanism directly, but observing it through the real wt config show is awkward because the probe is a transient grandchild — externally you can only see the consequence (suspend), which is the flaky bit. So the honest options are (a) a focused PTY test over the probe spawn asserting foreground integrity, or (b) an end-to-end wt config show PTY test with a forced slow .zshrc asserting clean exit (slightly flakier). I'd lean (a). Want me to add it?

@max-sixty max-sixty enabled auto-merge (squash) July 9, 2026 19:11
@max-sixty max-sixty merged commit adb417e into main Jul 9, 2026
36 checks passed
@max-sixty max-sixty deleted the fix/issue-3322 branch July 9, 2026 19:23
@max-sixty max-sixty mentioned this pull request Jul 10, 2026
max-sixty added a commit that referenced this pull request Jul 10, 2026
Release v0.67.0: version bump and changelog.

Highlights since v0.66.0: experimental `wt remove --reap` (#3396), `wt
switch -x` opening the picker (#3394), the termimad panic containment in
the picker's PR-comments preview (#3408), the `wt config show` SIGTTOU
fix (#3327), the `GIT_*` discovery-var scrub for `wt step for-each` and
the `--execute` fallback (#3400), and the `wt list` prompt-reserve fix
(#3409).

Seven commits landed on `main` after the initial cut; `origin/main` is
merged back in and the two user-facing ones (#3394, #3411) plus the BY
CONTEXT profile table (#3403) are folded into the changelog.

Pre-release validation: local pre-merge gate green on the merged tree
(4389 tests); nightly cross-platform suite green on the initial cut
([run
29086956095](https://github.com/max-sixty/worktrunk/actions/runs/29086956095)),
with the seven post-cut commits each validated by their own PR CI;
`cargo semver-checks` reports no breaking changes; data-loss surface
review of the cumulative diff (including the drift commits) found one
new destructive capability (`--reap`, explicit opt-in, adjudicated
acceptable).

> _This was written by Claude Code on behalf of max_
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

wt config show causes process to be suspended

2 participants