{"slug": "agent-state-in-the-tmux-status-line", "title": "Agent State in the Tmux Status Line", "summary": "A developer's attempt to render AI agent state (working, blocked, idle) directly into the tmux window list failed on both candidate signals: Claude Code sets its pane title once and never updates it, and tmux's pane_current_command reports versioned binary filenames such as 2.1.267 rather than 'claude', so the status-line rules for yellow and green never fired. The write-up, which also tested grok 1.0.30 (whose title does carry a braille spinner) and referenced the agent-first multiplexer Herdr and the tool agenmux, concludes the screen rather than OSC title updates is the viable source of agent state.", "body_md": "## The problem\n\nRunning several agent sessions (Claude Code, Codex, [PingMe](https://github.com/TheCloudlet/PingMe)) side by side in tmux\nwindows makes it easy to lose track of which window is still working, which is\nstuck waiting on a decision, and which finished. Checking each window by hand\ndoesn't scale past three or four of them.\n\nHerdr is the usual suggestion in this niche: an agent-first terminal multiplexer that classifies sessions as working, blocked, done, or idle and shows them in a sidebar. A week with it surfaced two objections — it's a separate surface to context-switch into for information that belongs next to the windows it describes, and it replaces tmux rather than fitting the existing habit of glancing at a status line.\n\nagenmux keeps tmux and answers the second objection, rendering agent state into a sidebar pane and a status-line segment. Both are regions separate from the window list. What follows writes the marker onto the window entries themselves.\n\nHerdr also tracks a fourth state, **done** — a turn that finished while the window\nwas not visible, cleared on the next visit. That requires per-pane focus\nhistory, which the setup below does not keep; done collapses into idle, leaving\nthree states.\n\nThe target is the same classification, rendered in the tmux window list. Two signals looked like they carried it and did not.\n\n## Attempt 1: the pane title\n\ntmux tracks `pane_title` per pane, and programs update it via an OSC escape\nsequence. Agent CLIs are interactive TUIs, so the title is the first candidate\nfor carrying their state.\n\n```\nset -g @agent-status \\\n'#{?#{m:*Action Required*,#{pane_title}},#[fg=colour196]#[bold]!,'\\\n'#{?#{m/r:(^| )[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏◐◓◑◒]( |$),#{pane_title}},#[fg=colour220]●,'\\\n'#{?#{m/r:(^|/)(codex|claude|pingme)$,#{pane_current_command}},#[fg=colour34]✓,}}}'\n```\n\nYellow never appeared once. Sampling the title twice a second through a full working turn explains why:\n\n``` bash\n$ for i in $(seq 1 40); do tmux list-panes -a -F '#{pane_id} [#{pane_title}]'; sleep 0.5; done | sort -u\n%0 [✳ Tmux config article]\n%1 [✳ Claude Code]\n```\n\nTwo unique values across the whole sample. Claude Code sets its title once and never touches it again — it carries the session's name, not its state, and no regex over a constant string will produce a state machine.\n\nThat sample contained two Claude panes. Grok, on the same tmux server:\n\n```\nt=1  title=[Review of tmux agent-status article - grok]\nt=2  title=[⠋ - Waiting for response… - Review of tmux agent-status article - grok]\nt=4  title=[⠋ - Thinking - Review of tmux agent-status article - grok]\nt=7  title=[⠸ - Thinking - Review of tmux agent-status article - grok]\n```\n\nA braille spinner, in the title, from the character class the yellow rule matched on. The title is inert for Claude Code and not for grok, so a per-agent rule set could read grok's title directly. The screen carries state for both without depending on OSC title updates being emitted.\n\n## Attempt 2: the foreground process\n\nWith Claude's title ruled out, the next candidate is `pane_current_command`, the\nprocess tmux considers to be in the foreground. An agent running a tool occupies\na different foreground process than one sitting at a prompt.\n\n``` bash\n$ tmux list-panes -a -F '#{pane_id} cmd=[#{pane_current_command}]'\n%0 cmd=[2.1.267]\n%1 cmd=[2.1.273]\n```\n\nNot `claude`. The install layout accounts for it:\n\n``` php\n$ ls -l ~/.local/bin/claude\n... -> ~/.local/share/claude/versions/2.1.273\n\n$ ls -l ~/.grok/bin/grok\n... -> ../downloads/grok-1.0.30-macos-aarch64\n```\n\nBoth launchers are symlinks to a versioned binary, and the process carries that\nbinary's filename. tmux reports it: `claude` as `2.1.267`, grok as\n`grok-1.0.30-mac`, truncated from `grok-1.0.30-macos-aarch64`. The value\nidentifies a release artifact rather than the program.\n\nThe green icon never lit on this machine either. Its rule required\n`pane_current_command` to end in `claude`, which this value never does, so the\nthird branch fell through to the empty fallback alongside the other two.\n\nThe process tree contains a process whose `comm` is `claude`, whatever the\npane's foreground command is named, so `ps -o comm=` over the tree answers the\npresence question. State is a separate matter: `pane_current_command` identifies\na process, and process identity does not encode \"thinking\" versus \"waiting for\napproval.\"\n\n## What actually carries the state\n\n[agenmux](https://github.com/snirt/agenmux), an open-source tmux agent monitor that ports\nHerdr's detection rules, documents its method:\n\nDetection is scraping-only: agents are identified by walking each pane's process tree, state is inferred from the pane's visible screen and title.\n\nThe rendered terminal buffer, then, rather than the title or the process. Herdr matches its rules for Claude Code and Codex against a snapshot of the bottom of the live buffer.\n\ntmux exposes exactly that through `capture-pane`. The bottom of a working Claude\nCode pane:\n\n```\n✽ Brewing… (2m 14s · ↓ 3.9k tokens)\n─────────────────────────────────────────────────────\n❯\n─────────────────────────────────────────────────────\n  ⏵⏵ auto mode on (shift+tab to cycle) · esc to interrupt · ← for agents\n```\n\nAnd the same pane once it's idle:\n\n```\n─────────────────────────────────────────────────────\n❯\n─────────────────────────────────────────────────────\n  ⏵⏵ auto mode on (shift+tab to cycle) · ← for agents\n```\n\n`esc to interrupt` is present in one and absent in the other. The hint can only\nbe rendered while something is interruptible, which makes it the state. The\nactivity line above it carries a spinner as well (`✽ Brewing…`), through the\nglyphs `✳ ✽ ✶` — a different character class from the braille and circle set the\nfirst attempt searched for, on a different surface.\n\nThe same config appeared to work on a Linux machine whose title was equally\nstatic. What lit there was green, which required only that\n`pane_current_command` resolve to `claude` — true on that box, false here. Green\nreported an agent process in the pane; it never reported what that process was\ndoing.\n\n## The implementation\n\nTwo signals, each answering the question it can actually answer: the process tree for \"which agent is here\", the screen buffer for \"what is it doing\".\n\nThe walk returns the agent's name rather than a yes/no, since the screen rules differ per agent:\n\n```\npane_agent() {\n\troot=\"$1\"; pids=\"$root\"; queue=\"$root\"\n\twhile [ -n \"$queue\" ]; do\n\t\tpid=\"${queue%% *}\"; queue=\"${queue#\"$pid\"}\"; queue=\"${queue# }\"\n\t\tfor c in $(pgrep -P \"$pid\" 2>/dev/null); do\n\t\t\tcase \" $pids \" in\n\t\t\t*\" $c \"*) ;;\n\t\t\t*) pids=\"$pids $c\"; queue=\"$queue $c\" ;;\n\t\t\tesac\n\t\tdone\n\tdone\n\tps -o comm= -p $pids 2>/dev/null |\n\t\tsed -nE 's|.*/||; /^(claude|claude-code|codex|grok|pingme)$/p' | head -n 1\n}\n```\n\nThen each agent gets its own patterns and its own order. Sketched as pseudocode\n— `match` stands in for a grep against the captured screen, and the patterns are\nelided; the [appendix](https://thecloudlet.github.io/technical/til/tmux-agent-status-indicator/#appendix-the-full-poller) has the runnable version:\n\n```\nclaude, claude-code, pingme:\n    working  if screen matches 'esc to interrupt|ctrl+c to interrupt'\n    idle     if screen matches a bare '❯' prompt line\n    action   if screen matches 'do you want to proceed?|waiting for permission|…'\n    idle     otherwise\n\ncodex:\n    action   if screen matches 'press enter to confirm or esc to cancel|…'\n    working  if screen matches 'esc to interrupt'\n    idle     otherwise\n\ngrok:\n    action   if screen matches '<n>/<n>:select|Allow …?|No, reject|dialog footer hints'\n    working  if screen matches '[stop]|Ctrl+c:cancel'\n    idle     otherwise\n```\n\nClaude and Codex both print `esc to interrupt`, and order their rules\ndifferently around it. Claude treats the hint as authoritative and checks\nworking first, with a bare-prompt idle rule ahead of the blocked patterns so an\nanswered permission prompt left on screen does not read as waiting. Codex checks\nits approval prompts first. Reversing either order makes every prompt read as\nbusy, or every busy turn read as blocked.\n\nOne caveat on provenance: herdr and agenmux check the **title** before the screen\nfor Codex (`CHECK_ORDER=\"bt wt bs ws\"`). Title rules are dropped here, which\nkeeps detection independent of an OSC sequence reaching tmux. Grok's title\ncarries its state and Claude's does not, so a title rule resolves for one agent\nand returns nothing for the other. Only the screen rules carry over, and the\nordering above is between screen rules alone. `pingme` rides along on Claude's\nrules because it wraps Claude Code and renders its UI; that grouping is an\nassumption, not something separately observed.\n\nGrok needed the most work. agenmux ships no manifest for it; herdr does\n([grok.toml](https://github.com/herdrdev/herdr/blob/master/src/detect/manifests/grok.toml)), but against Grok Build 0.2.101, whose footer differs from the\n1.0.30 installed here — it expects `Ctrl+.:shortcuts` where this build prints\n`Ctrl+x:shortcuts`. Close enough to confirm the approach, far enough that the\nstrings had to be re-read from a live session.\n\nA plain conversation suggested `Esc:cancel`, which sits in the footer while a\nturn runs and disappears when it ends. That rule held for chat and failed on the\nfirst shell command: during a long tool call the spinner text stops updating and\nthe cancel hint becomes `Ctrl+c:cancel`. A pane spending twenty seconds in\n`sleep` reported idle for the whole turn.\n\nThe `[stop]` affordance on the activity line persists across both cases. Blocked\nis still checked first, since grok's approval prompt retains `[stop]` from the\ntool call that raised it, and a working-first order would read every permission\nprompt as busy.\n\nherdr's manifest classifies `ctrl+c:cancel` as a blocked signal, paired with\n`:select` and `ctrl+o:yolo`, rather than a working fallback. Treating it as\nworking, as below, depends entirely on the blocked patterns matching first; a\npermission footer they miss reads as busy instead of waiting.\n\nBoth failures came from picking a signal out of a single observed scenario, and both surfaced when a second scenario ran. The rules cover the range of states actually watched.\n\nThe listing above is trimmed for reading; the full script, with every pattern\nspelled out, is in the [appendix](https://thecloudlet.github.io/technical/til/tmux-agent-status-indicator/#appendix-the-full-poller).\n\nThis runs as a background loop rather than inside the format string. The earlier\nversion called a script from `#(...)`, which fails in a subtle way: `#()` is not\na synchronous call but a job whose result is cached for the **next** redraw, so\nstate computed that way is only as fresh as the last time something drew the\nscreen. A detached session has no redraws at all. Process trees and screen\ncontents are facts about the machine, true whether or not anyone is looking — so\nthey belong on their own clock, with the format string reduced to a pure read:\n\n```\nset -g @agent-status \\\n'#{?#{==:#{@agent-state},action},#[fg=colour196]#[bold]!,'\\\n'#{?#{==:#{@agent-state},working},#[fg=colour220]●,'\\\n'#{?#{==:#{@agent-state},idle},#[fg=colour34]✓,}}}'\n\nset-window-option -g window-status-format '#{E:@agent-status}#[fg=colour18]#[nobold]#I:#W#F '\nset-window-option -g window-status-current-format '#{E:@agent-status}#[fg=colour252]#[nobold]#I:#W#[fg=colour196]#[bold]* '\n```\n\nBoth lines are needed: tmux renders the current window through its own format, so setting only the first leaves the window you're looking at without an icon.\n\n`set-option -p` scopes each answer to one pane. The window list renders one icon\nper window, resolved against its active pane, so a split running two agents\nsurfaces one of them. The loop starts once per server, guarded by a PID file\nrather than `pgrep -f agent-status-poll.sh`: a wrapper shell whose own argv\ncontains the script name is a false positive for any name-substring guard.\n\n```\nif-shell '! kill -0 \"$(cat /tmp/tmux-agent-status-poll.pid 2>/dev/null)\" 2>/dev/null' \\\n  'run-shell -b \"~/.config/tmux/agent-status-poll.sh\"'\n```\n\n## Result\n\nThe window list becomes the board: red `!` needs a decision, yellow `●` is\nworking, green `✓` is idle and ready, unmarked is a plain shell. It stays\ncorrect whether or not a client is attached, because nothing about the detection\ndepends on being watched.\n\nIn the bottom line, `✓1:123444` is an idle Claude pane and `●2:grok-1.0.30-mac`\nis grok mid-turn, its window name the versioned-binary filename from Attempt 2.\nThe working rule matched on that pane's `Waiting for response… 6.8s` line and\nits `[stop]` chip.\n\nTitles and process names are metadata, and each held for some agents and not others. Claude's title is a fixed session name; grok's carries a live spinner. Grok's foreground command is its own binary; Claude's is a version string. The screen is the surface every one of them populates, being the surface they exist to draw.\n\nThree agents required three rule sets and two orderings, and grok's were re-read from a live session because herdr's manifest targets Build 0.2.101 against the 1.0.30 installed here. The rules match footer strings and break whenever an agent redesigns its footer, silently and without a version number to check against.\n\nherdr prefers a lifecycle hook where one exists: `full_lifecycle_hook_authority`\nnames pi, omp, mastracode, opencode, kilo and kimi as agents whose own reports\noverride screen detection. Claude Code, Codex and grok are absent from that\nlist, and herdr scrapes them. Claude Code's hooks could write `@agent-state`\ndirectly, covering one of the three, installed into that agent's config, and\nreporting nothing about a pane running anything else. The screen is the one\ninput available for all of them.\n\n## Appendix: the full setup\n\n### The poller\n\n`~/.config/tmux/agent-status-poll.sh`, verified against claude 2.1.x and\ngrok 1.0.30 on macOS. Codex's patterns are agenmux's, carried over\nuntested. Two known rough edges: the PID file lives at a fixed `/tmp` path,\nso two tmux servers on one machine would contend for it, and grok's rules\nare pinned to the footer text of one release — a redesign there breaks them\nsilently, which is the standing cost of screen scraping.\n\n``` bash\n#!/bin/sh\n# Background poller: every second, classify each pane's agent state and write\n# it into that pane's @agent-state user option, which the status line reads.\n#\n# Two signals, both necessary:\n#   - process tree: which agent is running here? These CLIs launch through a\n#     symlink to a versioned binary, so pane_current_command reports that\n#     filename (claude -> \"2.1.267\", grok -> \"grok-1.0.30-mac\") rather than\n#     the agent's name. Walking the tree and matching `ps -o comm=` finds it.\n#   - visible screen: what is that agent doing? Every agent draws its state\n#     there, which is not true of the title: Claude's is a fixed session name\n#     while grok's carries a live spinner.\n#\n# Screen patterns and check order are per-agent. Claude's and Codex's come\n# from agenmux's agents/*.conf (which ports herdr's manifests), minus the\n# title rules those check first -- dropped on purpose, so detection never\n# depends on an OSC sequence reaching tmux. They differ in more than wording:\n# Claude treats the interrupt hint as authoritative and checks working first,\n# while Codex checks its blocked prompts first.\n# Verified against claude 2.1.x and grok 1.0.30; codex is untested.\n\necho $$ >/tmp/tmux-agent-status-poll.pid\n\n# Exit cleanly when killed: tmux reports any non-zero run-shell exit in the\n# status line, and a terminated daemon is routine, not an error worth showing.\ntrap 'rm -f /tmp/tmux-agent-status-poll.pid; exit 0' EXIT HUP INT TERM\n\n# Echo the agent binary found anywhere in the pane's process tree, if any.\npane_agent() {\n\troot=\"$1\"\n\tpids=\"$root\"\n\tqueue=\"$root\"\n\twhile [ -n \"$queue\" ]; do\n\t\tpid=\"${queue%% *}\"\n\t\tqueue=\"${queue#\"$pid\"}\"\n\t\tqueue=\"${queue# }\"\n\t\tchildren=$(pgrep -P \"$pid\" 2>/dev/null)\n\t\tfor c in $children; do\n\t\t\tcase \" $pids \" in\n\t\t\t*\" $c \"*) ;;\n\t\t\t*)\n\t\t\t\tpids=\"$pids $c\"\n\t\t\t\tqueue=\"$queue $c\"\n\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\tdone\n\t# shellcheck disable=SC2086\n\tps -o comm= -p $pids 2>/dev/null |\n\t\tsed -nE 's|.*/||; /^(claude|claude-code|codex|grok|pingme)$/p' |\n\t\thead -n 1\n}\n\nCLAUDE_WORKING='esc to interrupt|ctrl\\+c to interrupt'\nCLAUDE_IDLE='^[[:space:]]*❯[[:space:]]*$'\nCLAUDE_BLOCKED='do you want to proceed\\?|waiting for permission|do you want to allow this connection\\?|enter to select.*esc to cancel|esc to cancel.*enter to select'\n\nCODEX_WORKING='esc to interrupt'\nCODEX_BLOCKED='press enter to confirm or esc to cancel|enter to submit answer|enter to submit all|allow command\\?|\\[y/n\\]|yes \\(y\\)'\n\n# From observing grok 1.0.30. herdr has a grok manifest but targets Build\n# 0.2.101, whose footer differs (Ctrl+.:shortcuts vs Ctrl+x:shortcuts here).\n# The activity line's \"[stop]\" affordance is the only marker present for a\n# whole turn: the spinner text stops updating while a long tool call runs,\n# and the cancel hint moves between Esc and Ctrl+c depending on focus.\n# ponytail: Ctrl+c:cancel also appears in permission footers, where herdr\n# treats it as a blocked signal -- safe only because BLOCKED is checked\n# first; a permission footer those patterns miss would read as working.\nGROK_WORKING='\\[stop\\]|Ctrl\\+c:cancel'\nGROK_BLOCKED='^[[:space:]]*[0-9]+/[0-9]+:select|Allow .*\\?[[:space:]]*$|No, reject|Tab:scrollback|Shift\\+x:dismiss|Ctrl\\+o:yolo'\n\nclassify() {\n\tagent=\"$1\"\n\tscreen=\"$2\"\n\tcase \"$agent\" in\n\tcodex)\n\t\t# Blocked first, as in agenmux. Untested against a live codex session:\n\t\t# the patterns are its screen rules, minus the title checks that come\n\t\t# first upstream and are useless here.\n\t\tif printf '%s' \"$screen\" | grep -qiE \"$CODEX_BLOCKED\"; then\n\t\t\techo action\n\t\telif printf '%s' \"$screen\" | grep -qE \"$CODEX_WORKING\"; then\n\t\t\techo working\n\t\telse\n\t\t\techo idle\n\t\tfi\n\t\t;;\n\tgrok)\n\t\t# Blocked first: an approval prompt keeps the spinner and [stop]\n\t\t# indicator from the tool call that raised it, so working would win.\n\t\tif printf '%s' \"$screen\" | grep -qiE \"$GROK_BLOCKED\"; then\n\t\t\techo action\n\t\telif printf '%s' \"$screen\" | grep -qE \"$GROK_WORKING\"; then\n\t\t\techo working\n\t\telse\n\t\t\techo idle\n\t\tfi\n\t\t;;\n\tclaude | claude-code | pingme)\n\t\t# Working beats blocked (the interrupt hint is authoritative), and a\n\t\t# bare ❯ prompt means idle -- checked before blocked so an answered\n\t\t# permission prompt still on screen doesn't read as waiting.\n\t\tif printf '%s' \"$screen\" | grep -qE \"$CLAUDE_WORKING\"; then\n\t\t\techo working\n\t\telif printf '%s' \"$screen\" | grep -qE \"$CLAUDE_IDLE\"; then\n\t\t\techo idle\n\t\telif printf '%s' \"$screen\" | grep -qiE \"$CLAUDE_BLOCKED\"; then\n\t\t\techo action\n\t\telse\n\t\t\techo idle\n\t\tfi\n\t\t;;\n\t*)\n\t\techo idle\n\t\t;;\n\tesac\n}\n\nwhile true; do\n\tfor pane in $(tmux list-panes -a -F '#{pane_id}:#{pane_pid}' 2>/dev/null); do\n\t\tpane_id=\"${pane%%:*}\"\n\t\tpane_pid=\"${pane##*:}\"\n\n\t\tagent=$(pane_agent \"$pane_pid\")\n\t\tif [ -z \"$agent\" ]; then\n\t\t\ttmux set-option -p -t \"$pane_id\" @agent-state '' 2>/dev/null\n\t\t\tcontinue\n\t\tfi\n\n\t\tscreen=$(tmux capture-pane -p -t \"$pane_id\" 2>/dev/null | tail -20)\n\t\tstate=$(classify \"$agent\" \"$screen\")\n\n\t\ttmux set-option -p -t \"$pane_id\" @agent-state \"$state\" 2>/dev/null\n\tdone\n\tsleep 1\ndone\n```\n\n### The tmux config\n\n`~/.config/tmux/tmux.conf` in full, so the status-line pieces are visible in\nthe context they run in. The agent-status block is the middle section; the\nrest is ordinary setup that happens to surround it.\n\n```\n###############################################################################\n# Tmux display settings\n###############################################################################\nset -g default-terminal \"screen-256color\"\n\nset -g base-index 1           # start windows numbering at 1\nsetw -g pane-base-index 1     # make pane numbering consistent with windows\n\nset -g renumber-windows on    # renumber windows when a window is closed\nset -g set-titles on          # set terminal title\nset -g display-panes-time 800 # slightly longer pane indicators display time\nset -g display-time 1000      # slightly longer status messages display time\nset -g status-interval 1      # redraw status line every second\n\n# Set status bar background to light grey and foreground to a contrasting color\nset -g status-bg colour252 # light grey\nset -g status-fg colour18  # dark blue\n\n# Customize the left side of the status bar\nset -g status-left '#[bg=colour252,fg=colour18] #S #[bg=colour252,fg=colour18]'\n\n# Customize the right side of the status bar\nset -g status-right '#[bg=colour252,fg=colour18] %m-%d %H:%M #[bg=colour252,fg=colour18]'\n\n# Customize the window status format\nset-window-option -g window-status-current-style 'bg=colour18,fg=colour252'\n\n# Agent state is written by a background poller (agent-status-poll.sh) into\n# each pane's @agent-state option, not looked up here at render time --\n# process-tree checks via #() only get re-run when a client redraws, so a\n# detached/unwatched session would show stale or blank icons.\nset -g @agent-status \\\n'#{?#{==:#{@agent-state},action},#[fg=colour196]#[bold]!,'\\\n'#{?#{==:#{@agent-state},working},#[fg=colour220]●,'\\\n'#{?#{==:#{@agent-state},idle},#[fg=colour34]✓,}}}'\nset-window-option -g window-status-format '#{E:@agent-status}#[fg=colour18]#[nobold]#I:#W#F '\nset-window-option -g window-status-current-format '#{E:@agent-status}#[fg=colour252]#[nobold]#I:#W#[fg=colour196]#[bold]* '\n\n# Start the poller once per server. PID-file guarded so `tmux source-file`\n# reloads don't spawn duplicates -- `pgrep -f` alone false-positives when a\n# wrapper shell's argv happens to contain the script name.\nif-shell '! kill -0 \"$(cat /tmp/tmux-agent-status-poll.pid 2>/dev/null)\" 2>/dev/null' \\\n  'run-shell -b \"~/.config/tmux/agent-status-poll.sh\"'\n\n###############################################################################\n# Tmux bindings\n###############################################################################\n\n# Reload tmux config\nbind r source-file ~/.config/tmux/tmux.conf \\; display \"Reloaded ~/.config/tmux/tmux.conf!\"\n\n# Set new panes to open in current directory\nbind c new-window -c \"#{pane_current_path}\"\nbind '\"' split-window -c \"#{pane_current_path}\"\nbind % split-window -h -c \"#{pane_current_path}\"\n\n# mouse on\nsetw -g mouse on\n\n# set vi mode for copy mode\nsetw -g mode-keys vi\n\n## Clipboard integration\nset -s set-clipboard external\nbind Escape copy-mode\nbind p paste-buffer\nbind -T copy-mode-vi v send -X begin-selection\nbind -T copy-mode-vi y send-keys -X copy-selection-and-cancel\nbind -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-selection-and-cancel\nbind -T copy-mode-vi Enter send-keys -X copy-selection-and-cancel\n\n## hjkl pane traversal\nbind h select-pane -L\nbind j select-pane -D\nbind k select-pane -U\nbind l select-pane -R\n\n## move window right / left\nbind-key -n C-S-Left swap-window -t -1 \\; select-window -t -1\nbind-key -n C-S-Right swap-window -t +1 \\; select-window -t +1\n```\n\nBoth files live in the same directory, which is what lets the `if-shell`\nline reference the script by a fixed path. Stowed as one package, they land\nat `~/.config/tmux/` together.", "url": "https://wpnews.pro/news/agent-state-in-the-tmux-status-line", "canonical_source": "https://thecloudlet.github.io/technical/til/tmux-agent-status-indicator/", "published_at": "2026-09-16 07:09:43+00:00", "updated_at": "2026-09-16 08:11:08.626904+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-tools"], "entities": ["tmux", "Claude Code", "Codex", "PingMe", "Herdr", "agenmux", "grok", "Grok 1.0.30"], "alternates": {"html": "https://wpnews.pro/news/agent-state-in-the-tmux-status-line", "markdown": "https://wpnews.pro/news/agent-state-in-the-tmux-status-line.md", "text": "https://wpnews.pro/news/agent-state-in-the-tmux-status-line.txt", "jsonld": "https://wpnews.pro/news/agent-state-in-the-tmux-status-line.jsonld"}}