Skip to content

Instantly share code, notes, and snippets.

@eseiler
Last active October 22, 2025 23:53
Show Gist options
  • Save eseiler/d3a6f978ceea1853f0c558879cf859c1 to your computer and use it in GitHub Desktop.
Save eseiler/d3a6f978ceea1853f0c558879cf859c1 to your computer and use it in GitHub Desktop.
Remove Phantom Notifications

Remove Phantom Notifications on GitHub

This tool helps you clean up GitHub notifications from repositories that no longer exist ("phantom notifications"), which can clutter your notification feed.

The Node.js script comes from this GitHub Community discussion with minor modifications to console output.

Compatibility

  • The Node.js script works on any platform that supports Node.js
  • The integration shown in these instructions uses bash as the shell, but the concepts can be adapted to other shells (zsh, fish, etc.)
  • These shell examples are designed for GNU/Linux, macOS, or other Unix-like systems

Requirements

  • Node.js
  • GitHub CLI (gh) with completed authentication setup (verify with gh auth status, Token scopes must contain repo)

Installation

  1. Place the remove_notifications.cjs script in a directory of your choice. These instructions use /opt/node/ as an example.
  2. Add the following function to your ~/.bashrc or equivalent shell configuration file:
RemoveNotifications() {
    SINCE="${*:-yesterday}"
    FORMATTED_DATE=$(date --utc --date "${SINCE}" "+%Y-%m-%dT00:00:00Z")
    MESSAGE=$(date --utc --date "${FORMATTED_DATE}" "+%Y-%m-%d")
    echo "Removing all phantom notifications since ${MESSAGE}"
    node /opt/node/remove_notifications.cjs "${FORMATTED_DATE}"
}
  1. If you placed the script somewhere other than node, update the path in the function accordingly
  2. Reload your shell configuration with source ~/.bashrc or open a new terminal session

Usage

RemoveNotifications [date_expression]
  • date_expression: Optional. Any date string understood by the date command (defaults to "yesterday" if omitted)

Examples

# Remove phantom notifications since yesterday
RemoveNotifications

# Remove phantom notifications since 7 days ago
RemoveNotifications 7 days ago

# Remove phantom notifications since a specific date
RemoveNotifications 2025-09-01

Example output

$ RemoveNotifications
Removing all phantom notifications since 2025-09-22
  Processing repository "gitcoiindao/orgs" (https://api.github.com/notifications/threads/XXXXXXXXXXX)
   Marking notification read
   Marking notification done
   Unsubscribing from repo
Done
// Source: https://github.com/orgs/community/discussions/6874#discussioncomment-14481926
// Adapted console output (console.log and console.error)
const { exec } = require("node:child_process");
const { basename } = require("node:path");
function runShellCommand(command) {
return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
reject({ error, stderr });
return;
}
resolve(stdout);
});
});
}
let _githubToken = null;
async function getGithubToken() {
if (!_githubToken) {
_githubToken = await runShellCommand("gh auth token");
}
return _githubToken;
}
async function getNotifications(since) {
const response = await fetch(`https://api.github.com/notifications?all=true&since=${since}`, {
headers: {
'Accept': 'application/vnd.github+json',
'Authorization': `Bearer ${await getGithubToken()}`,
'X-GitHub-Api-Version': '2022-11-28',
},
});
return response.json();
}
async function shouldIncludeNotificationForRemoval(notification) {
try {
const response = await fetch(`https://api.github.com/repos/${notification.repository.full_name}`, {
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${await getGithubToken()}`,
"X-GitHub-Api-Version": "2022-11-28",
},
});
return response.status === 404;
} catch (error) {
console.log("threw");
if (error.code && error.code === 404) {
return true;
}
console.error(error);
throw error;
}
}
async function markNotificationRead(notification) {
const response = await fetch(notification.url, {
method: "PATCH",
headers: {
"Authorization": `Bearer ${await getGithubToken()}`,
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
});
if (!response.ok) {
console.error(` Failed to mark notification with thread URL ${notification.url} from repo ${notification.repository.full_name} as read: ${response.status} ${response.statusText}`);
}
}
async function markNotificationDone(notification) {
const response = await fetch(notification.url, {
method: "DELETE",
headers: {
"Authorization": `Bearer ${await getGithubToken()}`,
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
});
if (!response.ok) {
console.error(` Failed to mark notification with thread URL ${notification.url} from repo ${notification.repository.full_name} as done: ${response.status} ${response.statusText}`);
}
}
async function unsubscribe(notification) {
const response = await fetch(notification.subscription_url, {
method: "DELETE",
headers: {
"Authorization": `Bearer ${await getGithubToken()}`,
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
});
if (!response.ok) {
console.error(` Failed to unsubscribe from notification with thread URL ${notification.url} from repo ${notification.repository.full_name}: ${response.status} ${response.statusText}`);
}
}
async function main() {
const since = process.argv[2];
if (!since) {
console.error(` Usage: ${basename(process.argv[0])} ${basename(process.argv[1])} <since>`);
process.exit(1);
}
try {
new Date(since);
} catch (error) {
console.error(` ${since} is not a valid ISO 8601 date. Must be formatted as YYYY-MM-DDTHH:MM:SSZ.`);
console.error(` Usage: ${basename(process.argv[0])} ${basename(process.argv[1])} <since>`);
process.exit(1);
}
const notifications = await getNotifications(since);
for (const notification of notifications) {
if (await shouldIncludeNotificationForRemoval(notification)) {
console.log(` Processing repository "${notification.repository.full_name}" (${notification.url})`)
console.log(` Marking notification read`);
await markNotificationRead(notification);
console.log(` Marking notification done`);
await markNotificationDone(notification);
console.log(` Unsubscribing from repo`);
await unsubscribe(notification);
}
}
console.log("Done");
}
main().catch(console.error);
@Nezteb
Copy link

Nezteb commented Sep 25, 2025

I ran into this issue as well; here's a similar option: https://github.com/orgs/community/discussions/174310#discussioncomment-14514685

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment