- Node.js (v14 or higher)
- GitHub CLI (gh) - Install from https://cli.github.com/
- GitHub authentication - Run
gh auth loginto authenticate
Process all notifications from ghost repositories (deleted/missing repositories):
node github-notification-spam-remover.jsProcess only notifications updated since a specific date:
node github-notification-spam-remover.js --since 2025-01-01-
--since <date>- Only process notifications updated since this date- Date format:
YYYY-MM-DD(e.g.,2025-01-01) - If not provided, processes all available notifications
- Date format:
-
--help- Show help message and exit
Process all notifications from ghost repositories:
node github-notification-spam-remover.jsOnly process notifications from ghost repositories updated since January 1, 2025:
node github-notification-spam-remover.js --since 2025-01-01$ node github-notification-spam-remover.js --since 2025-11-01
Fetching notifications since 2025-11-01...
Fetching notifications...
Fetching page 1...
Fetched 18 notifications from page 1 (total: 18)
Finished fetching 18 total notifications
Found 18 notifications
Checking notification from repo example-org/example-repo...
Repository example-org/example-repo exists: true
Checking notification from repo deleted-org/ghost-repo...
Repository deleted-org/ghost-repo exists: false
Marking notification with thread URL https://api.github.com/notifications/threads/123 read from repo deleted-org/ghost-repo
Marking notification with thread URL https://api.github.com/notifications/threads/123 done from repo deleted-org/ghost-repo
Unsubscribing from notification with thread URL https://api.github.com/notifications/threads/123 from repo deleted-org/ghost-repo
DoneThe script:
- Fetches all your GitHub notifications (with pagination support)
- Checks each notification's repository to see if it still exists
- For notifications from deleted/ghost repositories (404 status):
- Marks the notification as read
- Marks the notification as done
- Unsubscribes from future notifications
- Pagination support: Fetches all pages of notifications (not just the first 50)
- Caching: Repository existence checks are cached to avoid redundant API calls
- Progress logging: Shows progress while fetching and processing notifications
- Debug output: Displays which notifications
Originally from @benjamincburns @ from https://github.com/orgs/community/discussions/6874?sort=new#discussioncomment-14481926
Here's a simple Node JS script that automates the process of detecting phantom notifications and then walking these "ghost notifications" through the read, done, unsubscribe states. To run it, just save it as a .cjs file somewhere and pass the since parameter as the first argument, formatted as a proper ISO 8601 timestamp.
Example execution:
node remove_phantom_notifications.cjs 2025-09-01T00:00:00Z To determine if a notification is from a banned account it first tries fetching the notification's repo details from the GitHub API. If that operation results in a 404, we know that the notification is from a banned account.
I should also note that you'll need the GitHub CLI installed in order for it to grab an auth token so it can authenticate its API requests. If you prefer to use a PAT, you can easily modify the getGithubToken function to return the token however you prefer (e.g. from process.env.GITHUB_TOKEN if you prefer an env var), or just hard code it into the script by replacing the line let _githubToken = null; with let githubToken = "ght..."; // your PAT here.
You'll need a classic PAT with repo and notifications permissions. It needs repo because otherwise it can't distinguish between notifications from private repositories and notifications from banned repositories. It does not perform any repository-level write operations. Unfortunately I couldn't find a way to get it working with a fine-grained PAT.
Edit: Initially this script only marked the notification as "done," but for good measure I've updated it to first mark the notification read, then mark it done, then unsubscribe. I suspect only the "done" or "unsubscribe" step are strictly necessary, but I figured I'd do all three just in case.