Skip to content
Go back

A Terminal Built for AI Coding Agents

By KingPin 15 min read
A Terminal Built for AI Coding Agents
Contents

Three Agents, One Checkout, Zero Idea Who Broke What

You started three coding agents because one felt slow. Now agent A is rewriting the auth module, agent B is “fixing” the same file, and agent C just killed the dev server that A was using. Your 2 AM self is scrolling through a wall of identical panes trying to remember which one asked you a question forty minutes ago.

The agent is not the bottleneck. Your terminal is. The setup that fixes it has three parts: one agent per git worktree, one tmux window per worktree, and a bell so the agent tells you when it is waiting. You stop babysitting panes and start answering the one that rang. This article builds that setup as of October 2026, with tmux 3.8 and Claude Code docs from the same month.

Full example: Clone the working files at github.com/KingPin/sumguy-examples/productivity/terminal-setup-ai-coding-agents

Why Parallel Agents Fight in One Checkout

Two agents in one directory share everything: the working tree, the index, the lockfile, and the port the dev server wants. Agent A runs git checkout to switch branches and agent B’s files change under its feet. Both run npm run dev and the second one dies with EADDRINUSE. Both stage changes and you get a commit that contains half of each task.

You can fight this with discipline, or you can give each agent its own directory. Git has had the feature for years. A worktree is a second working directory attached to the same repository, with its own checked-out branch and its own files. History and remote stay shared.

Terminal window
cd ~/code/myapp
git worktree add ../myapp-wt/fix-login -b agent/fix-login
git worktree list
/home/you/code/myapp bc1f2c4 [main]
/home/you/code/myapp-wt/fix-login bc1f2c4 [agent/fix-login]

The four commands you need:

Terminal window
git worktree add <path> -b <new-branch> # create a directory on a new branch
git worktree list # show all worktrees
git worktree remove <path> # delete one (clean trees only)
git worktree prune # clear records of directories you deleted by hand

Put the worktrees outside the repo, next to it. Then nothing shows up as untracked in your main checkout and no tool walks into the nested directory by accident.

Claude Code can do this for you

Current Claude Code has built-in support. The worktrees docs say to pass --worktree (short form -w) with a name:

Terminal window
claude --worktree feature-auth

That creates .claude/worktrees/feature-auth/ at the repo root on a new branch named worktree-feature-auth, then starts Claude in it. Run it again with another name in another terminal for a second isolated session. If you leave the name off, Claude picks one such as bright-running-fox. On exit, if the worktree is clean, an unnamed session removes the worktree and its branch automatically. A session you named with -n or /rename asks first. A worktree with changes or new commits always asks whether to keep or remove it.

The claude --help output on Claude Code 2.1.311 also lists --tmux, which requires --worktree and creates a tmux session for the worktree. It uses iTerm2 native panes when they are available, and --tmux=classic forces plain tmux. That flag only exists for Claude, so the rest of this setup does not depend on it.

The docs also describe a .worktreeinclude file (.gitignore syntax) that copies gitignored files such as .env into each new worktree. Add .claude/worktrees/ to your .gitignore.

That covers Claude Code. Codex CLI, Aider, and whatever ships next month do not necessarily have the same flag, and plain git works with all of them. The script below is agent-agnostic.

agent-wt: One Command, One Worktree, One Window

The glue is a small bash script. agent-wt new fix-login creates the worktree on branch agent/fix-login, opens a tmux window named wt-fix-login in it, and types your agent command into that window. agent-wt rm fix-login undoes all three.

