{"slug": "userscript-removes-amazon-s-alexa-for-shopping-nee-rufus-ai-sidebar-and-reclaims", "title": "Userscript: removes Amazon's \"Alexa for Shopping\" (née Rufus) AI sidebar and reclaims the reserved gutter. Survives randomized class names, closed shadow DOM, and page-level layout offsets.", "summary": "Developer Giles Hall released a userscript that removes Amazon's 'Alexa for Shopping' (formerly Rufus) AI sidebar and reclaims the reserved gutter space. The script uses multiple layers of defense, including CSS overrides, script blocking, and a geometry-based hunter that adapts to randomized class names and closed shadow DOM, ensuring the sidebar is hidden across all Amazon domains.", "body_md": "| // ==UserScript== | |\n| // @name Amazon: Nuke Alexa/Rufus Shopping Panel | |\n| // @description Hides the \"Alexa for Shopping\" (nee Rufus) AI sidebar on Amazon and reclaims the reserved gutter, whatever mechanism Amazon uses to create it. | |\n| // @namespace https://github.com/gileshall | |\n| // @version 3.0.0 | |\n| // @license MIT | |\n| // @match https://*.amazon.com/* | |\n| // @match https://*.amazon.ca/* | |\n| // @match https://*.amazon.co.uk/* | |\n| // @match https://*.amazon.de/* | |\n| // @match https://*.amazon.fr/* | |\n| // @match https://*.amazon.it/* | |\n| // @match https://*.amazon.es/* | |\n| // @match https://*.amazon.co.jp/* | |\n| // @match https://*.amazon.in/* | |\n| // @match https://*.amazon.com.au/* | |\n| // @match https://*.amazon.com.mx/* | |\n| // @match https://*.amazon.com.br/* | |\n| // @run-at document-start | |\n| // @grant none | |\n| // @updateURL https://gist.githubusercontent.com/gileshall/0213c0568a16eab0fd6c0a7cdbc7239a/raw/amazon-alexa-nuke.user.js | |\n| // @downloadURL https://gist.githubusercontent.com/gileshall/0213c0568a16eab0fd6c0a7cdbc7239a/raw/amazon-alexa-nuke.user.js | |\n| // ==/UserScript== | |\n| (() => { | |\n| 'use strict'; | |\n| // ------------------------------------------------------------------ | |\n| // Config | |\n| // ------------------------------------------------------------------ | |\n| const DEBUG = false; // true -> console.log everything hidden/fixed | |\n| const BLOCK_SCRIPTS = true; // best-effort: neutralize rufus/copilot bootstrap scripts | |\n| const SCRIPT_SRC_RE = /rufus|copilot/i; | |\n| const GUTTER_MIN = 50; // content starting past this = a gutter exists | |\n| const FIX_MIN = 40; // individual offsets larger than this get zeroed | |\n| // Layer-1 fast path: known names as of mid-2026 (internally the widget is | |\n| // still rufus/copilot despite the Alexa rebrand). | |\n| const SELECTORS = [ | |\n| '.rufus-container', | |\n| '.rufus-panel-container', | |\n| '.rufus-chat-container', | |\n| '.rufus-conversation-container', | |\n| '.rufus-conversation-container-inner', | |\n| '.rufus-container-peek-view', | |\n| '.rufus-panel-header-container', | |\n| '.rufus-view-filler', | |\n| '.nav-rufus-disco', | |\n| '.nav-rufus-content', | |\n| '#nav-flyout-rufus', | |\n| '.copilot-modal-container', | |\n| '.copilot-chat-root', | |\n| 'aside[data-copilot-chat-root]', | |\n| 'div[data-copilot-name]', | |\n| '[class*=\"rufus-panel\"]', | |\n| '[class*=\"rufus-chat\"]', | |\n| ]; | |\n| // Things the geometry hunter must NEVER hide. | |\n| const ROOT_IDS = new Set(['a-page', 'pageContent', 'search', 'dp', 'nav-main', 'nav-belt']); | |\n| const PROTECT_RE = /hmenu|s-refinements|a-popover/i; | |\n| const PROTECT_ANCESTOR = '#nav-main, #hmenu-canvas, #hmenu-container, #s-refinements'; | |\n| // ------------------------------------------------------------------ | |\n| // Layer 0: document-start CSS | |\n| // ------------------------------------------------------------------ | |\n| const css = ` | |\n| ${SELECTORS.join(',\\n ')} { | |\n| display: none !important; | |\n| width: 0 !important; | |\n| min-width: 0 !important; | |\n| visibility: hidden !important; | |\n| pointer-events: none !important; | |\n| } | |\n| body[class*=\"rufus\"], body[class*=\"copilot\"] { | |\n| padding-left: 0 !important; | |\n| margin-left: 0 !important; | |\n| } | |\n| html, body, #a-page, #pageContent, #search, #dp, main, [role=\"main\"], | |\n| #search > div, #search > div > div { | |\n| margin-left: 0 !important; | |\n| padding-left: 0 !important; | |\n| left: 0 !important; | |\n| transform: none !important; | |\n| } | |\n| /* pseudo-element spacers flagged by the degutter pass */ | |\n| [data-alexa-degutter]::before { | |\n| content: none !important; | |\n| display: none !important; | |\n| width: 0 !important; | |\n| min-width: 0 !important; | |\n| } | |\n| `; | |\n| const style = document.createElement('style'); | |\n| style.id = 'nuke-alexa-rufus'; | |\n| style.textContent = css; | |\n| (document.head || document.documentElement).appendChild(style); | |\n| // ------------------------------------------------------------------ | |\n| // Helpers | |\n| // ------------------------------------------------------------------ | |\n| const describe = (el) => ({ | |\n| tag: el.tagName, | |\n| id: el.id || null, | |\n| cls: String(el.className || '').slice(0, 100) || null, | |\n| left: Math.round(el.getBoundingClientRect().left), | |\n| width: Math.round(el.getBoundingClientRect().width), | |\n| }); | |\n| const isProtected = (el) => { | |\n| if (!el || !(el instanceof Element)) return true; | |\n| if (el.tagName === 'HTML' || el.tagName === 'BODY') return true; | |\n| if (el.id && ROOT_IDS.has(el.id)) return true; | |\n| if (PROTECT_RE.test(el.id + ' ' + String(el.className || ''))) return true; | |\n| if (el.closest(PROTECT_ANCESTOR)) return true; | |\n| return false; | |\n| }; | |\n| // Hide, don't remove: removal can throw inside Amazon's own code. | |\n| const hide = (el, why) => { | |\n| if (isProtected(el) || el.dataset.alexaNuked === '1') return; | |\n| el.dataset.alexaNuked = '1'; | |\n| el.style.setProperty('display', 'none', 'important'); | |\n| el.style.setProperty('visibility', 'hidden', 'important'); | |\n| el.style.setProperty('pointer-events', 'none', 'important'); | |\n| if (DEBUG) console.log('[alexa-nuke] hide:', why, describe(el)); | |\n| }; | |\n| // ------------------------------------------------------------------ | |\n| // Layer 1: named-selector reaper + body dock-class stripper | |\n| // ------------------------------------------------------------------ | |\n| const COMBINED = SELECTORS.join(','); | |\n| const reapKnown = () => { | |\n| for (const el of document.querySelectorAll(COMBINED)) hide(el, 'selector'); | |\n| const b = document.body; | |\n| if (!b) return; | |\n| for (const c of Array.from(b.classList)) { | |\n| const t = c.toLowerCase(); | |\n| if (t.includes('rufus') || t.includes('copilot')) b.classList.remove(c); | |\n| } | |\n| }; | |\n| // ------------------------------------------------------------------ | |\n| // Layer 2: geometry-based dock hunter (drawn shells, incl. closed | |\n| // shadow hosts -- elementFromPoint returns the host). | |\n| // Technique credit: desrod/disable-amazon-rufus-userscript (Apache-2.0); | |\n| // independent implementation. | |\n| // ------------------------------------------------------------------ | |\n| const scoreDock = (el) => { | |\n| if (isProtected(el)) return -1; | |\n| const r = el.getBoundingClientRect(); | |\n| if (r.width <= 0 || r.height <= 0) return -1; | |\n| if (r.left > 30 || r.width < 200 || r.width > 750 || r.height < 240) return -1; | |\n| const cs = getComputedStyle(el); | |\n| let s = 0; | |\n| if (cs.position === 'fixed' || cs.position === 'sticky') s += 3; | |\n| const z = parseInt(cs.zIndex, 10); | |\n| if (Number.isFinite(z) && z >= 50) s += 2; | |\n| if (r.height > r.width * 1.1) s += 1; | |\n| if (r.top < 140) s += 1; | |\n| if (r.width > innerWidth * 0.9 && r.height > innerHeight * 0.9) s = -10; | |\n| return s; | |\n| }; | |\n| const looksEmpty = (el) => | |\n| (el.innerText || '').trim().length < 5 && | |\n| !el.querySelector('img, input, select, textarea'); | |\n| const effectiveScore = (el) => { | |\n| let s = scoreDock(el); | |\n| if (s > -1 && s < 3 && looksEmpty(el)) s += 2; | |\n| return s; | |\n| }; | |\n| const bestAncestor = (start) => { | |\n| let best = null, bestScore = -1, el = start; | |\n| for (let i = 0; i < 12 && el instanceof Element; i++) { | |\n| const s = effectiveScore(el); | |\n| if (s > bestScore) { bestScore = s; best = el; } | |\n| el = el.parentElement; | |\n| if (!el || isProtected(el)) break; | |\n| } | |\n| return bestScore >= 3 ? { el: best, score: bestScore } : null; | |\n| }; | |\n| const huntDock = () => { | |\n| let best = null, bestScore = -1; | |\n| for (const x of [2, 8, 16, 24]) { | |\n| for (let y = 80; y < innerHeight - 10; y += 90) { | |\n| const hit = document.elementFromPoint(x, y); | |\n| if (!hit) continue; | |\n| const cand = bestAncestor(hit); | |\n| if (cand && cand.score > bestScore) { bestScore = cand.score; best = cand.el; } | |\n| } | |\n| } | |\n| if (best) hide(best, `dock-hunter score=${bestScore}`); | |\n| }; | |\n| // ------------------------------------------------------------------ | |\n| // Layer 3: degutter -- measurement-driven layout reclaim. | |\n| // | |\n| // Symptom: the WHOLE page (nav included) starts hundreds of px from the | |\n| // left, with nothing drawn in the gap. That means the offset lives on a | |\n| // page-level container, produced by any of: margin/padding (possibly | |\n| // inline !important), translateX, positioned left, a ::before spacer, | |\n| // a gutter-occupying sibling, or a grid's first column track. | |\n| // | |\n| // Instead of enumerating selectors, walk from a known content anchor up | |\n| // to <html>, find the parent->child boundary where rect.left jumps past | |\n| // GUTTER_MIN, and neutralize whichever property causes the jump there. | |\n| // Last resort: counter-shift the container by the measured amount. | |\n| // ------------------------------------------------------------------ | |\n| const anchorEl = () => | |\n| document.getElementById('nav-belt') || | |\n| document.getElementById('navbar') || | |\n| document.getElementById('a-page') || | |\n| document.querySelector('#search, #dp, main, [role=\"main\"]'); | |\n| const translateXOf = (cs) => { | |\n| const t = cs.transform; | |\n| if (!t || t === 'none') return 0; | |\n| let m = t.match(/matrix\\(([^)]+)\\)/); | |\n| if (m) return parseFloat(m[1].split(',')[4]) || 0; | |\n| m = t.match(/matrix3d\\(([^)]+)\\)/); | |\n| if (m) return parseFloat(m[1].split(',')[12]) || 0; | |\n| return 0; | |\n| }; | |\n| const degutter = () => { | |\n| const a = anchorEl(); | |\n| if (!a) return; | |\n| if (a.getBoundingClientRect().left < GUTTER_MIN) return; // nothing to fix | |\n| for (let pass = 0; pass < 4; pass++) { | |\n| // ancestor chain, html -> anchor | |\n| const chain = []; | |\n| for (let e = a; e; e = e.parentElement) chain.push(e); | |\n| chain.reverse(); | |\n| // first boundary where the left edge jumps | |\n| let P = null, E = null; | |\n| for (let i = 0; i < chain.length - 1; i++) { | |\n| const pl = chain[i].getBoundingClientRect().left; | |\n| const cl = chain[i + 1].getBoundingClientRect().left; | |\n| if (pl < GUTTER_MIN && cl >= GUTTER_MIN) { P = chain[i]; E = chain[i + 1]; break; } | |\n| } | |\n| if (!E) break; | |\n| let touched = false; | |\n| const pcs = getComputedStyle(P); | |\n| const ccs = getComputedStyle(E); | |\n| if (parseFloat(pcs.paddingLeft) > FIX_MIN) { | |\n| P.style.setProperty('padding-left', '0', 'important'); touched = true; | |\n| } | |\n| if (parseFloat(pcs.borderLeftWidth) > FIX_MIN) { | |\n| P.style.setProperty('border-left', '0', 'important'); touched = true; | |\n| } | |\n| if (parseFloat(ccs.marginLeft) > FIX_MIN) { | |\n| E.style.setProperty('margin-left', '0', 'important'); touched = true; | |\n| } | |\n| if (ccs.position !== 'static' && parseFloat(ccs.left) > FIX_MIN) { | |\n| E.style.setProperty('left', '0', 'important'); touched = true; | |\n| } | |\n| if (translateXOf(ccs) > FIX_MIN) { | |\n| E.style.setProperty('transform', 'none', 'important'); touched = true; | |\n| } | |\n| // ::before spacer on the parent (invisible to elementFromPoint and | |\n| // querySelectorAll) -- flag it; the Layer-0 stylesheet kills it. | |\n| const pb = getComputedStyle(P, '::before'); | |\n| if (pb.content !== 'none' && parseFloat(pb.width) > FIX_MIN) { | |\n| P.setAttribute('data-alexa-degutter', '1'); touched = true; | |\n| } | |\n| // sibling occupying the gutter to our left | |\n| for (let sib = E.previousElementSibling; sib; sib = sib.previousElementSibling) { | |\n| const r = sib.getBoundingClientRect(); | |\n| if (r.width > FIX_MIN && r.left < GUTTER_MIN && r.height > 0) { | |\n| hide(sib, 'gutter-sibling'); touched = true; | |\n| } | |\n| } | |\n| // grid reserving a left track | |\n| if (pcs.display.includes('grid')) { | |\n| const tracks = (pcs.gridTemplateColumns || '').split(' ').filter(Boolean); | |\n| if (tracks.length > 1 && parseFloat(tracks[0]) > FIX_MIN) { | |\n| P.style.setProperty('grid-template-columns', tracks.slice(1).join(' '), 'important'); | |\n| touched = true; | |\n| } | |\n| } | |\n| if (DEBUG && touched) { | |\n| console.log('[alexa-nuke] degutter pass', pass, { parent: describe(P), child: describe(E) }); | |\n| } | |\n| const leftNow = E.getBoundingClientRect().left; | |\n| if (leftNow >= GUTTER_MIN && (!touched || pass === 3)) { | |\n| // Sledgehammer: mechanism unknown -- counter-shift by measurement. | |\n| const n = Math.round(leftNow); | |\n| if (getComputedStyle(E).position === 'static') { | |\n| E.style.setProperty('position', 'relative', 'important'); | |\n| } | |\n| E.style.setProperty('left', `-${n}px`, 'important'); | |\n| E.style.setProperty('width', `calc(100% + ${n}px)`, 'important'); | |\n| if (DEBUG) console.log('[alexa-nuke] degutter sledgehammer', n, describe(E)); | |\n| break; | |\n| } | |\n| if (a.getBoundingClientRect().left < GUTTER_MIN) break; | |\n| } | |\n| }; | |\n| // ------------------------------------------------------------------ | |\n| // Scheduling | |\n| // ------------------------------------------------------------------ | |\n| const sweep = () => { reapKnown(); huntDock(); degutter(); }; | |\n| let queued = false; | |\n| const scheduleSweep = () => { | |\n| if (queued) return; | |\n| queued = true; | |\n| requestAnimationFrame(() => { queued = false; sweep(); }); | |\n| }; | |\n| const mo = new MutationObserver((mutations) => { | |\n| if (BLOCK_SCRIPTS) { | |\n| // Must run synchronously, before the parser-inserted script executes. | |\n| for (const m of mutations) { | |\n| for (const node of m.addedNodes) { | |\n| if (node.tagName === 'SCRIPT' && SCRIPT_SRC_RE.test(node.src || '')) { | |\n| node.type = 'text/blocked'; | |\n| node.remove(); | |\n| } | |\n| } | |\n| } | |\n| } | |\n| scheduleSweep(); | |\n| }); | |\n| mo.observe(document.documentElement, { | |\n| childList: true, | |\n| subtree: true, | |\n| attributes: true, | |\n| attributeFilter: ['class', 'style'], | |\n| }); | |\n| document.addEventListener('DOMContentLoaded', sweep, { once: true }); | |\n| setTimeout(sweep, 300); | |\n| setTimeout(sweep, 1500); | |\n| setInterval(sweep, 3000); | |\n| // ------------------------------------------------------------------ | |\n| // Console diagnostics | |\n| // __alexaNuke.gutter() -> ancestor-chain table: which node carries the | |\n| // offset, and via which property | |\n| // __alexaNuke.scan() -> left-edge dock candidates with scores | |\n| // ------------------------------------------------------------------ | |\n| window.__alexaNuke = { | |\n| sweep, | |\n| degutter, | |\n| gutter() { | |\n| const a = anchorEl(); | |\n| if (!a) { console.log('[alexa-nuke] no anchor element found'); return []; } | |\n| const rows = []; | |\n| for (let e = a; e; e = e.parentElement) { | |\n| const cs = getComputedStyle(e); | |\n| const pb = getComputedStyle(e, '::before'); | |\n| rows.push({ | |\n| tag: e.tagName, | |\n| id: e.id || null, | |\n| cls: String(e.className || '').slice(0, 60) || null, | |\n| left: Math.round(e.getBoundingClientRect().left), | |\n| marginLeft: cs.marginLeft, | |\n| paddingLeft: cs.paddingLeft, | |\n| position: cs.position, | |\n| cssLeft: cs.left, | |\n| transform: cs.transform === 'none' ? null : cs.transform, | |\n| display: cs.display, | |\n| gridCols: cs.display.includes('grid') ? cs.gridTemplateColumns : null, | |\n| beforeWidth: pb.content !== 'none' ? pb.width : null, | |\n| }); | |\n| } | |\n| rows.reverse(); // html first | |\n| console.table(rows); | |\n| return rows; | |\n| }, | |\n| scan() { | |\n| const seen = new Set(); | |\n| const out = []; | |\n| for (const x of [2, 8, 16, 24]) { | |\n| for (let y = 60; y < innerHeight - 10; y += 60) { | |\n| let el = document.elementFromPoint(x, y); | |\n| for (let i = 0; i < 12 && el instanceof Element; i++, el = el.parentElement) { | |\n| if (seen.has(el)) continue; | |\n| seen.add(el); | |\n| const s = effectiveScore(el); | |\n| if (s > 0) out.push({ score: s, ...describe(el) }); | |\n| } | |\n| } | |\n| } | |\n| out.sort((a, b) => b.score - a.score); | |\n| console.table(out); | |\n| return out; | |\n| }, | |\n| }; | |\n| })(); |", "url": "https://wpnews.pro/news/userscript-removes-amazon-s-alexa-for-shopping-nee-rufus-ai-sidebar-and-reclaims", "canonical_source": "https://gist.github.com/gileshall/0213c0568a16eab0fd6c0a7cdbc7239a", "published_at": "2026-08-01 03:19:52+00:00", "updated_at": "2026-08-03 17:30:52.804611+00:00", "lang": "en", "topics": ["developer-tools", "ai-products"], "entities": ["Amazon", "Alexa for Shopping", "Rufus", "Giles Hall"], "alternates": {"html": "https://wpnews.pro/news/userscript-removes-amazon-s-alexa-for-shopping-nee-rufus-ai-sidebar-and-reclaims", "markdown": "https://wpnews.pro/news/userscript-removes-amazon-s-alexa-for-shopping-nee-rufus-ai-sidebar-and-reclaims.md", "text": "https://wpnews.pro/news/userscript-removes-amazon-s-alexa-for-shopping-nee-rufus-ai-sidebar-and-reclaims.txt", "jsonld": "https://wpnews.pro/news/userscript-removes-amazon-s-alexa-for-shopping-nee-rufus-ai-sidebar-and-reclaims.jsonld"}}