Last active
January 31, 2026 21:19
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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