Ghostty Is the Terminal Claude Code Deserves

Ghostty the terminal for Claude Code: Config, Splits, and Scripts

👁18,544views

Ghostty pairs exceptionally well with Claude Code because its GPU-accelerated rendering, native split panes, and robust protocol support eliminate the terminal friction that accumulates during long agentic sessions. When Claude Code is looping through file edits and test runs, a fast, well-configured terminal keeps context visible and reduces the small interruptions that quietly erode focus over hours of work.

CloudScale AI SEO - Article Summary
  • 1.
    What it is
    Ghostty terminal setup for Claude Code covers core configuration, three split layouts, and paste ready bootstrap scripts to optimise your agentic coding environment.
  • 2.
    Why it matters
    Ghostty forwards Claude Code desktop notifications to your OS by default and restores every split and tab on relaunch, eliminating the environment rebuilding friction that breaks focus across multi hour sessions.
  • 3.
    Key takeaway
    No other terminal has a built in Quake style quick terminal, meaning Ghostty is the only option that lets you check a secondary window mid operation without losing your IDE context through Alt Tab switching.
~15 min read

There is a meaningful difference between running Claude Code and running it well. The agent is doing serious work: reading your codebase, modifying files, running tests, interpreting output, and looping back. If your terminal is fighting you the whole time, you are burning cognitive load on the environment instead of the work.

Ghostty is a fast terminal emulator written in Zig, created by Mitchell Hashimoto (the person who built Terraform and Vagrant at HashiCorp). It is configured through a plain text file with no nesting and no mandatory quotes, and it has native split panes, native tabs, a drop-down quick terminal, and first-class support for the notification and keyboard protocols Claude Code relies on.

This post is a practical setup guide covering the core configuration, three split layouts I use daily, and paste-ready scripts to bootstrap everything in one go.

A note on terminology before we start. The Super key is the key with the cmd symbol on a Mac keyboard, and the Windows key on a Linux keyboard. Throughout this post, super+d means hold that key and press D at the same time, exactly like cmd+c for copy. A split pane is when the terminal window is divided into two sections side by side or top and bottom, each running independently. A shell is the command line prompt you type into; on macOS this is zsh by default, and on Linux it is usually bash.

1. Why Ghostty over the Alternatives

Ghostty is not the only serious terminal on macOS or Linux. Here is the honest comparison:

AlacrittyKittyWezTermGhostty
Native splitsNoYesYesYes
TabsNoYesYesYes
Fast renderingYesYesYesYes
Config formatTOMLkitty.confLuaPlain text
Drop-down quick terminalNoNoNoYes
Claude Code notificationsManual setupYesManual setupYes (default)

The two things that matter most for Claude Code work are native splits and notification forwarding. Claude Code fires desktop notifications when it finishes a task or needs a permission decision, and Ghostty forwards those to your OS notification centre without any configuration. Kitty does this too, but nothing else does by default.

The drop-down quick terminal is a global keyboard shortcut that slides a terminal down from the top of the screen regardless of which app you are currently in. When Claude is running a long operation and you want to check something without losing your editor, this saves you from switching apps back and forth. No other terminal has this built in.

Alacritty has no native splits or tabs, so you need tmux (explained in section 6) for everything. Kitty is powerful but has a large configuration surface that is tedious to get started with. WezTerm is configured in Lua, a full programming language, which is overkill if you just want a terminal that works well. Ghostty’s plain text config is a genuine competitive advantage.

2. Installation

macOS: if you have Homebrew installed (the standard macOS package manager), run:

brew install --cask ghostty

If you do not have Homebrew, install it first from brew.sh, then run the command above.

Linux (Debian/Ubuntu):

curl -LO https://github.com/ghostty-org/ghostty/releases/latest/download/ghostty_linux_x86_64.tar.gz
tar -xzf ghostty_linux_x86_64.tar.gz
sudo mv ghostty /usr/local/bin/ghostty

Once installed, open Ghostty. The config file lives at ~/.config/ghostty/config on both platforms, where the tilde (~) is shorthand for your home directory. The file does not exist yet; the bootstrap script in the next section creates it.

2.1 Make Ghostty Your Default Terminal

Yes, I know this is premature, but I promise when your done reading this post – you will be running this script:

cat > setup-ghostty-claude.sh << 'OUTEREOF'
#!/usr/bin/env bash
#
# setup-ghostty-claude.sh
#
# Adds a Finder right-click Quick Action: "Launch Claude Code in Ghostty".
# Right-click any folder -> Quick Actions -> Launch Claude Code in Ghostty,
# and a new Ghostty window opens in that folder running Claude Code.
#
# macOS only. Requires Ghostty (https://ghostty.org) and Claude Code.
#
# Run it once from a terminal:
#   bash setup-ghostty-claude.sh
#
set -euo pipefail

SERVICE_NAME="Launch Claude Code in Ghostty"
SERVICE_PATH="${HOME}/Library/Services/${SERVICE_NAME}.workflow"
LAUNCHER="${HOME}/.local/bin/ghostty-claude-launcher"

# ── 1. Sanity checks ──────────────────────────────────────────────────────────
if [[ "$(uname)" != "Darwin" ]]; then
  echo "Error: this script is macOS only." >&2
  exit 1
fi
if ! osascript -e 'id of application "Ghostty"' >/dev/null 2>&1; then
  echo "Error: Ghostty is not installed. Get it from https://ghostty.org" >&2
  exit 1
fi

# ── 2. Launcher script ────────────────────────────────────────────────────────
# Locates the claude binary, cd's into the selected folder, and runs Claude Code.
# Kept as a separate file so the Quick Action command stays a one-liner.
mkdir -p "$(dirname "$LAUNCHER")"
cat > "$LAUNCHER" << 'LAUNCHER_EOF'
#!/bin/bash
# Source login files so `claude` is on PATH no matter how we were launched.
for rc in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.profile"; do
  [ -f "$rc" ] && source "$rc" 2>/dev/null || true
done

CLAUDE="$(command -v claude 2>/dev/null || true)"
if [ -z "$CLAUDE" ]; then
  for candidate in \
    "$HOME/.local/bin/claude" \
    "$HOME/.npm-global/bin/claude" \
    "/opt/homebrew/bin/claude" \
    "/usr/local/bin/claude" \
    "/usr/bin/claude"; do
    if [ -x "$candidate" ]; then CLAUDE="$candidate"; break; fi
  done
fi
if [ -z "$CLAUDE" ]; then
  osascript -e 'display alert "Claude Code not found" message "Install Claude Code, or make sure the claude command is on your PATH."'
  exit 1
fi

# First argument is the folder Finder passed in.
FOLDER="$1"
if [ -n "$FOLDER" ] && [ -d "$FOLDER" ]; then
  cd "$FOLDER"
fi

# caffeinate -i keeps the Mac awake while a session is running.
caffeinate -i "$CLAUDE"
LAUNCHER_EOF
chmod +x "$LAUNCHER"

# ── 3. Build the Automator Quick Action (Service) ─────────────────────────────
# The plists are generated with Python's plistlib so the embedded shell command
# is XML-escaped correctly. (Hand-writing the XML breaks the moment the command
# contains <, > or &.)
rm -rf "$SERVICE_PATH"
mkdir -p "$SERVICE_PATH/Contents"

python3 - "$SERVICE_PATH/Contents/Info.plist" "$SERVICE_NAME" << 'PYEOF'
import plistlib, sys
path, name = sys.argv[1], sys.argv[2]
plist = {
    "NSServices": [{
        "NSMenuItem": {"default": name},
        "NSMessage": "runWorkflowAsService",
        "NSRequiredContext": {"NSApplicationIdentifier": "com.apple.finder"},
        "NSSendFileTypes": ["public.folder"],
    }]
}
with open(path, "wb") as f:
    plistlib.dump(plist, f)
PYEOF

python3 - "$SERVICE_PATH/Contents/document.wflow" "$LAUNCHER" << 'PYEOF'
import plistlib, sys
path, launcher = sys.argv[1], sys.argv[2]

# macOS note: you cannot launch the Ghostty terminal via its binary directly,
# and `+new-window` is Linux-only. The supported path is:
#   open -na Ghostty.app --args ... -e <command>
# Flags passed to this instance:
#   --window-save-state=never        open a clean window; do NOT restore the
#                                    user's previous tabs/session into it
#   --quit-after-last-window-closed=true  quit (and drop the Dock icon) when the
#                                    window closes, so instances don't pile up
command = (
    'for folder in "$@"; do
'
    '  [ -d "$folder" ] || continue
'
    '  open -na Ghostty.app --args --window-save-state=never '
    '--quit-after-last-window-closed=true -e "'
    + launcher + '" "$folder" &
'
    'done
'
)

wflow = {
    "AMApplicationBuild": "521.1",
    "AMApplicationVersion": "2.10",
    "AMDocumentVersion": "2",
    "actions": [{
        "action": {
            "AMAccepts": {"Container": "List", "Optional": True,
                          "Types": ["com.apple.cocoa.path"]},
            "AMActionVersion": "2.0.3",
            "AMApplication": ["Automator"],
            "ActionBundlePath": "/System/Library/Automator/Run Shell Script.action",
            "ActionName": "Run Shell Script",
            "ActionParameters": {
                "COMMAND_STRING": command,
                "CheckedForUserDefaultShell": True,
                "inputMethod": 1,
                "shell": "/bin/bash",
                "source": "",
            },
            "BundleIdentifier": "com.apple.RunShellScript",
            "CFBundleVersion": "2.0.3",
            "CanShowSelectedItemsWhenRun": False,
            "CanShowWhenRun": True,
            "Category": ["AMCategoryUtilities"],
            "Class Name": "RunShellScriptAction",
            "InputUUID": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
            "OutputUUID": "B2C3D4E5-F6A7-8901-BCDE-F12345678901",
            "UUID": "C3D4E5F6-A7B8-9012-CDEF-123456789012",
            "UnlockPlugin": False,
            "arguments": {},
            "isViewVisible": 1,
            "location": "309.500000:253.000000",
            "nibPath": "/System/Library/Automator/Run Shell Script.action/Contents/Resources/English.lproj/main.nib",
        },
        "isViewVisible": 1,
    }],
    "connectors": {},
    "workflowMetaData": {
        "serviceApplicationBundleID": "com.apple.finder",
        "serviceApplicationPath": "/System/Library/CoreServices/Finder.app",
        "serviceInputTypeIdentifier": "com.apple.Automator.fileSystemObject.folder",
        "serviceOutputTypeIdentifier": "com.apple.Automator.nothing",
        "serviceProcessesInput": 0,
        "workflowTypeIdentifier": "com.apple.Automator.servicesMenu",
    },
}
with open(path, "wb") as f:
    plistlib.dump(wflow, f)
PYEOF

# ── 4. Register the service ───────────────────────────────────────────────────
/System/Library/CoreServices/pbs -update || true
killall Finder >/dev/null 2>&1 || true

cat << EOF

Done.

Right-click any folder in Finder ->
  Quick Actions (or Services) -> ${SERVICE_NAME}

To uninstall:
  rm -rf "${SERVICE_PATH}"
  rm -f "${LAUNCHER}"
  /System/Library/CoreServices/pbs -update && killall Finder
EOF
OUTEREOF
chmod +x setup-ghostty-claude.sh

2.1.1 What it does

  • Verifies Ghostty is installed
  • Installs duti (via Homebrew) if needed
  • Sets Ghostty as the default handler for .command shell scripts
  • Sets Ghostty as the default handler for .term Terminal.app session documents
  • Provides VS Code configuration for external terminal integration EOF chmod +x ~/ghostty-setup.md

3. Bootstrap Script

Before you do anything productive in Ghostty, three things need to be in place: the config file, a compatible shell prompt, and the right font.

Without the config file, Ghostty opens with defaults that are fine for general use but wrong for Claude Code. The scrollback history is too short, there are no split pane shortcuts, and window state is not saved between sessions.

Without a compatible shell prompt, Claude Code’s shell integration cannot reliably detect where your prompt ends and where command output begins. This causes parsing errors and, in some cases, Claude silently missing the output of commands it ran. The bootstrap script installs Starship, a fast and compatible prompt replacement. If you are currently using Powerlevel10k, do not skip this step; see section 4 for why P10k specifically breaks Claude Code.

Without a Nerd Font, certain icon characters that Claude Code’s interface and Starship use for git branch symbols, file type icons, and status indicators will render as blank boxes or question marks. The script installs JetBrains Mono Nerd Font, which covers all of them.

The script below handles all three in one paste. Copy the entire block, paste it into your terminal, and press Enter. It is safe to run more than once because it checks whether each thing is already installed before doing anything. After it finishes, follow the one manual step it prints at the end: adding the Starship initialisation line to your shell config file.

cat > ~/ghostty-bootstrap.sh << 'OUTER_EOF'
#!/usr/bin/env bash
set -euo pipefail

# 1. Ghostty config
mkdir -p ~/.config/ghostty

cat > ~/.config/ghostty/config << 'EOF'
# ghostty/config: Claude Code optimised configuration

# Typography. JetBrains Mono Nerd Font ships ligatures plus icon glyphs
# used by eza, Starship, and Claude Code's TUI for file types and git
# symbols.
font-family = JetBrainsMonoNerdFont
font-size = 14
font-thicken = true
adjust-cell-height = 2

# Theme. Auto light or dark follows the OS appearance setting, so
# switching the Mac to dark mode changes Ghostty without restarting.
# background-opacity lets you read background windows during long
# Claude runs without switching away. The blur keeps it from being
# distracting.
theme = light:Catppuccin Latte,dark:Catppuccin Mocha
background-opacity = 0.9
background-blur-radius = 20
window-padding-x = 10
window-padding-y = 8
window-theme = auto

# Window state. Restores all splits and tabs on reopen, so your Claude
# Code layout is waiting for you the next morning.
window-save-state = always

# Cursor. A bar is less distracting than a block when reading long
# output.
cursor-style = bar
cursor-style-blink = true
cursor-opacity = 0.8

# Mouse. Hide the cursor while typing so it does not overlap Claude's
# output stream. copy-on-select puts selected text into the clipboard
# immediately, which matters when grabbing file paths or commands.
mouse-hide-while-typing = true
copy-on-select = clipboard

# Shell integration. Gives Ghostty semantic knowledge of your prompt:
# working directory tracking across splits, better scrollback
# navigation.
shell-integration = detect
shell-integration-features = cursor,sudo,title

# Performance. Claude Code generates long outputs. 100000 lines is
# generous. Disable the resize overlay; it flickers on fast redraws.
scrollback-limit = 100000
resize-overlay = never

# Split keybindings. SAND pattern (Split, Across, Navigate, Destroy).
# super+d splits right. super+shift+d splits down. super+w closes a
# pane. ctrl with vim keys navigates between panes.
keybind = super+d=new_split:right
keybind = super+shift+d=new_split:down
keybind = super+w=close_surface
keybind = ctrl+h=goto_split:left
keybind = ctrl+l=goto_split:right
keybind = ctrl+k=goto_split:top
keybind = ctrl+j=goto_split:bottom

