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"
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.
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
--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
--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
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
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"
terminal-notifier always shows Terminal's icon (-appIcon has no effect on macOS 26), and osascript shows Script Editor's.
Thumbnails on any post, and avatars that turn it into a message from someone.
macOS relaunches Nudge for the click, so --url opens even after your script has finished.
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 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.
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.
| Variable | What it changes |
|---|---|
CODESIGN_IDENTITY | The signing identity. security find-identity -v -p codesigning lists yours; - signs ad hoc. The default is the author's certificate. |
NUDGE_INSTALL_DIR | Where Nudge.app goes (default ~/Applications). |
NUDGE_BIN_DIR | Where the nudge command goes (default ~/.local/bin). |
SWIFT_DEVELOPER_DIR | The toolchain (default /Library/Developer/CommandLineTools). |
NUDGE_PROFILE | The 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.
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]]
| Flag | What it does |
|---|---|
--title, --subtitle, --message | The text. A post needs a title or a message. |
--url U | Opened when the notification is clicked. |
--image PNG | The thumbnail on the right. |
--group G | The notification's identity: a later post with the same group replaces this one in place. |
--thread T | Posts with the same thread stack together in Notification Center. Defaults to the group. |
--action ID=LABEL | Adds a button labelled LABEL that runs the command configured for ID. Repeatable. |
--env KEY=VALUE | Passed to a button's command as an environment variable. Repeatable. |
--avatar PNG | Posts as a message: the picture is the main icon, nudge's icon a badge. |
--sender NAME | The sender's name for --avatar. Defaults to the title. |
--authorize | Asks for notification permission without posting. |
Exit status: 0 posted, 2 notifications not allowed, 3 failed, 64 usage, 69 Nudge.app not found.
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.
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.system_profiler SPHardwareDataType; registered under iOS it shows up as an "iPod" and can't be used.~/.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.