nudge

Post macOS notifications from the command line, from an app of your own: its own icon, thumbnails, avatars, click-to-open, and buttons that run your commands.

# a build finished
nudge --title "Build finished" \
  --message "acme/widgets: all 214 tests passed"

What it looks like

nudge is a small signed app, Nudge.app, that posts through UNUserNotificationCenter, so every notification carries its own icon, not Terminal's or Script Editor's. These are drawings of what each kind looks like on macOS 26; the names, repositories and pictures are made up.

Build finishednow
acme/widgets: all 214 tests passed

Plain, with a link

Title, optional subtitle and message, with nudge's icon on the left. --url opens when you click the notification, even after the posting process has exited.

nudge --title "Build finished" \
  --message "acme/widgets: all 214 tests passed" \
  --url https://ci.example.com/acme/widgets/1234
Nightly reportnow
widgets.example.com
Sign-ups up 12% this week

With a thumbnail

--image puts a picture on the right. nudge attaches a copy, so your file stays where it is.

nudge --title "Nightly report" \
  --subtitle widgets.example.com \
  --message "Sign-ups up 12% this week" \
  --image signups.png
Alice Examplenow
Review requested · acme/widgets !42
Could you take a look at the retry change?
Example CI2m ago
Pipeline passed · acme/widgets
main is green again

From a person, with an avatar

--avatar posts the notification as a message from --sender: the picture becomes the main icon and nudge's icon shrinks to a badge on its corner. It can be a person's picture or an icon for the source. macOS shows only the sender and the body here, so nudge moves the title and subtitle to the body's first line. The Homebrew install supports this as is; a build from source needs a provisioning profile.

nudge --avatar alice.png --sender "Alice Example" \
  --title "Review requested" \
  --subtitle "acme/widgets !42" \
  --message "Could you take a look at the retry change?" \
  --image diff.png
Sam Samplenow
Approved · acme/widgets !42
Looks good, ship it
MergeOpen

With buttons that run your commands

Each --action ID=LABEL adds a button. Clicking it runs the command you configured for that id in ~/.config/nudge/actions.json, with each --env pair in its environment. With the Alerts style the buttons stay on screen; with Banners they show on hover.

nudge --avatar sam.png --sender "Sam Sample" \
  --title Approved --subtitle "acme/widgets !42" \
  --message "Looks good, ship it" \
  --action merge=Merge --action open=Open \
  --env MR=42 --env REPO=acme/widgets
Deploy · widgetsnow
Rolled out to 3 of 3 regions

Updated in place, stacked by thread

A post with the same --group replaces the earlier one with a fresh banner, so a progress notification doesn't pile up. Posts with the same --thread stack together in Notification Center.

# each step replaces the last
nudge --group deploy --title "Deploy · widgets" \
  --message "Rolled out to 1 of 3 regions"
nudge --group deploy --title "Deploy · widgets" \
  --message "Rolled out to 3 of 3 regions"

Why another notifier

Its own icon

terminal-notifier always shows Terminal's icon (-appIcon has no effect on macOS 26), and osascript shows Script Editor's.

Pictures

Thumbnails on any post, and avatars that turn it into a message from someone.

Clicks that work later

macOS relaunches Nudge for the click, so --url opens even after your script has finished.

Buttons, safely

Commands come only from your own actions.json, read at click time. A post can pass values only as environment variables, never as shell text.

Install

Install the notarized app with Homebrew, or build it from source with the Xcode Command Line Tools. It needs macOS 13 or later.

brew install --cask jb55/nudge/nudge
nudge --authorize   # once: registers Nudge and asks for notification permission

nudge --title Hello --message "from the command line"

The cask puts a notarized Nudge.app in /Applications, with everything enabled, avatars included, and links the nudge command onto your PATH. brew upgrade --cask nudge updates it in place. Keep one copy: if you also build from source, remove one of the two, since two registered apps with the same bundle id can make macOS relaunch the wrong one for a click.

From source

No Xcode project, no dependencies. A source build signed ad hoc or with your own certificate posts everything except avatars, which need a provisioning profile.

