dsh-macos-notifications
A Host plugin for DeepSeek Harness that posts native macOS notifications through Notification Center. Clicking a banner brings the Harness window to the front — the same "take me back" behaviour the Dock-attention plugin gives its bouncing icon.
The plugin runs inside the Host process (the Node runtime behind the Desktop app),
watches the durable session/event feed, and hands every alert to a bundled native
helper, DSH Notify. It never writes to the session log and never talks to the model.
Why a native helper
osascript's display notification belongs to the script runner: the banner has no
app identity of its own, so clicking it can only focus the script editor (or reveal it
in Finder). The helper is a tiny LSUIElement app that posts the banner through
UNUserNotificationCenter under its own bundle id. macOS then routes the click back to
the helper, which activates DeepSeek Harness (or launches it, when it is not running)
and quits.
The helper is copied once to
~/Library/Application Support/dsh-macos-notifications/DSH Notify.app — quarantine
flag stripped, ad-hoc signed, registered with LaunchServices — and launched with
open -n -g, so posting a banner never steals focus and the notification permission
survives plugin updates. Request files and the helper log live next to it in
runtime/.
The banner wears the DeepSeek Harness icon: the banner shows the icon of the app
that posted it, so on install the plugin extracts icon.icns from the installed
Harness app, writes it into the helper bundle and signs it. Nothing third-party is
shipped in the repository; if Harness is not installed the configured
targetBundleId app's icon is used, and otherwise the generic app icon.
What triggers a notification
| Moment | Default | Notification |
|---|---|---|
| A turn finished | on | Turn finished / Ход завершён |
| A turn failed | on | Turn failed + the error message |
| A turn hit the token limit | on | Token limit reached |
| An approval is requested | on | Approval needed · <tool> |
The agent asks a question (ask_user_question, exit_plan_mode) |
on | Your answer is needed · <question> |
User-cancelled turns (aborted), forked and interrupted turns are silent. Subagent
sessions are ignored so only the sessions you actually see can ring the bell. The
notification body carries the session title (or, before a title exists, the workspace
folder name).
Configuration
Every field is optional; the row's config mapping is merged over these defaults.
| Field | Default | Meaning |
|---|---|---|
language |
auto |
en, ru, or auto (host locale). |
sound |
Glass |
macOS system sound name or an absolute audio path; null for a silent banner. |
notifyOnTurnEnd |
true |
Notify when a turn completes, fails, or is blocked. |
notifyOnApproval |
true |
Notify when the agent waits for an approval decision. |
notifyOnQuestion |
true |
Notify when the agent asks the user a question. |
targetBundleId |
com.deepseek.dsh |
App a banner click activates. Point it at your browser's bundle id when you run dsh web in a browser. |
openUrl |
dsh://open |
Opened by the helper when targetBundleId is not installed. |
The configured sound is played by the plugin itself while the banner stays silent, so
custom system sound names (Glass, Ping, …) keep working.
- id: macos-notifications
name: dsh-macos-notifications
config:
language: ru
sound: Ping
targetBundleId: com.deepseek.dsh
macOS permission
The first banner triggers a system prompt — "DSH Notify" Would Like to Send You Notifications. Click Allow, and the waiting banner is posted immediately. The answer is cached per bundle id, so this happens once.
The helper waits up to two minutes for the answer; if you miss the prompt, open
System Settings → Notifications → DSH Notify and enable notifications. Without
permission no banners appear, and the helper log says
authorization denied; the user must allow notifications for DSH Notify.
Installing
From GitHub (what the Plugins page manages). Ask the agent to install the bundle, or use the Plugins page with this spec:
git+https://github.com/ihodorchenko/dsh-macos-notifications.git
To pin the release that introduced clickable banners:
github:ihodorchenko/dsh-macos-notifications#v1.1.0
The plugin_manager tool takes the same spec as install_bundle.target. The bundle
patch inserts the macos-notifications row; the package carries its own
dsh.bundle.patch, locale metadata, and icon.
From a local checkout. Point install_bundle at the absolute package directory, or
pass link:<absolute directory> to the profile's package manager.
Without Plugin Manager. The Loader also accepts a plugin row that names a file URL
directly. Add this to $DSH_HOME/profiles/desktop/cordis.patch.yml (or your profile's
patch):
- insert:
- id: macos-notifications
name: 'file:///absolute/path/to/dsh-macos-notifications/index.js'
config:
language: ru
Save the file while the app runs: the profile's HMR watcher recomposes the loaded plugins, so a new row appears without restarting. A YAML error is rejected and the previous configuration keeps running.
Updating and moving
A file: row is not watched by HMR, because the file lives outside the profile
directory. After editing index.js, touch cordis.patch.yml or restart the app so the
module is imported again. Moving or deleting the checkout breaks a file: or link:
install; reinstall the bundle from the new path instead.
Troubleshooting
- No banners at all — check System Settings → Notifications → DSH Notify; the
first run asks for permission and the answer is cached. The helper log lives at
~/Library/Application Support/dsh-macos-notifications/runtime/helper.log. - Clicking a banner does nothing / opens Finder — that is the pre-1.1.0
osascriptbuild (banners belong to the script runner). Update to 1.1.0+, where the banner belongs to DSH Notify and the click activates Harness. - Click activates the wrong app — set
targetBundleId(browser bundle id fordsh web) and restart the app. - No icon appears in the Dock — by design: DSH Notify is an
LSUIElementagent, it only exists to own banners and handle clicks.
Development
node --test # 8 unit tests (config, sound, helper bundle, icon)
bash tools/build-helper.sh # rebuild bin/DSH Notify.app (needs Xcode CLT)
node tools/self-check.mjs # post a real banner and check the helper log
The helper source is small enough to audit: posting, the permission poll, and the
click/activation forwarding are all in src/helper/main.swift.
Files
| File | Role |
|---|---|
index.js |
Host plugin: session-event listeners, message rendering, helper install and launch |
bin/DSH Notify.app |
Native notification helper (universal binary, ad-hoc signed, LSUIElement) |
src/helper/main.swift |
Helper source; tools/build-helper.sh rebuilds the bundle |
tools/self-check.mjs |
Posts a real banner and reports the helper log tail |
~/Library/Application Support/dsh-macos-notifications/ |
Installed helper copy, one-shot runtime/requests/ and runtime/helper.log |
cordis.patch.yml |
Bundle patch that inserts the macos-notifications row |
locale/en.json, locale/ru.json |
Plugin Manager display metadata |
icon.svg |
Plugin icon |
License
MIT — see LICENSE. The helper is ad-hoc signed (no Apple Developer identity), so it carries no notarization; it is installed from the plugin package and needs no quarantine escape hatch.
No comments yet. Be the first to write one.