agent-wt
#!/usr/bin/env bash
# agent-wt: one git worktree + one tmux window per agent.
# agent-wt new <name> [base-ref] create worktree on branch agent/<name>, open a window
# agent-wt rm <name> close the window, remove the worktree and branch
# agent-wt ls list worktrees
# Run it from inside the main checkout, inside tmux.
# AGENT_CMD (default: claude) is typed into the new window. Set it empty to get a bare shell.
set -euo pipefail
cmd=${1:-}
name=${2:-}
repo=$(git rev-parse --show-toplevel)
# Worktrees live next to the repo: ~/code/myapp -> ~/code/myapp-wt/<name>
wt_root="${repo}-wt"
path="$wt_root/$name"
branch="agent/$name"
win="wt-$name"
find_window() {
tmux list-windows -F '#{window_id} #{window_name}' | awk -v n="$win" '$2 == n { print $1; exit }'
}
case "$cmd" in
new)
[ -n "$name" ] || { echo "usage: agent-wt new <name> [base-ref]" >&2; exit 2; }
[ -n "${TMUX:-}" ] || { echo "agent-wt: run this inside tmux" >&2; exit 1; }
base=${3:-HEAD}
mkdir -p "$wt_root"
git worktree add "$path" -b "$branch" "$base"
tmux new-window -n "$win" -c "$path"
if [ -n "${AGENT_CMD-claude}" ]; then
tmux send-keys -t "$(find_window)" "${AGENT_CMD-claude}" Enter
fi
;;
rm)
[ -n "$name" ] || { echo "usage: agent-wt rm <name>" >&2; exit 2; }
git worktree remove "$path" # refuses if the tree is dirty; add --force yourself
git worktree prune
id=$(find_window || true)
if [ -n "$id" ]; then tmux kill-window -t "$id"; fi
git branch -d "$branch" # refuses if the branch is unmerged; use -D yourself
;;
ls)
git worktree list
;;
*)
echo "usage: agent-wt {new <name> [base-ref] | rm <name> | ls}" >&2
exit 2
;;
esac

Two design choices are deliberate. The window is found by its ID, because a : in a window name makes tmux read it as session:window. And rm removes the worktree before it touches the window, so a dirty tree makes git refuse and your agent window survives with its work intact. The branch goes last: if it is not merged into your current branch (the normal case after a squash merge on GitHub), git branch -d refuses and the branch stays. Delete it with git branch -D once you have checked the PR.

I ran it in a throwaway repo inside an Alpine container with tmux 3.8 and git 2.56.0. new created the worktree and a named window, a second new worked, rm refused on a tree with an untracked file, and rm on a clean tree removed the worktree, the branch, and the window.

Terminal window
install -m 755 agent-wt ~/.local/bin/agent-wt
cd ~/code/myapp
tmux
agent-wt new fix-login
AGENT_CMD=aider agent-wt new docs-pass
agent-wt ls

tmux Settings That Matter for Agent Work

Agents print a lot, want modified keys, and need to reach your clipboard. Here is the config from the companion repo. I checked every option name and value against the tmux man page and loaded the file in tmux 3.8 without errors.

~/.tmux.conf
# Agent-friendly tmux settings. Tested against tmux 3.8.
set -g history-limit 100000
set -g mouse on
# Shift+Enter reaches the agent as a distinct key.
set -s extended-keys on
set -as terminal-features 'xterm*:extkeys'
# Let OSC 52 clipboard writes and notifications from the visible pane reach the outer terminal.
# Use 'allow-passthrough all' to pass them from background windows too.
set -g set-clipboard on
set -g allow-passthrough on
set -g focus-events on
# A bell in any window flags that window in the status bar.
set -g monitor-bell on
set -g bell-action other
set -g visual-bell off
set -g window-status-bell-style 'bg=red,fg=white,bold'
# New windows start at 1 so the keys line up with the status bar.
set -g base-index 1

What each block does:

  1. history-limit 100000 raises the scrollback per pane. An agent that dumps a test run and a diff burns through the default fast.
  2. mouse on gives you wheel scrolling and click-to-select-pane. Hold Shift in most terminals to select text natively.
  3. extended-keys on plus the extkeys terminal feature lets tmux tell Shift+Enter from plain Enter. The Claude Code docs list these two lines as the fix for Shift+Enter submitting instead of inserting a newline inside tmux. The man page accepts on, off, and always.
  4. set-clipboard on lets tmux set the clipboard of the outer terminal through OSC 52 (accepted values are on, external, and off).
  5. allow-passthrough on lets programs in a pane bypass tmux with a passthrough escape sequence, only while the pane is visible. Agents in background windows are not visible, so set it to all if you want their desktop notifications too. The Claude Code docs say desktop notifications and the progress bar never reach the outer terminal without it. You do not need it for the bell, which tmux handles itself.
  6. monitor-bell on makes tmux watch each window for a BEL. bell-action other raises the alert only for windows other than the one you are looking at (accepted values: any, none, current, other). visual-bell off passes the bell to your terminal instead of showing a tmux message (it also accepts on and both). window-status-bell-style colors the flagged window in the status bar.

I tested the flag end to end: I printed a BEL in a background window and #{window_bell_flag} went to 1 for that window and stayed 0 for the others.

Reload without restarting: tmux source-file ~/.tmux.conf. The -s options apply to the server, so detach and reattach if something looks stale.

Make the Agent Ring the Bell

A flagged window needs a bell to flag. Claude Code gives you two ways to produce one.

Option 1: the built-in setting

In terminals other than Ghostty, Kitty, and iTerm2, Claude Code does not send a desktop notification by default. The settings docs say to set preferredNotifChannel to "terminal_bell" instead:

~/.claude/settings.json
{
"preferredNotifChannel": "terminal_bell"
}

One line, no scripts. Start here.

Option 2: a Notification hook

If you want control over which events ring, use a hook. The Notification event takes a matcher on the notification type. Two types matter for this setup:

MatcherWhen it fires
permission_promptClaude needs you to approve a tool use, and the prompt has waited about six seconds
idle_promptClaude finished responding about 60 seconds ago and you have not typed since

Note the delays. The bell is not instant: a permission prompt rings after about six seconds, and an idle agent after about a minute. That is fine for walking away from the desk and not fine if you expect a bell on the same tick.

Now the trap. Your first instinct is printf '\a' > /dev/tty in the hook command. Do not. The hooks docs say hooks run without a controlling terminal, so writing to /dev/tty fails. And a hook’s stdout is not shown to you. The documented route is the terminalSequence field: the hook prints JSON, and Claude Code emits the sequence on its own terminal write path. The field accepts a bare BEL plus a few OSC notification sequences, and it works inside tmux. It is ignored in non-interactive -p mode and in the Agent SDK.

~/.claude/settings.json
{
"hooks": {
"Notification": [
{
"matcher": "permission_prompt|idle_prompt",
"hooks": [
{ "type": "command", "command": "printf '{\"terminalSequence\":\"\\\\u0007\"}'" }
]
}
]
}
}

The matcher uses | to match either name, which the hooks docs list as valid. The shell sees printf '{"terminalSequence":"\\u0007"}', and printf turns the double backslash into a single one, so the output is {"terminalSequence":"\u0007"}. I ran that command and parsed its output as JSON to confirm the field holds a single BEL character, and python3 -m json.tool accepts the settings file. I did not run the hook inside a live Claude Code session, so treat that last hop as documented rather than tested.

Hooks run next to the built-in notification, not instead of it. Use one mechanism or you get two alerts.

Shift+Enter and /terminal-setup

Enter submits your prompt. Ctrl+J, or a backslash followed by Enter, inserts a newline in every terminal with no setup. Shift+Enter works out of the box in Ghostty, Kitty, iTerm2, WezTerm, Warp, Apple Terminal, and Windows Terminal. Other terminals that speak the kitty keyboard protocol, such as foot and Alacritty 0.16 or later, work on Claude Code v2.1.269 or later.

You run /terminal-setup once in VS Code, Cursor, Devin Desktop, Alacritty before 0.16, and Zed. Run it in the host terminal, not inside tmux or screen, since it writes to the host terminal’s config. gnome-terminal and the JetBrains terminals have no Shift+Enter support at all. Inside tmux, the extended-keys lines from the previous section are required on top of all this.

The Terminal Emulator Side, Briefly