# once, if you don't have the Command Line Tools
xcode-select --install

git clone https://github.com/jb55/nudge
cd nudge
CODESIGN_IDENTITY=- ./build.sh   # "-" signs ad hoc; or name your own certificate

nudge --title Hello --message "from the command line"

build.sh compiles main.swift, assembles ~/Applications/Nudge.app, makes its icon, signs it, registers it with LaunchServices, asks for notification permission once, and installs the nudge command into ~/.local/bin.

VariableWhat it changes
CODESIGN_IDENTITYThe signing identity. security find-identity -v -p codesigning lists yours; - signs ad hoc. The default is the author's certificate.
NUDGE_INSTALL_DIRWhere Nudge.app goes (default ~/Applications).
NUDGE_BIN_DIRWhere the nudge command goes (default ~/.local/bin).
SWIFT_DEVELOPER_DIRThe toolchain (default /Library/Developer/CommandLineTools).
NUDGE_PROFILEThe provisioning profile for avatars (default ~/.config/nudge/nudge.provisionprofile).

If no permission prompt appears and posts exit with status 2, turn Nudge on in System Settings › Notifications.

Flags

nudge --title T --message M [--subtitle S] [--url U] [--image PNG]
      [--group G] [--thread T] [--action ID=LABEL]... [--env KEY=VALUE]...
      [--avatar PNG [--sender NAME]]
FlagWhat it does
--title, --subtitle, --messageThe text. A post needs a title or a message.
--url UOpened when the notification is clicked.
--image PNGThe thumbnail on the right.
--group GThe notification's identity: a later post with the same group replaces this one in place.
--thread TPosts with the same thread stack together in Notification Center. Defaults to the group.
--action ID=LABELAdds a button labelled LABEL that runs the command configured for ID. Repeatable.
--env KEY=VALUEPassed to a button's command as an environment variable. Repeatable.
--avatar PNGPosts as a message: the picture is the main icon, nudge's icon a badge.
--sender NAMEThe sender's name for --avatar. Defaults to the title.
--authorizeAsks for notification permission without posting.

Exit status: 0 posted, 2 notifications not allowed, 3 failed, 64 usage, 69 Nudge.app not found.

Buttons

Map each button id to a command in ~/.config/nudge/actions.json (or under $XDG_CONFIG_HOME):

{
  "merge": {
    "label": "Merge",
    "command": ["/Users/you/bin/merge-mr"]
  },
  "open": {
    "command": ["/bin/bash", "-lc", "open \"https://git.example.com/$REPO/-/merge_requests/$MR\""]
  }
}
  • command is an argv list whose first element is an absolute path. It runs detached from your home directory, with output appended to ~/Library/Logs/nudge-actions.log.
  • Its environment is launchd's minimal one plus each --env pair and NUDGE_ACTION=<id>. Use absolute paths, or bash -lc for your login environment.
  • The command is read when the button is clicked, never from the notification. nudge reads only command; other keys like label are for the scripts that decide which buttons to offer.

Avatars from a source build

The Homebrew install already has what avatars need, so this is only for building from source. Message-style notifications need Apple's Communication Notifications entitlement, which only a provisioning profile can grant. Without one, --avatar quietly falls back to a plain notification.

  1. In the Apple Developer portal, create an App ID for Nudge's bundle id (CFBundleIdentifier in Info.plist) with the Communication Notifications capability. App IDs belong to one team, so if you build nudge yourself, change the bundle id to a reverse-DNS name of your own first.
  2. Register your Mac under Devices with Platform macOS. On Apple silicon, use the Provisioning UDID from system_profiler SPHardwareDataType; registered under iOS it shows up as an "iPod" and can't be used.
  3. Make a macOS App Development profile for that App ID, your certificate and the Mac.
  4. Save it as ~/.config/nudge/nudge.provisionprofile (or point NUDGE_PROFILE at it) and run ./build.sh. It prints embedding … when it signs with the entitlement.

Message notifications can be allowed through a Focus, which plain ones aren't by default.