# Tab keybindings
keybind = super+t=new_tab
keybind = super+1=goto_tab:1
keybind = super+2=goto_tab:2
keybind = super+3=goto_tab:3
keybind = super+4=goto_tab:4
keybind = super+5=goto_tab:5

# Quick terminal. Global hotkey that drops from the top of the screen
# over whatever app is focused. 150ms animation keeps it snappy.
keybind = global:super+grave_accent=toggle_quick_terminal
quick-terminal-position = top
quick-terminal-screen = main
quick-terminal-animation-duration = 0.15
EOF
echo "Ghostty config written to ~/.config/ghostty/config"

# 2. Starship prompt
if ! command -v starship &> /dev/null; then
  echo "Installing Starship..."
  mkdir -p "$HOME/.local/bin"
  curl -sS https://starship.rs/install.sh | BIN_DIR="$HOME/.local/bin" sh -s -- --yes
else
  echo "Starship already installed."
fi

mkdir -p ~/.config

cat > ~/.config/starship.toml << 'EOF'
format = """
$directory$git_branch$git_status$nodejs$python$rust$golang$java
$character"""

[character]
success_symbol = "[❯](bold green)"
error_symbol = "[❯](bold red)"

[directory]
truncation_length = 3
truncate_to_repo = true

[git_branch]
symbol = " "
format = "[$symbol$branch]($style) "
style = "bold purple"

[git_status]
format = '([\[$all_status$ahead_behind\]]($style) )'
style = "bold red"

[nodejs]
symbol = " "
format = "[$symbol($version)]($style) "

[python]
symbol = " "
format = "[$symbol($version)]($style) "
EOF
echo "Starship config written to ~/.config/starship.toml"
echo ""
echo "MANUAL STEP REQUIRED:"
echo "Add these two lines to the bottom of your ~/.zshrc on macOS or"
echo "your ~/.bashrc on Linux:"
echo ""
echo '  export PATH="$HOME/.local/bin:$PATH"'
echo '  eval "$(starship init zsh)"'
echo ""
echo "Then run: source ~/.zshrc"

# 3. JetBrains Mono Nerd Font
if [[ "$(uname)" == "Darwin" ]]; then
  # On macOS we check the user and system Fonts directories directly
  # rather than relying on fc-list, which is not installed by default.
  if ls "$HOME/Library/Fonts"/JetBrainsMono*.ttf >/dev/null 2>&1 \
     || ls /Library/Fonts/JetBrainsMono*.ttf >/dev/null 2>&1; then
    echo "JetBrains Mono Nerd Font already installed."
  else
    brew install --cask font-jetbrains-mono-nerd-font \
      || echo "Font install failed. Download manually from https://www.nerdfonts.com/font-downloads"
  fi
elif [[ "$(uname)" == "Linux" ]]; then
  FONT_DIR="${HOME}/.local/share/fonts"
  mkdir -p "${FONT_DIR}"
  if ! fc-list 2>/dev/null | grep -qi "JetBrainsMono"; then
    echo "Downloading JetBrains Mono Nerd Font..."
    TMP=$(mktemp -d)
    curl -Lo "${TMP}/JetBrainsMono.zip" \
      "https://github.com/ryanoasis/nerd-fonts/releases/latest/download/JetBrainsMono.zip"
    unzip -q "${TMP}/JetBrainsMono.zip" -d "${FONT_DIR}"
    fc-cache -fv "${FONT_DIR}" > /dev/null 2>&1
    rm -rf "${TMP}"
    echo "JetBrains Mono Nerd Font installed."
  else
    echo "JetBrains Mono Nerd Font already installed."
  fi
fi

echo ""
echo "Bootstrap complete. Restart Ghostty to pick up the new config."
echo ""
echo "Some Ghostty config keys used here (font-thicken, cursor-opacity,"
echo "quick-terminal-screen) require Ghostty 1.0 or newer. Earlier"
echo "builds will print warnings for those lines and ignore them."
OUTER_EOF
chmod +x ~/ghostty-bootstrap.sh

4. The Config Explained

The config the bootstrap script writes is not just aesthetic preferences. Each setting either directly affects how Claude Code behaves inside Ghostty or eliminates a category of friction that accumulates over a long agentic session. The things that look like minor tweaks such as opacity, cursor style, and copy behaviour are the ones you stop noticing after a day because they quietly remove small interruptions that were costing you attention.

Why Starship and not Powerlevel10k: if you are already using Powerlevel10k as your shell prompt, you need to replace it. P10k has a feature called instant prompt that caches a rendered prompt before the shell finishes initialising. This is clever for human interactive use because it eliminates the half-second startup pause, but Claude Code probes your shell on launch to detect where prompts are and understand when commands have finished running. P10k’s cached prompt fires before that detection completes, which causes Claude Code to time out, produce parsing errors, and in some cases fail to detect that a command has finished at all. Starship has no instant prompt feature and has no such conflict.

window-save-state = always is the setting I would refuse to give up. When you close Ghostty and reopen it, every split pane and tab is restored exactly as you left it, with your Claude Code session ready, the git pane open, and the shell sitting in the project directory. When you are working on a problem that spans hours or days, a terminal that rebuilds your environment every morning is friction you do not need.

copy-on-select = clipboard means that any text you highlight with your mouse is immediately copied to the clipboard without needing to press cmd+c. Claude Code constantly surfaces file paths, function names, suggested commands, and error strings, and being able to highlight and immediately paste keeps your hands on the keyboard and your focus on the agent output.

background-opacity = 0.9 makes the terminal window ten percent transparent. When Claude is running an operation that takes several seconds, you can read a background window such as documentation or a file in your editor without switching away. The 20-pixel blur setting keeps the background soft enough that it does not compete with the terminal content.

The SAND keybindings are the split pane shortcuts configured in this file. SAND stands for Split, Across, Navigate, Destroy and covers the four things you do with panes. super+d splits the current pane to the right, super+shift+d splits it downward, ctrl+h/j/k/l moves focus between panes in Vim directions (h=left, j=down, k=up, l=right), and super+w closes the current pane. The pattern was published by Dani Avila and it is the cleanest split management system available in any terminal.

5. Three Layouts for Different Work Modes

A split layout is a way of dividing your terminal window into multiple panes so you can see Claude Code and a shell at the same time without switching between windows. Each pane is independent, so you can run different commands in each one and they do not interfere with each other.

The config from section 3 sets up the keyboard shortcuts, and this section shows you how to use them to build the three layouts I use most.

5.1 Standard Split

The layout I use for most sessions has Claude Code on the left at roughly two-thirds of the window and a shell on the right for running tests, checking git history, and reviewing anything before handing it to Claude.

┌──────────────────────────┬───────────────┐
│                          │               │
│      Claude Code         │  Shell / Git  │
│        (70%)             │    (30%)      │
│                          │               │
└──────────────────────────┴───────────────┘

To build it:

  1. Open Ghostty. You start with a single full-width pane.
  2. Press super+d (cmd+d on Mac, Win+d on Linux). Ghostty splits the window to the right and your cursor lands in the new right pane. Press cmd+w to shut the window.
  3. In the right pane, run whatever shell commands you need such as git log or ls.
  4. Press ctrl+h to move focus back to the left pane.
  5. In the left pane, run claude.

Navigate between panes at any time with ctrl+h to go left and ctrl+l to go right. Close either pane with super+w.

5.2 Three-Pane Neovim Layout

This layout places Claude Code alongside a text editor and a dedicated shell at the bottom for test output and logs. Neovim is a terminal-based text editor; if you do not use it, substitute any editor that can open from the command line, or simply use the bottom pane as a second shell.

┌────────────────────┬────────────────────┐
│                    │                    │
│    Claude Code     │      Neovim        │
│      (50%)         │       (50%)        │
│                    │                    │
├────────────────────┴────────────────────┤
│              Shell / Test Output        │
└─────────────────────────────────────────┘

To build it:

  1. Open Ghostty. Single full-width pane, cursor is here.
  2. Press super+shift+d. Ghostty splits the window downward and your cursor moves to the new bottom pane.
  3. The bottom pane will be your shell. Leave it at a prompt for now.
  4. Press ctrl+k to move focus back up to the top pane.
  5. Press super+d. Ghostty splits the top pane to the right and your cursor moves to the new top-right pane.
  6. In the top-right pane, run nvim . or your editor of choice.
  7. Press ctrl+h to move focus to the top-left pane.
  8. In the top-left pane, run claude.

Navigate with ctrl+h/j/k/l in any direction. ctrl+j drops focus to the bottom shell and ctrl+k brings it back up.

5.3 Multi-Project Tab Layout

This layout is for working across two repositories at the same time. Each tab is fully independent with its own shell environment, its own Claude Code session, and no shared state between them.

[ Tab 1: Project A ] [ Tab 2: Project B ] [ Tab 3: Scratchpad ]

To build it:

  1. Open Ghostty. You are in Tab 1.
  2. cd into your first project directory and run claude. Optionally press super+d to add a shell pane on the right.
  3. Press super+t to open Tab 2. Ghostty creates a fresh tab and focuses it.
  4. cd into your second project and run claude. Add a shell pane with super+d if you want one.
  5. Press super+t again for Tab 3 and use this as a general scratchpad shell.

Jump between tabs with super+1, super+2, and super+3. Each tab remembers its own pane layout, and because window-save-state = always is in the config, all three tabs with their layouts and working directories are restored the next time you open Ghostty.

6. tmux Script

Ghostty’s native splits are excellent for local work. The moment you move to SSH, running Claude Code on a remote server, a homelab machine, or a cloud dev environment, they stop helping you because if your connection drops the panes disappear and whatever Claude was doing is gone.

tmux (short for terminal multiplexer) solves this. It is a program that runs sessions inside the remote machine rather than inside your terminal window. Your terminal is just a viewport into the session, so when you drop the connection and reconnect the session is exactly where you left it, including any Claude Code operation that was mid-task. Locally, tmux is also useful if you want Claude Code to keep running after you close your laptop lid.

If you have never used tmux before, here is the mental model. A tmux server runs silently in the background, and you create named sessions inside it. Each session has windows, and each window can have panes. You attach your terminal to a session to see it and detach without killing anything, and the session keeps running whether you are attached or not.

Essential tmux commands to run in any terminal:

tmux ls                              List all running sessions
tmux attach-session -t claude        Attach to a session named "claude"
tmux new-session -s myproject        Create a new session named "myproject"
tmux kill-session -t claude          Kill a session entirely

Inside a tmux session, ctrl+b is the prefix key. Press it, release it, then press the second key:

ctrl+b then d       Detach from the session (leaves it running in the background)
ctrl+b then $       Rename the current session
ctrl+b then c       Create a new window
ctrl+b then n       Switch to the next window
ctrl+b then p       Switch to the previous window
ctrl+b then %       Split the pane left and right
ctrl+b then "       Split the pane top and bottom
ctrl+b then arrow   Move between panes
ctrl+b then z       Zoom the current pane to full screen (press again to shrink back)

There is a catch specific to Claude Code. By default, tmux intercepts certain signals before they reach Claude Code, and two things break as a result. Desktop notifications never reach your OS because tmux swallows them, and shift+enter becomes indistinguishable from plain Enter, so Claude submits when you meant to continue typing. Both are fixed with three lines added to ~/.tmux.conf, which is tmux’s config file, and the script below adds them automatically.

What the script does: it writes itself to ~/ghostty-tmux-claude.sh, first checks that tmux is installed and installs it via Homebrew if not, patches ~/.tmux.conf with the three fix lines only if they are not already present, then creates a named tmux session with Claude Code running in a left pane at 65% width and a shell in the right pane. If a session with the same name already exists it attaches to it instead of creating a duplicate.

Paste this once to create the script:

cat > ~/ghostty-tmux-claude.sh << 'OUTER_EOF'
#!/usr/bin/env bash
# Usage: ./ghostty-tmux-claude.sh [session-name] [project-dir]
#
# The first argument is the tmux session name and defaults to "claude".
# Two readers running this script back to back with no argument will
# share the same session name, which is usually what you want for a
# personal machine but worth knowing about.
set -euo pipefail

SESSION="${1:-claude}"
PROJECT_DIR="${2:-${PWD}}"

# 1. Dependencies
if ! command -v tmux &>/dev/null; then
  if ! command -v brew &>/dev/null; then
    echo "Error: tmux and Homebrew are both missing. Install Homebrew first:" >&2
    echo "  https://brew.sh" >&2
    exit 1
  fi
  echo "tmux not found. Installing via Homebrew..."
  brew install tmux
fi

# 2. tmux passthrough config
# allow-passthrough lets Claude Code notifications reach the outer
# terminal instead of being swallowed by tmux.
# extended-keys lets tmux distinguish Shift+Enter from plain Enter so
# the newline shortcut works correctly inside Claude Code.
if ! grep -qF "Claude Code passthrough" ~/.tmux.conf 2>/dev/null; then
  cat >> ~/.tmux.conf << 'TMUXEOF'

# Claude Code passthrough (added by ghostty-tmux-claude.sh)
set -g allow-passthrough on
set -s extended-keys on
set -as terminal-features 'xterm*:extkeys'
TMUXEOF
fi

# Reload only if a server is already running
if tmux list-sessions &>/dev/null; then
  tmux source-file ~/.tmux.conf 2>/dev/null || true
fi

# 3. Session setup
if tmux has-session -t "${SESSION}" 2>/dev/null; then
  echo "Session '${SESSION}' already exists. Attaching."
  tmux attach-session -t "${SESSION}"
  exit 0
fi

tmux new-session -d -s "${SESSION}" -c "${PROJECT_DIR}"

# Left pane: Claude Code
tmux send-keys -t "${SESSION}:0" "claude" Enter

# Right pane: shell
tmux split-window -h -t "${SESSION}:0" -c "${PROJECT_DIR}"
tmux send-keys -t "${SESSION}:0.1" "clear" Enter

# Resize Claude Code to 65% of the window width. The percentage form
# requires tmux 3.1 or newer; on older tmux this resize is a no op
# and panes end up at the default 50% split.
TMUX_MAJOR=$(tmux -V | awk '{print $2}' | cut -d. -f1)
TMUX_MINOR=$(tmux -V | awk '{print $2}' | cut -d. -f2 | tr -dc '0-9')
if [ "${TMUX_MAJOR:-0}" -gt 3 ] || { [ "${TMUX_MAJOR:-0}" -eq 3 ] && [ "${TMUX_MINOR:-0}" -ge 1 ]; }; then
  tmux resize-pane -t "${SESSION}:0.0" -x "65%"
else
  # Fall back to an absolute column count that approximates 65% on a
  # typical 200 column window.
  tmux resize-pane -t "${SESSION}:0.0" -x 130
fi

tmux select-pane -t "${SESSION}:0.0"
tmux set-option -t "${SESSION}" history-limit 100000

tmux attach-session -t "${SESSION}"
OUTER_EOF
chmod +x ~/ghostty-tmux-claude.sh

Then use it like this:

# Start a session in your current directory (the session will be named "claude")
./ghostty-tmux-claude.sh

# Start a session with a specific name and project directory
./ghostty-tmux-claude.sh myproject ~/code/myproject

# If you close Ghostty or get disconnected, reattach with:
tmux attach-session -t myproject

When the script runs you land inside a tmux session with Claude Code already started on the left and a plain shell on the right. You are now inside tmux, which means pressing ctrl+b then d detaches you cleanly without stopping anything, and running tmux attach-session -t myproject from any terminal window brings you straight back.

One thing that surprises first-time tmux users is that closing the Ghostty window does not kill the session. The tmux server keeps running in the background, so you can reopen Ghostty, run tmux attach-session -t myproject, and find Claude exactly where you left it.

To move focus to the right pane:

Ctrl+b then →

or

Ctrl+b then o

to cycle between panes. Just type exit or Ctrl+D in the tmux session to detach/close it, then you’ll be back at a plain shell in Ghostty.

Here is my typical layout:

Ghostty terminal split pane layout configured for Claude Code agentic workflow

7. Claude Code Settings Script

Claude Code has its own settings file at ~/.claude/settings.json that is completely separate from Ghostty’s config. By default, Claude Code makes conservative assumptions about the terminal it is running in and does not know you are in Ghostty, does not know your OS theme, and does not know how you want notifications delivered. Left at defaults, you get a colour scheme that may clash with your Ghostty theme and notifications that either go nowhere or only appear inside the Claude Code interface where you might miss them.

Two settings matter here. theme: auto tells Claude Code to detect whether Ghostty is currently in light or dark mode and match its own colour scheme accordingly. Because Ghostty’s window-theme = auto already tracks your OS appearance, setting this means both Ghostty and Claude Code switch together when you change your desktop theme and you never need to manually toggle Claude Code’s colours.

preferredNotifChannel: ghostty tells Claude Code to deliver task completion and permission-request notifications to the OS notification centre, the same place where calendar reminders and messages appear, rather than only showing them inside the terminal interface. Ghostty forwards these natively, so you get a real desktop notification even when you are in a different application. The hook at the bottom of the settings file adds an audio cue so that a single system sound plays when Claude finishes a task and you hear it without having to watch the terminal.

The script backs up any existing settings.json before overwriting it and detects whether you are on macOS or Linux to set the correct sound command.

cat > ~/ghostty-claude-settings.sh << 'OUTER_EOF'
#!/usr/bin/env bash
set -euo pipefail

mkdir -p ~/.claude

# 1. Back up any existing settings with a timestamp plus PID suffix so
# running this script twice in the same second cannot clobber a backup.
if [[ -f ~/.claude/settings.json ]]; then
  BACKUP=~/.claude/settings.json.bak.$(date +%Y%m%d%H%M%S).$$
  cp ~/.claude/settings.json "$BACKUP"
  echo "Existing settings backed up to $BACKUP"

  # Warn if the existing settings were not valid JSON so the reader
  # knows their original was already broken before we touched it.
  if ! python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$BACKUP" 2>/dev/null; then
    echo "  Note: the previous settings.json was not valid JSON."
  fi
fi

# 2. Platform sound command
if [[ "$(uname)" == "Darwin" ]]; then
  NOTIFY_CMD="afplay /System/Library/Sounds/Glass.aiff"
else
  NOTIFY_CMD="paplay /usr/share/sounds/freedesktop/stereo/complete.oga 2>/dev/null || true"
fi

# 3. Write the new settings. The hooks block uses the current Claude
# Code schema with a matcher field alongside hooks. The matcher is an
# empty string here because we want every Notification event to fire
# the sound, not a subset.
cat > ~/.claude/settings.json << SETTINGSEOF
{
  "theme": "auto",
    "preferredNotifChannel": "ghostty",
    "syntaxHighlightingDisabled": true,
    "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "${NOTIFY_CMD}"
          }
        ]
      }
    ]
  }
}
SETTINGSEOF

echo "Claude Code settings written to ~/.claude/settings.json"
echo "  theme: auto, follows Ghostty's light or dark mode"
echo "  notifications: system, forwarded to OS notification centre by Ghostty"
echo "  sound: ${NOTIFY_CMD}"
OUTER_EOF
chmod +x ~/ghostty-claude-settings.sh

Run it with ./ghostty-claude-settings.sh. No restart is required because Claude Code reads settings.json each time it starts.

8. Quick Reference

Once everything is configured, you should not need to think about the terminal at all. These are the only keyboard shortcuts you will actually use day to day.

A reminder: super is the cmd key on Mac and the Win key on Linux. The ctrl+h/j/k/l navigation uses Vim-style directions where h means left, j means down, k means up, and l means right.

Ghostty pane and tab shortcuts:

super+d             Split current pane to the right (creates a new pane alongside)
super+shift+d       Split current pane downward (creates a new pane below)
super+w             Close the current pane
ctrl+h              Move focus to the pane on the left
ctrl+l              Move focus to the pane on the right
ctrl+k              Move focus to the pane above
ctrl+j              Move focus to the pane below
super+t             Open a new tab
super+1             Switch to Tab 1
super+2             Switch to Tab 2
super+3             Switch to Tab 3 (and so on up to 5)
super+`             Toggle the drop-down quick terminal (works from any app)

Inside Claude Code:

shift+enter         Insert a new line without submitting (works natively in Ghostty)
ctrl+j              Insert a new line (works everywhere including inside tmux)
shift+tab twice     Switch to Plan Mode (Claude plans before acting)
/theme              Change Claude Code's colour theme to match Ghostty
/terminal-setup     Not needed in Ghostty because shift+enter works without it

9. One Last Thing

window-save-state = always is the setting I keep coming back to. Open Ghostty in the morning and your panes are exactly where you left them, with the Claude Code session ready, the git pane open, and the shell sitting in the project directory. The environment picks up where you stopped.

When you are working at the Claude Code level of abstraction, you are not just writing code. You are directing an agent through a problem that might span hours, and a terminal that rebuilds your workspace every session is friction you are paying with attention. This setting eliminates it.

Run the bootstrap script, run the settings script, restart Ghostty, and you are done.

10. Final Final Thing

I cannot see the cursor in Ghostty, so if you have a similar issue run the script below (it was surprisingly difficult to fix).

cat > setup-ghostty-cursor.sh << 'EOF'
#!/usr/bin/env zsh
# setup-ghostty-cursor.sh
# Fixes the "cursor disappears after exiting Claude Code" problem in Ghostty.
# Safe to re-run — all changes are idempotent.
set -e

echo "==> Ghostty cursor fix setup"

# ── 1. Ghostty config ────────────────────────────────────────────────────────
GHOSTTY_CONFIG="$HOME/.config/ghostty/config"
mkdir -p "$(dirname "$GHOSTTY_CONFIG")"

apply_ghostty_setting() {
  local key="$1" value="$2"
  if grep -q "^${key} *=" "$GHOSTTY_CONFIG" 2>/dev/null; then
    sed -i '' "s|^${key} *=.*|${key} = ${value}|" "$GHOSTTY_CONFIG"
  else
    echo "${key} = ${value}" >> "$GHOSTTY_CONFIG"
  fi
}

echo "--> Patching Ghostty config: $GHOSTTY_CONFIG"
apply_ghostty_setting "cursor-style"       "block"
apply_ghostty_setting "cursor-style-blink" "false"
apply_ghostty_setting "cursor-color"       "#0055ff"
apply_ghostty_setting "cursor-text"        "#ffffff"

if grep -q "^shell-integration-features" "$GHOSTTY_CONFIG" 2>/dev/null; then
  sed -i '' 's/^shell-integration-features = .*/shell-integration-features = sudo,title/' "$GHOSTTY_CONFIG"
else
  echo "shell-integration-features = sudo,title" >> "$GHOSTTY_CONFIG"
fi
echo "    Ghostty config updated."

# ── 2. ~/.zshrc cursor hooks ─────────────────────────────────────────────────
ZSHRC="$HOME/.zshrc"
MARKER="# [cursor-fix] Force steady block cursor"

if grep -q "$MARKER" "$ZSHRC" 2>/dev/null; then
  echo "--> ~/.zshrc already patched, skipping."
else
  echo "--> Adding cursor hooks to $ZSHRC"
  printf '%s
' \
    '' \
    '# [cursor-fix] Force steady block cursor — fixes invisible cursor after exiting Claude Code' \
    '# precmd fires before each prompt; zle-line-init fires just before ZLE accepts input' \
    '# (after Ghostty shell integration), so the block cursor is guaranteed.' \
    '_reset_cursor_block() { printf $'"'"'\033[2 q'"'"' }' \
    'autoload -Uz add-zsh-hook' \
    'add-zsh-hook precmd _reset_cursor_block' \
    'zle-line-init() { printf $'"'"'\033[2 q'"'"' }' \
    'zle -N zle-line-init' >> "$ZSHRC"
  echo "    ~/.zshrc patched."
fi

# ── 3. claude() wrapper ───────────────────────────────────────────────────────
if grep -q "^claude ()" "$ZSHRC" 2>/dev/null || grep -q "^claude()" "$ZSHRC" 2>/dev/null; then
  if grep -q "printf.*033.*2 q" "$ZSHRC" 2>/dev/null; then
    echo "--> claude() wrapper already has cursor reset."
  else
    echo "--> NOTE: You have a claude() wrapper in ~/.zshrc but it lacks the cursor reset line."
    echo "    Add  printf \$'\033[2 q'  after 'command claude \"\$@\"' in your wrapper."
  fi
fi

# ── 4. Done ───────────────────────────────────────────────────────────────────
echo ""
echo "Done. Next steps:"
echo "  1. Fully quit Ghostty (Cmd+Q) and relaunch — config changes need a restart."
echo "  2. Open a fresh terminal tab, run 'claude', then exit."
echo "  3. Cursor should remain a solid blue block."
echo ""
echo "If cursor is still a bar: run 'printf \$'\033[2 q'' manually to confirm the"
echo "escape sequence works in your terminal, then check 'echo \$GHOSTTY_SHELL_FEATURES'."
EOF
chmod +x setup-ghostty-cursor.sh

Posted on andrewbaker.ninja. More at @futureherman on Substack.