# terminal-notifier: sending macOS User Notifications from a shell prompt

> A command-line front end for macOS User Notifications, shipped as an app bundle. It is useful when a script needs to tell you something on a Mac, and it has real constraints around quarantine, escaping and the macOS version floor.

**julienXX/terminal-notifier** — Send User Notifications on macOS from the command-line.

- Repository: https://github.com/julienXX/terminal-notifier
- Stars: 7,343 · Forks: 359
- Language: Objective-C
- License: NOASSERTION
- Published: 2026-09-22 · Updated: 2026-09-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/julienxx-terminal-notifier

## The gap terminal-notifier fills on macOS

A long-running build, a backup job or a deploy script has no good way to reach the person watching it. Printing to a terminal only works if that terminal is visible. macOS has User Notifications, but the API sits behind Objective-C frameworks, so a shell script cannot call it directly. terminal-notifier wraps that API in a single executable and exposes it as flags. The README shows the shape of it in one line: `terminal-notifier -title 'Build' -message 'Finished in 42s' -sound default`.

The audience is narrow and specific. Anyone writing shell scripts, Makefiles, CI steps or Ruby tooling on macOS who wants an out-of-band signal. The repository carries a `Ruby/` directory and the README has a section on calling it from Ruby, so that audience is explicitly served. It is not a cross-platform notification library, and the related search phrases about Windows and Linux point at a need this project does not answer.

One design decision explains most of the oddities: a notification is attributed to an application, so terminal-notifier ships as an app bundle rather than a bare executable. That is why the macOS 11 and later behaviour described in the README is that the notification also shows the name of the sending application, and why the README offers a way to change that name and icon.

## How a notification travels from your shell to Notification Center

The command line is parsed and the values are read through `NSUserDefaults`, which tries to interpret each value as a property list. That detail drives the escaping rule: a value whose first character is `[`, `(`, `{`, `"` or similar is misread, and the notification is not sent. The README gives the fix, a backslash before the offending character, and notes that only the first character ever needs it. Since 3.0.0 the tool reports a value it could not read instead of failing silently, which is a real improvement over a script that appears to succeed while nothing appears.

From there the options map onto notification fields. `-title`, `-subtitle` and `-message` fill the text, with the title defaulting to `Terminal`. `-sound` names a file in `/System/Library/Sounds`, and an unrecognised name is ignored in favour of the default sound. `-contentImage` attaches a local PNG, JPEG or GIF shown inside the notification. `-group` is the interesting one: only one notification per group is ever shown, so posting again to the same group replaces the previous one, which is how you implement a progress indicator. The README suggests `$$` to scope by process and `$PWD` to scope by project.

Scheduling and removal share the group concept. `-in` and `-at` schedule a notification, and `-remove` cancels one that has not fired yet, but only if it was scheduled with a `-group`. Without a group there is no name to remove by, and only `-remove ALL` can cancel it. That is a sharp edge worth knowing before you build a scheduler around it.

## Installing terminal-notifier and sending a first notification

The README lists three installation routes: Homebrew, a prebuilt binary from the releases page, or building from source. Homebrew is the shortest path and the one that avoids the quarantine problem entirely.

```bash
brew install terminal-notifier
```

After that, `terminal-notifier` is on your PATH. The minimum invocation needs one of `-message`, `-remove` or `-list`. Try the README's own example:

```bash
terminal-notifier -title 'Build' -message 'Finished in 42s' -sound default
```

You should see a notification with the title Build and the body Finished in 42s, and hear the standard notification sound. Note that the title defaults to `Terminal` when you omit `-title`, which is why that word appears above the body in the README's screenshot.

If you download the prebuilt app bundle instead, macOS quarantines it and the first run is blocked. The README's workaround is to strip the attribute:

```bash
xattr -dr com.apple.quarantine /path/to/terminal-notifier.app
```

This only affects downloaded copies. Homebrew and `make install` are not quarantined and need nothing. When calling a downloaded bundle, the README says to invoke the binary inside it:

```bash
./terminal-notifier.app/Contents/MacOS/terminal-notifier -message 'Hello'
```

If you would rather build, the Makefile is written to work with the Command Line Tools alone and treats the Xcode project as optional. `make install` puts the app in `/Applications` by default and links the binary into `/usr/local/bin`; both are overridable through `APPDIR` and `PREFIX`.

## Grouping, listing and cancelling notifications

The stateful part of the tool is worth a closer look, because it is where the design either fits your use case or does not. `-list ID` prints delivered notifications in a group, or `ALL` for everything, as tab-separated output with a header row of GroupID, Title, Subtitle, Message and Delivered At. Tabs and newlines inside a field are replaced with spaces, so each notification is always exactly one row of five columns. That is a deliberate choice in favour of parseable output, and it means a message containing a newline will not break your `awk` pipeline.

`-list PENDING` shows what `-in` and `-at` have scheduled but not fired, with the soonest first and the due time in the last column. The README is explicit that a scheduled notification appears in the pending list until it fires and in `-list ALL` afterwards, never in both. That invariant is what makes the two lists usable together.

Cancellation depends on the group. Schedule with a group and you can remove it later:

```bash
terminal-notifier -message 'Backup starting' -group backup -at 02:00
terminal-notifier -remove backup
```

The second command cancels the first, so nothing is shown at 02:00. Drop the `-group` and the only remaining escape hatch is `-remove ALL`, which also wipes every other notification terminal-notifier has posted. In a shared script that is a blunt instrument.

## Click actions and where their results go

`-open URL` opens a URL when the user clicks the notification body. The README notes that any scheme works, including deeplinks with no host such as `msteams:`, `mailto:` and `tel:`, and that the value needs a scheme, so a file must be passed as `file:///path` rather than a bare path. That last point is the kind of thing that silently does nothing if you get it wrong.

`-execute COMMAND` runs a shell command on click instead. This is where the security question in the search data has a concrete answer: a notification posted by terminal-notifier can be configured to run an arbitrary shell command when clicked, so a notification is only as trustworthy as the script that created it. The README states that what happened is written to the system log and read with Console.app, which means debugging a click action that did not fire starts in Console rather than in your terminal.

There is no documented callback into your process. If your script needs to know whether the user clicked, the documentation does not describe a mechanism for that, so plan around it rather than assuming one exists.

## Quarantine, notarization and the macOS version floor

The README is unusually direct about the biggest practical limitation: terminal-notifier is not notarized, and the author states he does not want to pay the Apple tax for now. macOS quarantines anything you download, so the first run of a downloaded copy is blocked until you strip the attribute. That is a real friction point for anyone distributing the binary inside an installer or a managed fleet, and it is the reason Homebrew and `make install` are the recommended routes in practice.

The version floor is macOS 10.14 or higher, stated plainly in the README and encoded in the Makefile as `MIN_MACOS := 10.14`. On macOS 11 and later the notification shows the name of the sending application, which the README addresses with a custom icon section rather than treating as a bug.

Escaping is the second limitation. Because values pass through `NSUserDefaults` and are parsed as property lists, a message beginning with a bracket or a quote is misread and the notification is not sent. The workaround is a backslash on the first character, and 3.0.0 added a warning when a value could not be read. If your messages are generated from arbitrary input, this is a class of bug you will hit, and the fix has to live in your script, not in the tool.

Finally, there is no Linux or Windows support. The related searches for those platforms describe a need this project does not serve, and the code is Objective-C against Cocoa and UserNotifications frameworks.

## terminal-notifier against osascript and notifier tools

The most direct alternative on macOS is `osascript`, which can post a notification through AppleScript with `display notification`. The difference in approach is architectural: `osascript` is already present on every Mac and needs no installation, but it gives you a limited set of fields and no grouping, no listing, no scheduled delivery and no cancellation. terminal-notifier's value is precisely those stateful features, plus `-contentImage` and the click actions. If all you need is a title and a body, `osascript` avoids a dependency entirely.

A second comparison point comes from the search data rather than the repository: Jamf Notifier appears as a related query. That is a managed-fleet tool for pushing notifications to enrolled Macs, which is a different job from a script notifying its own author. If your requirement is reaching many machines under management, terminal-notifier is the wrong layer.

The Ruby directory in the repository is worth noting too. The README documents using terminal-notifier from Ruby, so if your tooling is already Ruby, the gem route is a genuine option rather than a shell-out. The Makefile's release target also builds a gem alongside the app bundle and the release zip, and it deliberately stops short of publishing unless `PUBLISH=1` is set, on the grounds that publishing cannot be undone.

## Maintenance, licence and upgrade cost

The repository is not archived, and the last push was on 2026-08-30. The release history shows a long gap: 2.0.0 was tagged on 2017-11-01, then 3.0.0 on 2026-08-23 and 3.1.0 on 2026-08-30. The README carries an explicit upgrading section for people coming from 2.x, which tells you the maintainer expects the jump to be disruptive enough to need documentation. Anyone pinned to 2.x should read that section before moving.

The licence is recorded as NOASSERTION, which means the repository's licence metadata does not resolve to a recognised SPDX identifier. The repository does contain a `LICENSE.md` file, so the terms exist in the tree, but the machine-readable field does not say what they are. That is worth reading directly before you ship the binary inside a product. This is not legal advice, and a licence file is the thing to read, not this article.

Upgrade cost is low for the common case. The command line is stable across 3.x, and the 3.0.0 change that matters day to day is the added warning when an option value cannot be read. If you are on Homebrew, `brew upgrade terminal-notifier` is the whole procedure and no quarantine step is involved. If you vendored a downloaded bundle, every upgrade reintroduces the quarantine attribute and the `xattr` step.

## Conclusion

Adopt terminal-notifier if you write shell scripts, CI jobs or Ruby tools on macOS 10.14 or later and want a notification that can carry a title, a subtitle, an image, a click action and a group ID. Do not adopt it if you need notifications on Linux or Windows, if you cannot run xattr on a downloaded copy, or if you want a notarized, signed distribution. Verify first that the machine meets the 10.14 floor, that you are installing through Homebrew or make install so the quarantine attribute never appears, and that any option value starting with a bracket or a quote is escaped with a backslash.

## FAQ

### What is a terminal notifier on a Mac?

It is a command-line tool that sends macOS User Notifications, so a script can post a notification with a title, subtitle, message, sound and image. It ships as an app bundle because macOS attributes a notification to an application. It requires macOS 10.14 or higher.

### What is terminal-notifier?

terminal-notifier is a command-line tool to send macOS User Notifications, invoked as terminal-notifier with one of -message, -remove or -list. It is written in Objective-C and requires macOS 10.14 or higher.

### What is terminal notifier on Mac?

It is a shell-facing front end to the macOS User Notifications API, distributed as an app bundle through Homebrew, a prebuilt binary or a source build. A notification is attributed to an application, which is why it is not a bare executable.

### Is terminal notifier safe?

The tool itself is a wrapper around the macOS notification API, but it can be told to run a shell command when the notification is clicked via -execute. A notification is therefore only as trustworthy as the script that created it. Downloaded copies are quarantined by macOS until the attribute is removed.

### What is the difference between terminal notifier and osascript?

osascript can post a notification through AppleScript and needs no installation, but it does not offer grouping, listing, scheduled delivery or cancellation. terminal-notifier adds those stateful features plus -contentImage and click actions, at the cost of an installed dependency.

## Sources

- [Issues](https://github.com/julienXX/terminal-notifier/issues)
- [julienXX/terminal-notifier on GitHub](https://github.com/julienXX/terminal-notifier)
- [README](https://github.com/julienXX/terminal-notifier/blob/master/README.md)
- [Releases](https://github.com/julienXX/terminal-notifier/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/julienxx-terminal-notifier
