Skip to content

Instantly share code, notes, and snippets.

@grmkris
Created February 27, 2026 11:00
Show Gist options
  • Select an option

  • Save grmkris/85a9b8b0cbdffaa752d2fcc4ae619dcd to your computer and use it in GitHub Desktop.

Select an option

Save grmkris/85a9b8b0cbdffaa752d2fcc4ae619dcd to your computer and use it in GitHub Desktop.
Claude Code Hooks + tmux pane auto-focus setup

Claude Code Hooks + tmux Pane Focus Setup

Auto-focus the correct terminal pane when Claude Code finishes a task or needs your input. Shows macOS notifications with sound.

Prerequisites

  • Claude Code CLI installed
  • jq installed (brew install jq)
  • macOS (uses osascript for notifications)

Step 1: Create the activation script

mkdir -p ~/.claude
cat > ~/.claude/activate-terminal.sh << 'SCRIPT'
#!/bin/bash
detect_terminal() {
  [ -n "$GHOSTTY_RESOURCES_DIR" ] && echo "Ghostty" && return
  [ -n "$ITERM_SESSION_ID" ] && echo "iTerm2" && return
  [ -n "$WEZTERM_PANE" ] && echo "WezTerm" && return
  [ -n "$KITTY_PID" ] && echo "kitty" && return

  case "${TERM_PROGRAM:-}" in
    ghostty) echo "Ghostty" && return ;;
    iTerm.app) echo "iTerm2" && return ;;
    WezTerm) echo "WezTerm" && return ;;
    Apple_Terminal) echo "Terminal" && return ;;
  esac

  # Inside tmux: walk up process tree to find parent terminal
  if [ -n "$TMUX" ]; then
    pid=$$
    for _ in {1..15}; do
      pid=$(ps -p "$pid" -o ppid= 2>/dev/null | tr -d ' ')
      [ -z "$pid" ] || [ "$pid" = "1" ] && break
      name=$(ps -p "$pid" -o comm= 2>/dev/null)
      case "$name" in
        *Ghostty*) echo "Ghostty" && return ;;
        *iTerm*) echo "iTerm2" && return ;;
        *WezTerm*|*wezterm*) echo "WezTerm" && return ;;
        *kitty*) echo "kitty" && return ;;
        *Terminal*) echo "Terminal" && return ;;
      esac
    done
  fi

  echo "Ghostty"  # default fallback — change to your terminal
}

activate_iterm2() {
  # In tmux -CC mode, match by pane title (synced between tmux and iTerm2)
  if [ -n "$TMUX" ]; then
    local pane_title
    pane_title=$(tmux display-message -p '#{pane_title}' 2>/dev/null)
    if [ -n "$pane_title" ]; then
      osascript <<EOF
tell application "iTerm2"
  activate
  repeat with aWindow in windows
    repeat with aTab in tabs of aWindow
      repeat with aSession in sessions of aTab
        if name of aSession is "$pane_title" then
          select aWindow
          select aTab
          select aSession
          return
        end if
      end repeat
    end repeat
  end repeat
end tell
EOF
      return
    fi
  fi

  # Non-tmux: match by session UUID
  local session_uuid="${ITERM_SESSION_ID##*:}"
  if [ -n "$session_uuid" ]; then
    osascript <<EOF
tell application "iTerm2"
  activate
  repeat with aWindow in windows
    repeat with aTab in tabs of aWindow
      repeat with aSession in sessions of aTab
        if unique id of aSession is "$session_uuid" then
          select aWindow
          select aTab
          select aSession
          return
        end if
      end repeat
    end repeat
  end repeat
end tell
EOF
  else
    osascript -e 'tell application "iTerm2" to activate'
  fi
}

app=$(detect_terminal)

# For plain tmux (not iTerm2 -CC mode): focus the specific pane
if [ -n "$TMUX" ] && [ "$app" != "iTerm2" ]; then
  PANE_ID=$(tmux display-message -p '#{pane_id}' 2>/dev/null)
  [ -n "$PANE_ID" ] && tmux select-pane -t "$PANE_ID"
fi

case "$app" in
  iTerm2) activate_iterm2 ;;
  *) osascript -e "tell application \"$app\" to activate" 2>/dev/null ;;
esac
SCRIPT
chmod +x ~/.claude/activate-terminal.sh

Step 2: Add hooks to settings

Add the hooks block to ~/.claude/settings.json (create if it doesn't exist):

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "msg=$(jq -r '.message // \"Awaiting input\"'); osascript -e \"display notification \\\"$msg\\\" with title \\\"Claude Code\\\" sound name \\\"Ping\\\"\"; bash ~/.claude/activate-terminal.sh"
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "project=$(jq -r '.transcript_path | split(\"/\")[-2] // \"unknown\"'); osascript -e \"display notification \\\"Task completed\\\" with title \\\"Claude Code - $project\\\" sound name \\\"Glass\\\"\"; bash ~/.claude/activate-terminal.sh"
          }
        ]
      }
    ]
  }
}

If you already have a settings.json, merge the hooks key into it.

How it works

Event When it fires What happens
Notification Claude needs your input Ping sound + macOS notification + focuses terminal pane
Stop Claude finished its task Glass sound + notification with project name + focuses pane

The activation script:

  1. Detects which terminal app launched the shell (Ghostty, iTerm2, WezTerm, kitty, Terminal)
  2. For iTerm2 + tmux -CC mode: matches by pane title to select the exact iTerm2 session/tab
  3. For plain tmux: uses tmux select-pane to focus the correct pane
  4. Brings the terminal app to the foreground via AppleScript

Customization

Change default terminal: Edit the fallback echo "Ghostty" at the end of detect_terminal() to match your terminal.

Change notification sounds: Replace "Ping" or "Glass" with any sound from /System/Library/Sounds/ (e.g., "Hero", "Submarine", "Tink").

Add more hook events: Other useful events you can hook into:

  • PermissionRequest — Claude is waiting for a permission approval
  • SessionStart — inject context when a session begins
  • PreToolUse / PostToolUse — run commands before/after specific tools

See Claude Code Hooks docs for the full reference.

Verify it works

  1. Start Claude Code in a tmux pane
  2. Switch to a different pane or app
  3. Ask Claude something — when it finishes, your terminal should auto-focus and you'll hear the notification sound
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment