Keep a process running after you close the terminal or SSH disconnects

You start something long in a terminal, close the window or lose your SSH connection, and it's gone. That isn't a bug: it's how Unix terminals were designed. Here's why it happens, and five ways around it, from a one-word prefix to a service that survives reboots.

Why closing the terminal kills your program

A program you start in a terminal belongs to that terminal's session. When the terminal goes away, because you closed the window or your SSH connection dropped, the system sends the session a hangup signal (SIGHUP). The name dates from modems literally hanging up. Most programs exit when they receive it, which is the right default for a shell you've finished with, and the wrong one for a build that has another twenty minutes to go.

A second problem hides behind the first: even if the program survives, its output was going to a terminal that no longer exists, and if it ever asks a question, there's no one left to answer it.

Each method below solves some of this. The difference is how much.

1. nohup: ignore the hangup

nohup starts a program with the hangup signal ignored. Send the output to a file, and put & at the end to get your prompt back:

nohup ./backup.sh > backup.log 2>&1 < /dev/null &

Good for: scripts that run start to finish without asking anything. Not for: anything interactive. You can't answer a prompt, and you can't get back into it.

2. disown: you already started it

Forgot nohup? If the program is still running in your shell, you can detach it from the shell after the fact:

# It's running in the foreground. Pause it with Ctrl-Z, then:
bg        # continue it in the background
disown    # remove it from the shell's jobs, so the shell won't hang it up

zsh also has a shortcut for starting a job that's disowned from the beginning: long-command &!.

disown stops your shell from sending the hangup when it exits, but the program's output still goes to the old terminal, and it will fail if it tries to write there after the terminal closes. Treat it as a rescue, not a plan.

3. tmux: a terminal that outlives your connection

A terminal multiplexer runs your shell inside a server process that doesn't belong to your window or your SSH connection. You can detach, disconnect, reconnect from anywhere and pick up exactly where you left off, including answering prompts.

tmux new -s build        # start a session named "build"
npm run build            # run things as usual
# Detach: press Ctrl-b, then d. The session keeps running.
tmux ls                  # later: list sessions
tmux attach -t build     # and get back in

tmux isn't installed on macOS by default; get it with brew install tmux. It's in every Linux distribution's package manager.

Good for: interactive work over SSH, especially on servers. The catch: you have to start tmux before the job. A job you started in a plain terminal can't be moved into tmux later. On a phone keyboard, the Ctrl-b shortcuts take practice.

4. screen: the older sibling

GNU screen does the same job as tmux with different keys, and is often already installed:

screen -S build          # start a named session
# Detach: Ctrl-a, then d
screen -ls               # list sessions
screen -r build          # reattach

If you're choosing fresh, tmux is more actively developed and easier to configure. If screen is what's on the machine in front of you, it works fine.

5. launchd (or systemd): a real service

For something that should always be running, and should come back after a crash or a reboot, a terminal is the wrong home. On a Mac, that's a job for launchd. Save this as ~/Library/LaunchAgents/com.example.worker.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>Label</key><string>com.example.worker</string>
  <key>ProgramArguments</key>
  <array><string>/Users/you/bin/worker.sh</string></array>
  <key>KeepAlive</key><true/>
  <key>StandardOutPath</key><string>/Users/you/Library/Logs/worker.log</string>
  <key>StandardErrorPath</key><string>/Users/you/Library/Logs/worker.log</string>
</dict></plist>

Then load it, and unload it when you're done:

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.worker.plist
launchctl bootout gui/$(id -u)/com.example.worker

Use full paths in the plist: launchd doesn't read your shell's PATH. On Linux, a systemd user service does the same job. Good for: daemons, watchers, bots. Not for: a one-off job you want to watch or answer.

Which one to use

MethodSurvives disconnectCan answer prompts laterSurvives rebootPlan ahead?
nohupYesNoNoYes
disownMostlyNoNoNo
tmux / screenYesYesNoYes
launchd / systemdYesNoYesYes

One more thing none of these handle: a Mac that goes to sleep stops everything, including tmux. See caffeinate: keep your Mac awake from the terminal.

If the job is on your Mac and you're not

Most of these tools come from a world of servers you SSH into. If the job is on your own Mac and you want to check on it from your phone, the missing piece is usually reaching the Mac at all, and then a terminal you can actually use on a phone keyboard.

PandaSend Terminal takes the tmux idea and builds it into the app: every session runs in a background process on your Mac, so closing the window, quitting the app or losing your phone's connection never ends it. You open the same live session from your phone's browser, at home or on cellular, with Esc, Tab, Ctrl and arrow keys on a key bar. There's nothing to remember before you start: every session already works this way. For a side-by-side look at the SSH route, see SSH, tmux, mosh or PandaSend.