# tmux for coding agents: the commands that matter

> The tmux commands that keep agent work on a remote machine safe to repeat, from guarded starts and logs to port waits, reading panes and reboots.

*2026-11-02 · tmux, agents, remote-dev, workflows*

A person uses tmux by attaching to a session and watching it. An agent mostly can't. It runs one command at a time over SSH with no terminal attached, its commands time out, and it may run a command again when the first attempt looked stuck. The tmux commands that matter for agents are the ones that hold up under those conditions.

Our posts on [closing the laptop](/blog/keep-claude-code-running-after-closing-laptop) and [git worktrees](/blog/parallel-coding-agents-git-worktrees) cover starting, detaching and reattaching. This one picks up there, with the patterns from [shedix's guide for agents](/docs/agents). We ran each example below on tmux 3.4, the version Ubuntu 24.04 installs.

## Start it only if it isn't running

A start command an agent might repeat has to be safe to run twice. `tmux new -d` refuses a name that's taken and exits with `duplicate session: api`. `tmux new -A`, which attaches when the session exists, suits a person and not a script: without a terminal it fails with `open terminal failed: not a terminal`.

Ask first instead. The [tmux manual](https://man.openbsd.org/tmux.1) says `has-session` exits with 0 when the session exists and 1 when it doesn't:

```bash
tmux has-session -t api 2>/dev/null || tmux new -d -s api -c ~/work/app/api 'npm run dev > ~/work/api.log 2>&1'
```

Run it twice and you still have one server. This matters most after a timeout. Claude Code waits two minutes for a foreground command by default, then [moves it to the background](https://code.claude.com/docs/en/tools-reference) rather than stopping it, so the start it gave up on is probably still going. Check `tmux ls` before starting anything again.

## One session per service, and its output in a file

When the command in a session exits, the session closes, and whatever it printed goes with it. A dev server that crashed on start leaves nothing to read. So give each service its own session, named after it, and send its output to a file:

```bash
tmux has-session -t api 2>/dev/null || tmux new -d -s api "cd ~/work/app/api && (npm ci && npm run dev) > ~/work/api.log 2>&1"
tmux has-session -t web 2>/dev/null || tmux new -d -s web "cd ~/work/app/web && (npm ci && npm run dev) > ~/work/web.log 2>&1"
```

To restart one service, end its session with `tmux kill-session -t api` and run its line again. The others keep running.

For a job that finishes, write down how it ended, since nobody will be watching:

```bash
tmux new -d -s tests "cd ~/work/app && npm test > ~/work/tests.log 2>&1; echo \"exit \$?\" >> ~/work/tests.log"
```

`tail -n 30 ~/work/tests.log` then shows the end of the run and its exit code. To keep the screen itself instead, `tmux set -g remain-on-exit on` leaves a pane open, marked dead, after its program exits.

Two more habits from the guide:

- **Close stdin on installs.** An install that asks a question waits forever in a detached session. Add `< /dev/null` and it fails at once instead.
- **Run servers in tmux, not as the agent's background tasks.** Claude Code's [docs](https://code.claude.com/docs/en/interactive-mode) say its background tasks are cleaned up when it exits. A server in its own tmux session outlives the agent, your SSH connection and the next agent.

## The environment comes from the server

The manual explains that a tmux server copies the environment once, when it starts, and new sessions get that copy rather than the environment of the shell that created them. Only the few variables in the `update-environment` option, such as `SSH_AUTH_SOCK` and `DISPLAY`, are refreshed. Try it where no tmux server is running yet:

```bash
FOO=first tmux new -d -s one 'sleep 60'
FOO=second tmux new -d -s two 'echo "FOO=$FOO" > ~/two.txt; sleep 5'
cat ~/two.txt
```

That prints `FOO=first`. So `export DATABASE_URL=…` followed by `tmux new -d …` works on a fresh server and quietly does nothing on a running one. Pass the variable with `-e DATABASE_URL="$DATABASE_URL"`, or keep it in the `.env` file the app reads.

The command also runs as your shell's `-c`, not as a login shell. In our test it read neither `.bashrc` nor `.profile`, so whatever those set up, such as a version manager's `PATH`, may be missing. Wrap the command in `bash -lc '…'` or use full paths. tmux won't activate a Python virtual environment either, so start the app with `uv run …` or `.venv/bin/…`.

## Wait for the port, or for the job

A server that has started isn't necessarily ready. Wait for its port, and check its session on every pass, so a service that dies stops the wait at once, with its log, instead of running out the clock. This is the guide's loop for two services:

```bash
end=$((SECONDS+600))
for sp in api:4000 web:3000; do
  s=${sp%%:*} p=${sp##*:}
  until ss -Hltn "sport = :$p" | grep -q .; do
    tmux has-session -t "$s" 2>/dev/null || { echo "$s stopped"; tail -n 40 ~/work/$s.log; exit 1; }
    [ $SECONDS -lt $end ] || { echo "$s: port $p never opened"; tail -n 40 ~/work/$s.log; exit 1; }
    sleep 2
  done
done
echo "all ports open"
```

`ss` exits with 0 whether or not a port matches, which is why its output goes through `grep -q .`. An open port only means something is listening, so finish with a real request to the app.

For a job, tmux can do the waiting. `wait-for` blocks until another command signals the same channel:

```bash
tmux new -d -s tests "cd ~/work/app && npm test > ~/work/tests.log 2>&1; echo \"exit \$?\" >> ~/work/tests.log; tmux wait-for -S tests-done"
timeout 110 tmux wait-for tests-done; tail -n 20 ~/work/tests.log
```

Put `;` before the signal, not `&&`, or a failing run never signals. If the job finishes before anyone waits, the next `wait-for` returns at once, but only one does. The `timeout` keeps each wait inside Claude Code's two-minute default, and a wait cut off that way can be run again: it still gets the signal.

## Read a session without attaching

Services write logs, but an agent's own screen doesn't. `capture-pane -p` prints a pane to standard output:

```bash
tmux capture-pane -p -t claude
tmux capture-pane -p -J -S -200 -t claude
```

The first prints what's on screen. The second adds 200 lines of history (`-S -` starts at the very beginning) and joins wrapped lines. A session started with `-d` is 80 columns wide until someone attaches, so long lines wrap: in our test, a 138-character error came back as two lines without `-J` and as one with it, which is what you want before a `grep`.

Typing into a session uses `send-keys`, which has two traps. An argument that is a key name, such as `Enter` or `Escape`, is sent as that key unless you add `-l`. And a semicolon at the end of an argument ends the tmux command, even with `-l`, so `const a = 1;` arrives without it unless you write `\;`. Send the text and the Enter key separately:

```bash
tmux send-keys -t claude -l 'Run the tests again and fix what fails'
tmux send-keys -t claude Enter
```

crystl, who make a terminal app for Claude Code, cover typing into agents in more depth in their [tmux guide](https://crystl.dev/guides/tmux-for-ai-agents/), including a short pause before the Enter. To know when an agent is waiting for you without reading its screen, Manuel Schipper [uses](https://schipper.ai/posts/parallel-coding-agents/) a Claude Code [hook](https://code.claude.com/docs/en/hooks) that rings the terminal bell when the agent goes idle, and has tmux highlight that window.

## Three settings worth changing

Put these in `~/.tmux.conf` before you start sessions:

```text
set -sg escape-time 0
set -g history-limit 50000
set -g mouse on
```

- **escape-time:** after an Esc, tmux waits to see whether more keys follow. tmux 3.4 waits 500 ms, and [3.5](https://raw.githubusercontent.com/tmux/tmux/master/CHANGES) cut the default to 10 ms. Esc is how you interrupt Claude Code, so half a second is noticeable.
- **history-limit:** the default is 2,000 lines, a short stretch of agent output. In 3.4 it applies only to windows created after the change.
- **mouse:** the wheel or trackpad scrolls back through history. Without it, `Ctrl-b [` enters copy mode and `q` leaves it.

Claude Code's `Ctrl+B`, which backgrounds a running command, is also tmux's prefix, so inside tmux you press it twice.

## After a reboot

tmux keeps sessions through lost connections, not through restarts. The server is an ordinary process: when the machine restarts, every session and everything running in it is gone. Files stay, logs included.

That's when guarded starts pay off: run the same start commands again, and each service comes back. Claude Code keeps conversations on disk, so `claude --continue` in the same folder reopens the latest one ([sessions docs](https://code.claude.com/docs/en/sessions)).

## How shedix uses this

A shedix machine that goes unused for an hour falls asleep, and sleep is a stop: for tmux, a reboot. Turn on Keep awake for a long job nobody is watching.

The Terminal, Claude Code and Codex tabs on a machine's page are tmux sessions, opened with `tmux new -A`. When an agent starts Claude Code in a session called `claude`, as the guide's hand-off does, the Claude Code tab opens that session from a browser or your phone. The machines come with the three settings above, and the [agents' guide](/docs/agents) is built on these patterns: with it, two fresh agents each had a project running in about three minutes.
