Forward Slack notifications recorded on macOS to Discord through an incoming webhook.
Specter runs in your terminal using the Python standard library. It reads the macOS notification database in read-only mode; no custom Slack app or Slack API token is required.
Slack -> macOS notification database -> Specter -> Discord
- macOS and Python 3.10 or later.
- The Slack desktop app, signed in and generating macOS notifications.
- A Discord incoming webhook.
- Access to a compatible macOS notification database.
The database format is undocumented and may change. Specter reads app and record tables and the req.titl, req.subt, and req.body fields in property-list data. Use doctor to check compatibility on your Mac.
Run these commands from the repository directory.
Enable desktop notifications and message previews in Slack, and allow Slack notifications in macOS settings. Choose the activity you want Slack to notify you about. For example, use mentions and DMs as the default and enable all-message notifications for selected channels.
Specter does not independently detect Slack mentions or channel activity. It reads notification records for the configured application bundle identifiers.
Create an incoming webhook for a Discord text channel. Then create and edit the local configuration:
python3 specter.py init
open -e ./config.jsonSet webhook_url to your webhook URL. If config.json already exists, skip init; it refuses to overwrite an existing file.
init creates the configuration with owner-only read/write permissions. config.json is excluded from Git. Keep the webhook URL private.
Receive a Slack notification, then run:
python3 specter.py doctorThis prints the database path, matching record count, Python executable path, and decodable record count. Zero records does not confirm that notification capture works. If records exist but none can be decoded, the command reports an error.
If access is denied, check Full Disk Access permissions for your terminal and Python executable in macOS settings. Restart the terminal after changing permissions. To find the Python executable:
python3 -c 'import pathlib, sys; print(pathlib.Path(sys.executable).resolve())'Send a fixed test message to Discord, then start forwarding:
python3 specter.py test
python3 specter.py runAfter Skipped existing notifications appears, receive a new Slack notification and check Discord. Logs appear in the terminal. Press Ctrl+C to stop, and keep the Mac awake while forwarding. To keep forwarding with the lid closed, see Running with the lid closed.
Closing the lid puts the Mac to sleep unless an external display is connected. While the Mac sleeps, Slack disconnects and posts no notifications, so Specter has nothing to forward. caffeinate cannot prevent sleep when the lid is closed. To keep the system awake with the lid closed, disable sleep system-wide:
sudo pmset -a disablesleep 1
python3 specter.py runRestore normal sleep when you stop forwarding:
sudo pmset -a disablesleep 0The setting persists across restarts until you restore it. Check the current value with pmset -g | grep -i sleepdisabled. disablesleep is not documented in man pmset and may stop working in future macOS releases.
While sleep is disabled:
- Keep the Slack app running and the terminal running
runopen. - Connect the power adapter and avoid enclosed spaces such as bags; a closed, awake Mac can overheat and drain the battery.
- Closing the lid does not reliably lock the screen. Lock it manually with Control+Command+Q.
- Run only
pmsetwithsudo. Runningspecter.pyas root changes the home directory and Full Disk Access target, so the notification database is not found.
Slack must still post notifications while the display is off or the screen is locked. Check the following settings:
- System Settings > Notifications: Allow notifications when the display is sleeping and when the screen is locked. For Slack, turn on Allow notifications and Show in Notification Center.
- System Settings > Focus: Make sure no Focus turns on automatically during forwarding hours, or allow Slack in that Focus.
- Slack > Settings > Notifications: Make sure the notification schedule covers forwarding hours and notifications are not paused.
To verify, start run, send /remind me in 3 minutes test in Slack, close the lid, and check Discord from another device.
The default file is config.json next to specter.py, regardless of the working directory. config.example.json lists all supported options. Unknown keys are rejected.
| Option | Default | Description |
|---|---|---|
webhook_url |
Placeholder; replace for test and run |
Discord incoming webhook URL using HTTPS. |
poll_seconds |
2 |
Delay after each processing cycle, from 0.5 to 60 seconds. Reading and sending add to the time between polls. |
database_path |
null |
Automatically detected; supply a database path to override detection. |
slack_bundle_ids |
["com.tinyspeck.slackmacgap"] |
Non-empty list of application bundle identifiers to read. |
include_regex |
null |
Require a match in the combined notification title, subtitle, and body. |
exclude_regex |
null |
Reject notifications matching this expression. |
Filters use Python regular expressions, are case-sensitive by default, and inspect text rather than Slack channel or workspace IDs. If both filters are set, a notification must match include_regex and must not match exclude_regex.
Use another configuration file by placing --config before the command:
python3 specter.py --config /path/to/config.json runConfiguration is read once per invocation. Restart run to apply changes.
| Command | Purpose |
|---|---|
init |
Create a configuration file without overwriting an existing one. |
doctor |
Read notification records and report decoding counts. |
preview |
Print decodable notifications among the five records with the highest record IDs, including filter matches. |
test |
Attempt to send one fixed Discord connection test message. |
run |
Collect and forward notifications until stopped. |
preview does not send messages. Its output is ordered by record ID, not notification timestamp, and may contain fewer than five notifications if records cannot be decoded.
- The first successful database read in each
runinvocation establishes the baseline. Existing records are skipped; subsequent unseen notification contents can be forwarded. - Only the title, subtitle, and body stored in notification records are forwarded. Specter does not fetch Slack history, recover missing text, or download attachments. Records removed before a poll can be missed.
- Deduplication and pending messages exist only in memory. Deduplication hashes for records no longer in the latest accepted snapshot are discarded. Stopping or restarting loses pending messages and establishes a new baseline.
- There is no workspace-specific selection. Matching records from multiple Slack workspaces are included unless excluded by text filters or Slack notification settings.
- Outgoing messages start with
Slack notification, are split to fit a 2,000 UTF-16-unit limit, and disable Discord mention parsing and link embeds. - The Mac must be awake and the process running to forward notifications. Activity during downtime is not guaranteed to be recovered. Delivery is not guaranteed to occur exactly once: network timeouts, changes to notification text, or records disappearing and reappearing can cause another delivery.
runcreates an empty.specter.lockfile next to the configuration to prevent concurrent runs from the same configuration directory. It does not save notification bodies or create its own notification database. Logs go to the terminal and omit notification text and webhook URLs;previewexplicitly displays notification text.
Database access or decoding fails: Run doctor, check permissions, and verify the database format. Automatic detection checks ~/Library/Group Containers/group.com.apple.usernoted/db2/db, then the com.apple.notificationcenter/db2/db path under getconf DARWIN_USER_DIR. Set database_path if needed. Unsupported records are skipped during run.
Notifications are not forwarded: Check that Slack generates notification records and use preview to inspect text and filter matches. run logs database errors and continues trying; the first successful read still becomes the baseline.
Discord delivery fails: Check the webhook URL and terminal logs. During run, network failures, HTTP 408, and HTTP 5xx responses are retried with increasing delays, up to a 300-second backoff. HTTP 401, 403, and 404 responses are retried no sooner than 300 seconds later. HTTP 429 responses wait at least the reported retry delay, or a fallback delay when it is unavailable. Other HTTP errors drop the affected message and allow later messages to proceed. test makes a single send attempt and reports failures without retrying.
Retries hold the first pending message and delay later deliveries, while subsequent cycles can still collect notifications. If adding a batch would exceed 10,000 pending message parts, that batch is not queued and collection is tried again in later cycles. Records removed before they can be queued are lost. Restarting discards the queue.
python3 -m unittest discover -s tests -vTests use synthetic notification databases and mocked HTTP responses. They do not read your Slack notifications or send messages to Discord.