Skip to content

Instantly share code, notes, and snippets.

@marcomontalbano
Last active September 27, 2025 15:40
Show Gist options
  • Select an option

  • Save marcomontalbano/366b485be082cec29876d5aaf0346b27 to your computer and use it in GitHub Desktop.

Select an option

Save marcomontalbano/366b485be082cec29876d5aaf0346b27 to your computer and use it in GitHub Desktop.

GitHub - Remove phantom notifications

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.

https://github.com/orgs/community/discussions/6874?sort=new#discussioncomment-14481926

Thanks a lot @benjamincburns for this super helpful script! Really appreciated 🙏

Usage

First of all you'll need the GitHub CLI installed and then login with gh auth login.

Run this deno command first:

deno run -r\
  --allow-net\
  --allow-env\
  --allow-run\
  https://gist.githubusercontent.com/marcomontalbano/366b485be082cec29876d5aaf0346b27/raw/remove_phantom_notifications.js\
  2025-09-01T00:00:00Z
import { exec } from "node:child_process";
import { basename } from "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(`Marking notification with thread URL ${notification.url} read from repo ${notification.repository.full_name}`);
await markNotificationRead(notification);
console.log(`Marking notification with thread URL ${notification.url} done from repo ${notification.repository.full_name}`);
await markNotificationDone(notification);
console.log(`Unsubscribing from notification with thread URL ${notification.url} from repo ${notification.repository.full_name}`);
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