fish-shell/fish-shell · critical
setpgid
Error message
setpgid
What it means
At startup an interactive fish attempts to create/verify its own process group with setpgid(2) so it can own the terminal. If tcgetpgrp/setpgid fail such that fish cannot control the TTY, it logs the 'No TTY for interactive shell' warning, perror("setpgid"), and exits via exit_without_destructors(1). This is a hard fatal condition — the interactive reader cannot run without a controlling terminal.
Source
Thrown at src/reader/reader.rs:4963
// just an "invalid" pid for all intents and purposes.
if owner == Ok(NullablePid::from_raw(0)) {
let _ = tcsetpgrp(STDIN_FD, shell_pgid);
// Since we expect the above to work, call `tcgetpgrp()` immediately to
// avoid a second pass through this loop.
owner = tcgetpgrp(STDIN_FD);
}
if owner == Err(Errno::ENOTTY) {
if !is_interactive_session() {
// It's OK if we're not able to take control of the terminal. We handle
// the fallout from this in a few other places.
break;
}
// No TTY, cannot be interactive?
flog!(
warning,
wgettext!("No TTY for interactive shell (tcgetpgrp failed)")
);
perror("setpgid");
exit_without_destructors(1);
}
if owner == Ok(shell_pgid) {
break; // success
} else {
if check_for_orphaned_process(loop_count, shell_pgid) {
// We're orphaned, so we just die. Another sad statistic.
let pid = getpid();
flog!(
warning,
sprintf!(
"I appear to be an orphaned process, so I am quitting politely. My pid is %d.",
pid.as_raw()
)
);
exit_without_destructors(1);
}
View on GitHub (pinned to 35fd72ad03)
Solutions
- Don't force -i when there is no tty; run fish non-interactively (remove -i/--interactive)
- Allocate a pty: use `script -qc fish /dev/null`, `ssh -t`, or `docker run -it`
- Avoid starting fish via setsid/daemonization if interactive use is intended
- Check job control support: ensure the parent shell isn't disabling job control and the process isn't a session leader before setpgid
Example fix
// before (script/CI) fish -i -c 'my_interactive_setup' // after fish -c 'my_interactive_setup' # drop -i; no tty available // or, to force a pty: script -qc "fish -i" /dev/null
Defensive patterns
Strategy: validation
Validate before calling
# Ensure a controlling tty exists before launching interactive fish:
if not tty >/dev/null 2>&1
echo "no controlling terminal; run fish without -i or allocate a pty (ssh -t, docker -it, script -qc)" >&2
exit 1
end Try / catch
// Only enter interactive mode when the tty is controllable:
let tty_ok = unsafe { libc::tcgetpgrp(libc::STDIN_FILENO) } != -1;
if !tty_ok {
flog!(warning, "No TTY for interactive shell");
// fall back to non-interactive instead of exit_without_destructors(1)
} Prevention
- Never pass -i/--interactive when stdin is a file, pipe, or CI runner
- Use ssh -t / docker run -it / script -qc to guarantee a pty for interactive shells
- Don't start interactive fish under setsid or session leaders that can't setpgid
- Check `tty` succeeds in wrapper scripts before exec'ing fish -i
When it happens
Trigger: Launching fish with -i (interactive) when stdin is not a tty or has no controlling terminal: e.g. `setsid fish`, running under a daemon/CI without a pty, or the shell's pgid differs from the tty's foreground pgid and setpgid fails with EPERM (session leader restrictions) in src/reader/reader.rs:4963.
Common situations: `ssh host fish -i` with stdin redirected from a file; running fish -i inside Docker without -t; job-control-less environments (cron, systemd services); EPERM because the process is a session leader that cannot change its pgid.
Related errors
AI-assisted analysis of fish-shell/fish-shell@35fd72ad03 (2026-09-03).
Data as JSON: /api/errors/d82983ed0d32037f.
Report an issue: GitHub.