Pick a terminal that gets Shift+Enter right without ceremony and move on. The site already compares Ghostty, Kitty, and WezTerm and tmux, Zellij, and screen, so this is not another review. Zellij users can keep the worktree half of this setup unchanged, because git does not care which multiplexer holds the window. The agent-wt script and the bell flagging are tmux-specific, and I did not test the Zellij equivalents.

The Daily Loop

Here is what a morning looks like once this is in place:

  1. cd ~/code/myapp && tmux.
  2. agent-wt new fix-login, agent-wt new docs-pass, agent-wt new flaky-test. Three windows, three branches, three agents.
  3. Give each a task. Switch to your own work or your coffee.
  4. A window label turns red in the status bar. Jump to it with the window number, answer the prompt, jump back.
  5. When an agent finishes, review the branch with git diff main...agent/fix-login, merge or open a PR, then agent-wt rm fix-login.

You check windows when they ring instead of cycling through all of them. A forklift driver does not walk the warehouse checking every pallet. They wait for the beeper.

Gotchas Worth Knowing Before 2 AM

The object store is shared. All worktrees write to the one .git directory in the main repo. The Claude Code worktree docs say as much. Branches and tags are shared, which is why a commit in one worktree is visible from another right away. HEAD is the exception: each worktree has its own.

One branch, one worktree. git refuses to check out the same branch in two worktrees. I hit it on purpose:

fatal: 'agent/feat-a' is already used by worktree at '/home/you/code/myapp-wt/feat-a'

The --force flag on git worktree add overrides the safeguard, and you should not. Two agents on one branch is the original problem again. Give every agent its own branch and merge later.

Every worktree is a fresh checkout. There is no node_modules, no virtualenv, and no .env. Install dependencies in each worktree. Claude Code’s .worktreeinclude copies gitignored files for worktrees it creates, but agent-wt does not, and I removed an early .env copy step on purpose: an untracked file makes git worktree remove refuse, because git only removes clean worktrees (no untracked files, no modified tracked files).

Ports collide. Two worktrees means two dev servers. Pass a distinct port per agent, for example PORT=3001 npm run dev in one and PORT=3002 in the other, or let your tooling pick a free one. Tell each agent which port is its own.

Removal is deliberately timid. git worktree remove refuses unclean trees unless you add --force. git branch -d refuses unmerged branches unless you use -D. That is the safety net, so use the force flags only after you have looked at the diff.

Deleted directories leave records. If you rm -rf a worktree directory, git keeps its administrative files until gc.worktreePruneExpire expires or you run git worktree prune. Run git worktree list now and then and prune what is stale.

Moved worktrees break links. If you move a worktree with plain mv, run git worktree repair from the moved directory, or use git worktree move next time.

Common Questions

Can I run two Claude Code sessions in the same repo?

Yes, but give each session its own git worktree. Two sessions in one checkout share the working tree, the index, and the dev-server port, so they overwrite each other’s edits. Start each with claude --worktree <name>, or create worktrees with git worktree add and run claude inside them.

Does claude --worktree work in every Claude Code version?

No. The --worktree flag (short form -w) is documented for current Claude Code releases, and older installs lack it. Run claude --help and look for --worktree before you build a workflow on it. If the flag is missing, git worktree add plus a plain claude inside the new directory gives the same isolation.

How do I get a notification when Claude Code needs input?

Set preferredNotifChannel to "terminal_bell" in ~/.claude/settings.json, or add a Notification hook that returns a terminalSequence BEL. With monitor-bell on in tmux, the window that rang is highlighted in the status bar. Ghostty, Kitty, and iTerm2 also get desktop notifications by default.

Can I use Zellij instead of tmux for this?

Yes for the worktree half, which depends only on git. The agent-wt script calls tmux commands, so it needs rewriting for Zellij, and I did not test Zellij’s bell handling. If you already live in Zellij, create worktrees by hand with git worktree add and open a tab in each.

How do I clean up worktrees I deleted by hand?

Run git worktree prune from the main repo. It removes the administrative records for worktree directories that no longer exist. Run git worktree list first to see which ones are stale. Git also expires these records on its own after the gc.worktreePruneExpire period.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
When Vaultwarden Dies: A Recovery Drill

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts