Skip to content

Instantly share code, notes, and snippets.

@StrangeRanger
Last active January 31, 2026 21:19
Show Gist options
  • Select an option

  • Save StrangeRanger/f1021caf369a9ce6148e2341f0c5f4cf to your computer and use it in GitHub Desktop.

Select an option

Save StrangeRanger/f1021caf369a9ce6148e2341f0c5f4cf to your computer and use it in GitHub Desktop.
Easily remove bookmarks en masse on Twitter/X via the browser's developer tools console.
// Easily remove bookmarks en masse on Twitter/X via the browser's developer tools console.
//
// NOTE: If you have many bookmarks, you WILL encounter the "429 (Too Many Requests)" error.
// The best way to resolve this is use a VPN to change your IP while still logged into your existing account.
//
// NOTE 2: Code was generated by ChatGPT and modified by me.
const bulkUnbookmark = (() => {
let running = false;
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// Try several selectors because Twitter/X changes labels frequently.
function findRemoveButtons() {
const selectors = [
// Common aria-label variants.
'button[aria-label="Remove Bookmark"]',
'button[aria-label="Remove bookmark"]',
'button[aria-label="Remove from Bookmarks"]',
'button[aria-label="Remove from bookmarks"]',
// Sometimes it's a menu item (less common).
'[role="menuitem"][data-testid="removeBookmark"]',
// Fallback: some builds use testids.
'button[data-testid="removeBookmark"]',
// Very broad fallback: any button whose aria-label contains "Remove" + "Bookmark".
'button[aria-label*="Remove"][aria-label*="Bookmark"]',
'button[aria-label*="Remove"][aria-label*="bookmark"]',
];
for (const sel of selectors) {
const nodes = Array.from(document.querySelectorAll(sel));
if (nodes.length > 0) return nodes;
}
return [];
}
async function clickButtons(buttons, delayMs) {
// Click from bottom up to reduce layout shifting surprises.
for (const btn of buttons.reverse()) {
if (!running) return;
btn.click();
await sleep(delayMs);
}
}
async function loop({
clickDelayMs = 250,
roundDelayMs = 800,
maxEmptyRounds = 8,
} = {}) {
let emptyRounds = 0;
while (running) {
const buttons = findRemoveButtons();
if (buttons.length === 0) {
emptyRounds += 1;
if (emptyRounds >= maxEmptyRounds) {
console.log("[bulkUnbookmark] No more remove buttons found; stopping.");
running = false;
break;
}
await sleep(roundDelayMs);
continue;
}
emptyRounds = 0;
console.log(`[bulkUnbookmark] Found ${buttons.length} remove buttons. Clicking...`);
await clickButtons(buttons, clickDelayMs);
// Give the UI time to re-render/remove items
await sleep(roundDelayMs);
}
}
return {
start: (opts) => {
if (running) return console.log("[bulkUnbookmark] Already running.");
running = true;
console.log("[bulkUnbookmark] Starting...");
loop(opts).catch(err => {
running = false;
console.error("[bulkUnbookmark] Error:", err);
});
},
stop: () => {
running = false;
console.log("[bulkUnbookmark] Stopped.");
},
};
})();
bulkUnbookmark.start();
// Use the below line to stop the script when needed.
// bulkUnbookmark.stop();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment