How I Know Which AI Agent Needs Me (and When)

tmux Silence Detection for Multi-Agent Workflows

How I Know Which AI Agent Needs Me (and When)

When you’re running 18 Claude Code agents across 6 tmux sessions, the biggest UX problem isn’t the AI–it’s knowing which one is waiting for you.

The Problem

I run a multi-agent Claude Code setup for development. Different agents handle different concerns–one leads infrastructure, another writes code, another manages job search prep, and a whole squad works on a Rails app. Each agent gets its own tmux window, organized into sessions by project: WertLead and WertCoder in one session, the Orthodox Daily squad in another, and so on.

The workflow is asynchronous by nature. I give an agent a task, switch to a different window, do something there, and at some point need to check back. But when? An agent might finish in 10 seconds or 10 minutes. Without cycling through every window manually, I don’t know who’s done and who’s still working.

I tried a few notification approaches before landing on something built entirely from what tmux already provides.

The Insight: Silence Means “Waiting”

Claude Code produces continuous output while it works–streaming text, tool call results, progress indicators. When it finishes and drops back to the input prompt, the pane goes silent. That silence is the signal.

tmux has a built-in feature for this: monitor-silence. Set a timeout on a window, and after N seconds of no output, tmux flags it and fires a hook. No polling. No custom watchers. No process inspection.

# In tmuxinator config, per agent window:
- WertLead:
    layout: main-vertical
    options:
      monitor-silence: 20
    panes:
      - claude-code-launch WertLead
      - yazi
      - zsh

20 seconds is the sweet spot. Short enough to catch idle agents quickly, long enough to avoid false positives during brief pauses in output (like when Claude thinks between tool calls).

The Visual: Gold Means “Come Talk to Me”

When a window’s silence timer fires, tmux sets an internal flag. There’s a style for flagged windows: window-status-activity-style. The default is reverse, which flips your foreground and background colors. It looks terrible.

Override it:

set -g window-status-activity-style "fg=colour220,nobold,noreverse"

Now any window waiting for input shows gold text in the status bar. The focused window gets a gold background with bold black text:

set-window-option -g window-status-current-style "bg=colour220,fg=#000000,bold"

Three states, one glance (the “Wertzy” window is a local Fizzy fork for kanban tracking–not an agent, just a neighbor):

  • Gold background, black bold text–the window you’re in
  • Gold text–agents waiting for your input
  • Gray text–agents still working (or non-agent windows like yazi, zsh)

tmux handles the flag lifecycle natively. Gold appears when silence is detected, disappears when the agent starts producing output again. No per-window style management, no cleanup scripts.

macOS Notifications for When You’re Elsewhere

The gold text only helps if you’re looking at the tmux status bar. When you’re in a browser, a design tool, or making lunch, a hook script pushes a macOS notification:

set -g silence-action any
set -g visual-silence off
set-hook -g alert-silence[0] 'run-shell -b "~/.config/agent-attention.sh \"#{hook_window_name}\" \"#{hook_session_name}\""'

The important settings:

  • silence-action any–fire the hook for all sessions, not just the one in view
  • visual-silence off–suppress tmux’s built-in message (we’re handling it ourselves)
  • -b on run-shell–non-blocking, so the script doesn’t freeze tmux

The notification script guards against non-agent windows, rate-limits to one notification per agent per 3 minutes, captures the last few lines of output for context, and calls osascript:

osascript -e "display notification \"${context}\" \
  with title \"Agent Ready\" \
  subtitle \"${display_name} @ ${session}\""

Rate limiting matters. Without it, every re-evaluation of the silence timer on an idle agent fires another notification. The approach is a state file per agent: touch it when notifying, check its mtime before the next one. Under 180 seconds? Skip.

Cleaning Up the Status Bar

monitor-silence sets a ~ flag on windows, and tmux’s default format renders all flags. Every monitored window gets a trailing ~, which is noisy. Override the format to show only what you actually use:

set -g window-status-format ' #I:#W#{?window_zoomed_flag,Z,} '
set -g window-status-current-format ' #I:#W#{?window_zoomed_flag,Z,} '

This keeps the zoom indicator (Z) and drops everything else. The gold color change IS our silence indicator–the ~ character is redundant noise.

Resetting When You Visit

When you switch to a waiting agent’s window, a hook clears the rate-limit state for that agent. If it goes idle again after your interaction, it can notify immediately instead of waiting out the old cooldown:

set-hook -g after-select-pane[0] 'run-shell -b "~/.config/agent-attention-reset.sh \"#{window_name}\" \"#{session_name}\""'
set-hook -g after-select-window[0] 'run-shell -b "~/.config/agent-attention-reset.sh \"#{window_name}\" \"#{session_name}\""'

Two hooks: after-select-pane fires when switching panes within a window, after-select-window fires when switching between windows. Both are needed to cover all navigation paths.

Things I Learned the Hard Way

window-status-activity-style defaults to reverse. If you’re setting per-window window-status-style and the colors look backwards, this is why. Set noreverse explicitly or you’ll spend an hour wondering why your foreground is your background.

alert-silence fires once per silence event, not on a loop. After the hook runs, it won’t fire again until the window produces output and then goes silent again. This is ideal–the system self-regulates. I wrote a bunch of deduplication logic before realizing tmux already handles it.

pane-focus-in is broken in tmux 3.5a. It accepts the set-hook command without error but never shows up in show-hooks output and never fires. Wasted an afternoon on that. Use after-select-pane and after-select-window instead.

Let tmux do the visual work. My first version tried to manage per-window styles from shell scripts–setting colors on notification, unsetting on reset. Race conditions, stale state, ugly edge cases. The final version uses zero per-window style management. window-status-activity-style handles everything through tmux’s native flag system. Fewer moving parts, no cleanup.

The Full Setup

Everything required:

  • 7 lines in .tmux.conf (settings + hooks)
  • One notification script (~30 lines)
  • One reset script (~10 lines)
  • monitor-silence: 20 in each agent window’s tmuxinator config

Full tmux view showing the agent attention system in action–gold-highlighted focused window, notification scripts visible, status bar with all agent windows

For 18 agents across 6 sessions, the difference was immediate. Instead of cycling through windows hunting for who needs me, I glance at the status bar or wait for a tap on the shoulder from macOS. Gold means “come talk to me.” Everything else means “still working, leave me alone.”

>_

For your agent

Everything above was written for you. This section is written for your AI coding agent.

Copy everything below into your agent’s context and ask it to add silence-based attention notifications to your tmux setup. The config, scripts, and hooks are structured so an agent can implement them directly.

Prerequisites

  • tmux 3.3+ installed (tmux -V to check; 3.5a confirmed working)
  • tmuxinator installed (gem install tmuxinator) with at least one agent config
  • A CLI-based AI agent running in tmux panes (Claude Code, Aider, Codex CLI, etc.)
  • macOS for the notification script (Linux users: replace osascript with notify-send)

Already doing this?

  • Already have monitor-silence set? Skip to the visual indicator config.
  • Already have gold styling? Skip to the notification script.
  • Already getting notifications but too many? Add the rate-limiting and reset hooks.
  • Already have everything but seeing ~ flags cluttering the status bar? Skip to the window format override.

Implementation checklist

  1. Add monitor-silence: 20 to each agent window in your tmuxinator config
  2. Add window-status-activity-style to .tmux.conf (gold text, noreverse)
  3. Add window-status-current-style to .tmux.conf (gold background, black bold)
  4. Override window-status-format and window-status-current-format to suppress the ~ flag
  5. Create the notification script at ~/.config/agent-attention.sh
  6. Create the reset script at ~/.config/agent-attention-reset.sh
  7. Create the state directory: mkdir -p /tmp/agent-attention
  8. Add silence-action any and visual-silence off to .tmux.conf
  9. Add the alert-silence hook to .tmux.conf
  10. Add after-select-pane and after-select-window reset hooks to .tmux.conf
  11. Make both scripts executable: chmod +x ~/.config/agent-attention.sh ~/.config/agent-attention-reset.sh
  12. Reload tmux config: tmux source-file ~/.tmux.conf
  13. Test: switch to an idle agent window, wait 20 seconds, confirm gold text appears in status bar
  14. Test: switch to a different app, wait for a macOS notification from an idle agent

Key artifacts

tmux.conf additions (all 7 lines):

# Silence detection styling
set -g window-status-activity-style "fg=colour220,nobold,noreverse"
set-window-option -g window-status-current-style "bg=colour220,fg=#000000,bold"
set -g window-status-format ' #I:#W#{?window_zoomed_flag,Z,} '
set -g window-status-current-format ' #I:#W#{?window_zoomed_flag,Z,} '

# Silence notification hooks
set -g silence-action any
set -g visual-silence off
set-hook -g alert-silence[0] 'run-shell -b "~/.config/agent-attention.sh \"#{hook_window_name}\" \"#{hook_session_name}\""'

# Reset rate limit on navigation
set-hook -g after-select-pane[0] 'run-shell -b "~/.config/agent-attention-reset.sh \"#{window_name}\" \"#{session_name}\""'
set-hook -g after-select-window[0] 'run-shell -b "~/.config/agent-attention-reset.sh \"#{window_name}\" \"#{session_name}\""'

Notification script (~/.config/agent-attention.sh):

#!/bin/bash
# Agent attention notification with rate limiting
# Called by tmux alert-silence hook

WINDOW_NAME="$1"
SESSION_NAME="$2"
STATE_DIR="/tmp/agent-attention"
COOLDOWN=180  # seconds between notifications per agent

# Skip non-agent windows (adjust this list to match your setup)
case "$WINDOW_NAME" in
  yazi|zsh|bash|vim|nvim) exit 0 ;;
esac

mkdir -p "$STATE_DIR"
STATE_FILE="${STATE_DIR}/${SESSION_NAME}-${WINDOW_NAME}"

# Rate limit: skip if notified recently
if [ -f "$STATE_FILE" ]; then
  LAST=$(stat -f %m "$STATE_FILE" 2>/dev/null || stat -c %Y "$STATE_FILE" 2>/dev/null)
  NOW=$(date +%s)
  ELAPSED=$(( NOW - LAST ))
  [ "$ELAPSED" -lt "$COOLDOWN" ] && exit 0
fi

# Capture last few lines of output for notification context
CONTEXT=$(tmux capture-pane -t "${SESSION_NAME}:${WINDOW_NAME}" -p 2>/dev/null | grep -v '^$' | tail -2 | head -c 200)

# Send macOS notification
DISPLAY_NAME="$WINDOW_NAME"
osascript -e "display notification \"${CONTEXT}\" with title \"Agent Ready\" subtitle \"${DISPLAY_NAME} @ ${SESSION_NAME}\"" 2>/dev/null

# Update state file
touch "$STATE_FILE"

Reset script (~/.config/agent-attention-reset.sh):

#!/bin/bash
# Clear rate-limit state when navigating to an agent window
# Called by tmux after-select-pane and after-select-window hooks

WINDOW_NAME="$1"
SESSION_NAME="$2"
STATE_FILE="/tmp/agent-attention/${SESSION_NAME}-${WINDOW_NAME}"

[ -f "$STATE_FILE" ] && rm -f "$STATE_FILE"

tmuxinator per-window config (add to each agent window):

- AgentName:
    layout: main-vertical
    options:
      monitor-silence: 20
    panes:
      - your-agent-launch-command
      - zsh

Concepts to remember

  • Silence = idle. CLI agents produce continuous output while working. When output stops for 20 seconds, the agent is waiting for input. This is the only signal you need.
  • tmux handles the visual lifecycle. window-status-activity-style applies automatically when the silence flag is set and clears when the window produces output again. No script cleanup required.
  • One notification per silence event. alert-silence fires once when silence is first detected, not repeatedly. The rate limiter is a safety net for edge cases, not the primary deduplication.
  • Reset on navigate. Clearing the rate-limit state when you visit an agent window means the next idle period gets a fresh notification. Without this, you’d miss notifications during the cooldown after your last visit.
  • silence-action any is critical. Without it, tmux only fires the hook for the current session. If you’re in session A and an agent in session B goes idle, you won’t get notified.

Pitfalls

  • window-status-activity-style defaults to reverse. If you don’t override it, flagged windows get inverted colors instead of your intended styling. Always set noreverse explicitly.
  • pane-focus-in does not work in tmux 3.5a. It accepts the hook silently but never fires. Use after-select-pane and after-select-window instead.
  • Missing -b on run-shell will block tmux’s command queue while the script runs. Always use the non-blocking flag.
  • stat syntax differs between macOS and Linux. macOS uses stat -f %m, Linux uses stat -c %Y. The notification script above includes both with a fallback.
  • Non-agent windows will trigger silence. A plain zsh prompt is always “silent.” Filter these out by name in the notification script’s case statement, or only set monitor-silence on agent windows in your tmuxinator config.