Skip to content

Instantly share code, notes, and snippets.

@CTimmerman
Forked from eseiler/README.md
Created September 24, 2025 17:49
Show Gist options
  • Save CTimmerman/105ddef67c3f095f46c0db0a4a4887af to your computer and use it in GitHub Desktop.
Save CTimmerman/105ddef67c3f095f46c0db0a4a4887af 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);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment