Created
August 19, 2026 16:22
-
-
Save dorelljames/378b5fcc2d51f301e67163e82185522a to your computer and use it in GitHub Desktop.
zsh - auto-notify via herdr when long shell commands finish
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Notify through herdr when a long-running command finishes. | |
| # | |
| # Why this exists (2026-08-20): herdr only sends lifecycle notifications | |
| # for things it recognizes as agents. A plain `composer qa` or | |
| # `sail artisan test` finishes silently, so switching to another tab | |
| # means polling the old one. This hook times every interactive command | |
| # and raises a herdr notification when a slow one returns. The exit | |
| # code picks the sound: "done" for success, "request" for failure. | |
| # | |
| # Lives in $ZSH_CUSTOM, so oh-my-zsh sources it automatically. | |
| # To uninstall, delete this file. | |
| # HERDR_ENV is exported by herdr into the panes it manages. A normal | |
| # Terminal/iTerm shell never registers these hooks. | |
| [[ "$HERDR_ENV" == 1 ]] || return 0 | |
| (( $+commands[herdr] )) || return 0 | |
| zmodload zsh/datetime | |
| autoload -Uz add-zsh-hook | |
| # Commands faster than this never notify. Raise it if sub-minute | |
| # builds feel noisy. | |
| : ${HERDR_NOTIFY_MIN_SECONDS:=15} | |
| # Glob patterns matched against the full command line; matches never | |
| # notify. claude and codex already notify through herdr's own agent | |
| # lifecycle, so a second "command finished" for them is pure noise. | |
| # Override by defining this array in .zshrc before oh-my-zsh loads. | |
| typeset -ga HERDR_NOTIFY_IGNORE | |
| (( $#HERDR_NOTIFY_IGNORE )) || HERDR_NOTIFY_IGNORE=( | |
| 'claude*' 'codex*' 'herdr*' | |
| ) | |
| typeset -g _herdr_notify_cmd='' | |
| typeset -g _herdr_notify_started=0 | |
| _herdr_notify_preexec() { | |
| _herdr_notify_cmd=$1 | |
| _herdr_notify_started=$EPOCHSECONDS | |
| } | |
| _herdr_notify_precmd() { | |
| local exit_code=$? # must stay the first statement or it gets clobbered | |
| local started=$_herdr_notify_started | |
| local cmd=$_herdr_notify_cmd | |
| _herdr_notify_started=0 | |
| _herdr_notify_cmd='' | |
| # started stays 0 when no command ran: empty enter, ctrl-c at the | |
| # prompt. Without this guard those would "inherit" the previous | |
| # command's timer. | |
| (( started > 0 )) || return 0 | |
| local elapsed=$(( EPOCHSECONDS - started )) | |
| (( elapsed >= HERDR_NOTIFY_MIN_SECONDS )) || return 0 | |
| local pattern | |
| for pattern in $HERDR_NOTIFY_IGNORE; do | |
| [[ $cmd == ${~pattern} ]] && return 0 | |
| done | |
| # Mirror what herdr does for agents: a focused tab means the user | |
| # watched the command finish, so a notification is noise. This also | |
| # silences editors and pagers without listing them one by one — an | |
| # editor can only be quit from its own tab, which focuses it. The | |
| # socket round-trip only happens for commands that already ran long. | |
| if [[ -n "$HERDR_TAB_ID" ]] && \ | |
| [[ "$(herdr tab get "$HERDR_TAB_ID" 2>/dev/null)" == *'"focused":true'* ]]; then | |
| return 0 | |
| fi | |
| # Multi-line and very long commands overflow the notification popup; | |
| # flatten and cap them. | |
| cmd=${cmd//$'\n'/ } | |
| (( ${#cmd} > 120 )) && cmd="${cmd[1,117]}..." | |
| if (( exit_code == 0 )); then | |
| herdr notification show "✓ Finished (${elapsed}s)" \ | |
| --body "$cmd" --sound done >/dev/null 2>&1 | |
| else | |
| herdr notification show "✗ Failed: exit $exit_code (${elapsed}s)" \ | |
| --body "$cmd" --sound request >/dev/null 2>&1 | |
| fi | |
| } | |
| add-zsh-hook preexec _herdr_notify_preexec | |
| add-zsh-hook precmd _herdr_notify_precmd |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
If you're using
zsh, place it at~/.oh-my-zsh/custom/herdr-notify.zshor you can also translate this in your own shell.NOTE:
exec zshor open a new terminal after applying...