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

  1. Don't force -i when there is no tty; run fish non-interactively (remove -i/--interactive)
  2. Allocate a pty: use `script -qc fish /dev/null`, `ssh -t`, or `docker run -it`
  3. Avoid starting fish via setsid/daemonization if interactive use is intended
  4. 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

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.