Notifications¶
When an agent in a pane you are not looking at blocks or finishes, taw tells you where you are looking:
| Where | What you see | To turn it on |
|---|---|---|
| In tmux | a toast on the status line: claude blocked · api:2 · needs input |
nothing, it is the default |
| On your desktop | a macOS or Linux notification | [ui.toast] delivery = "system" in place of the toast |
| In your terminal, over ssh too | your terminal's own notification (OSC 777 and OSC 9, for a terminal that shows them) | [ui.toast] delivery = "terminal" in place of the toast |
| On your status line, all the time | how many agents are blocked, working and done: ⏸1 ⏵3 ✓2 |
one line in tmux.conf: set -ag status-right ' #{@agent_wizard_status}' |
| On your phone | a message through ntfy, Pushover or any command | a one-line [hooks] notification (below) |
| Out loud | a sound file of yours | [ui.sound] path |
The last part of a toast is a line from the agent's screen: what it asks for when it is blocked, the last line of what it said when it is done. An empty input box says nothing, so a done agent with no reply on screen has no last part. When the agent's state was read from its window title (Codex's title tells it), the last part is that title.
prefix + O takes you there
From anywhere in tmux, prefix + O goes to the agent that needs you, and the next press to
the next one. With nothing waiting it goes to the next agent of any kind. It is the same rule
as Tab in the popup (see Next up).
Fires on two transitions: request: an agent just became blocked; done:
working or blocked to idle/done. Only for background panes (the pane's
window isn't the active window of any attached client). The first idle of an agent
that has just started (by you, taw agent start or a session restore), or that has
moved to another session, is not a done until the agent has worked: an agent that
asks its trust question at startup in a session you are not looking at is a request,
and once you answer it, idle with no toast (herdr v0.9.2's rule). A new session of
the agent (Claude's /clear, Pi's /new) turns a done back into idle and drops
a done toast still waiting out its delay. With tmux 3.6+ and
set -g focus-events on, a pane counts as background while your terminal itself is
in the background too: a finish while your terminal is in the background counts as
not looking, so it is done, with its toast. A done toast is decided when the
agent finishes: if you were looking at the pane then, there is none, even if you
leave before its delay is up. An agent that exits mid-turn while you are away has
finished too: its done toast arrives even though its pane has left the popup by then.
After the transition the watcher waits, then re-reads @agent_state, the agent and
its session, and the background gate, and only notifies if what triggered it is still
true.
| Key | Default | Waits before |
|---|---|---|
[ui.toast] delay_seconds |
1 |
a request toast |
[ui.toast] done_delay_seconds |
5 |
a done toast |
The two are separate because they are answering different questions. A blocked agent
is waiting on you right now, so a second's confirmation is enough. A finished turn is
not urgent, and agents routinely continue after one: Claude Code ends a turn with a
background shell still running and picks up again a moment later, and a done toast
fired into that gap is noise. Five seconds is long enough for the continuation to
show up and cancel the toast. Set done_delay_seconds = 0 to get herdr's timing back.
Both accept fractions, up to 3600.
[ui.toast] delivery |
What happens |
|---|---|
off |
nothing (this mutes the pop-up only; sound, if enabled, still plays) |
herdr (default) |
tmux display-message to every attached client. This replaces the status line while it's shown: it's tmux's own mechanism, not a floating overlay, so it borrows the same space your status line uses for the display-time duration |
terminal |
writes OSC 777 and OSC 9 notification sequences straight to each attached client's tty. Works over ssh: it doesn't depend on tmux's own display-message, just on your terminal understanding the OSC sequences |
system |
the host OS's own notifier: osascript on macOS, so toasts land in Notification Centre and survive you not looking at the terminal; notify-send on Linux, where the notification names tmux-agent-wizard as its source. Nothing on any other platform. Set delivery = "herdr" to go back to the in-tmux toast |
The popup does not change any of this. It is sized to leave the tmux status line
in place (showing taw's bar while it is open), so a herdr toast draws into that
line exactly as it would with the popup closed. The popup's own hint line is only
ever used for the popup's own results: "renamed", a failed kill-pane, the filter
you are typing.
A herdr toast ends with the key's own hint, prefix+O next up. The key goes to who
to deal with next (see "Next up"), which is the agent the toast is about only when it
is the next one in line.
Sounds ([ui.sound]) follow the same background-only gating, independent of
delivery (even off still plays a sound if one is configured; herdr mutes the
pop-up, not the sound). afplay on macOS, paplay falling back to aplay on
Linux. No sound files ship with this project: set path/done_path/
request_path (relative to your config.toml's own directory) to enable audio.
droid is muted by default; override per-agent under [ui.sound.agents].
Next up¶
Tab in the popup and prefix + O (@agent-wizard-notification-key) outside it are
one rule, "next up": who to deal with next. Both walk one step a press, wrapping:
- the agents that need you, while any does: blocked, or finished and not yet looked at;
- otherwise every agent, idle and working alike, and one that is unknown or has not reported a state.
Tab steps to the next row of the walk in the order the popup draws it (Shift-Tab to
the previous), and never leaves an agent that needs you for one that does not while one
is in view. With one agent in the walk it stays on it, so a blocked agent you leave
waiting on purpose keeps Tab there until it is answered. The tree's order does not
change as agents change state, so a sweep goes on from the cursor; f and then Tab
walks the same agents in the flat list's order, most urgent first. To walk the
agents whose row says idle, filter first: /'idle (the ' asks fzf for the word as
written), then Tab; an agent whose title has the word idle in it is found too, and
so are the rows under a header that draws it (a branch called idle), so they are in the walk
whatever their state. At the sessions depth a folded header stands for the
agents of its session that an agent view shows, and under a filter too when the filter found it by
its words; one found only by the title it draws stands for that agent. prefix + O runs taw notification open-target, which goes to the next
agent of the walk after the pane you are in, in tmux order (session, window, pane), and
reports the group of the pane it went to: needs_you, idle or rest. With no agent anywhere it does nothing, and the
status line says taw: no agents. If it fails any other way, the status line says
taw: next up failed (see taw notification open-target); running that command in a
shell shows the error.
The flat list is unchanged: it puts what needs you first and idle above working, and
Tab works the same way there as everywhere else. The latest notification
is not a step of its own any more, so a fresh request does not jump the queue; a
request is an agent that needs you, so it is in the first group anyway. herdr's own key
focuses the notification on screen and nothing else. The same key works inside the popup,
through the prefix chord: see "Keys".
Notifications on your phone¶
taw does not send anything to a phone itself, and herdr does not either: its toasts go to
the app, the terminal or the OS of the machine you are on. What taw has is a hook that runs
whenever it shows a toast, and a command that sends a message is all a phone needs.
[hooks] notification runs at the moment a toast would, told the toast's words in
TAW_MESSAGE (for example claude blocked · api:2 · needs input) and whether it is a
request (an agent needs you) or a done in TAW_KIND. It runs for taw notification
show too.
It fires only when a toast would: after the same delay (delay_seconds for a request,
done_delay_seconds for a done), and not for a pane you are looking at or a finish you
watched. So you are not pinged for what you are already looking at, and you are pinged for
the request that has waited a second while you were away. It runs whatever
[ui.toast] delivery says: off mutes the pop-up and still sends the message, as it
leaves sounds alone.
Three ready examples, each a command under [hooks] or a script it names.
ntfy (ntfy.sh, free, no account: pick a topic name nobody will guess and subscribe to it in the app):
Pushover, with the token and the user in the environment, and neither in the config nor
on a command line (curl -K - reads them from standard input, so ps does not show them):
#!/bin/sh
# ~/bin/push-pushover
exec curl -fsS -K - --form-string "message=$TAW_MESSAGE" \
https://api.pushover.net/1/messages.json <<EOF
form-string = "token=$PUSHOVER_TOKEN"
form-string = "user=$PUSHOVER_USER"
EOF
with notification = "~/bin/push-pushover".
Any command, from a plain script that forwards the message to whatever sends yours.
alert here stands for any command that takes the text as its one argument:
TAW_KIND tells a request from a done, so a script can be louder for one. With ntfy, high
priority for a request and the default for a finish:
#!/bin/sh
# ~/bin/push-ntfy
: "${NTFY_TOPIC:?set NTFY_TOPIC}"
priority=default
[ "$TAW_KIND" = request ] && priority=high
printf 'url = "https://ntfy.sh/%s"\n' "$NTFY_TOPIC" |
curl -fsS -K - -H "Priority: $priority" --data-raw "$TAW_MESSAGE"
taw hooks test notification sends the sample request, claude blocked ·
taw-hooks-test:2 · needs input, through your command as it would a real one, which is how
to check the whole path.
-f makes curl fail on an HTTP error, so a wrong topic or token lands in taw hooks log.
--data-raw and --form-string are there so that a message that starts with @ is sent
as text: -d and -F would try to read a file of that name.
Every account on a shared machine can read a command line with ps. The secret of ntfy is
its topic, and the one-liner above puts it in the URL, which is fine on a machine that is
yours. The Pushover script and the ntfy script with the priority keep their secret off the
command line: they give it to curl -K - on standard input.
A hook runs with tmux's global environment plus the TAW_* variables, whichever process
fired it, so NTFY_TOPIC and the like can live where tmux keeps its own. Put
set-environment -g NTFY_TOPIC yourtopic in tmux.conf, or export it in the shell that
starts the server. set-environment -gh works too: the value stays out of tmux
show-environment and out of every pane's shell, and it still reaches taw's hooks. The
watcher reads that environment when a hook first needs it and again on taw config reload,
so a value you change while tmux is running reaches your hooks after taw config reload.
A variable the session the event is about has of its own goes over the global one, as it
does in a pane of that session ("How a hook runs" has the rules).
taw hooks test notification runs your command with the same environment, so a test that
sends is a hook that will send. A script that reads its secret from a file of yours has
none of these questions.