# 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.

> Source: <https://gist.github.com/gileshall/0213c0568a16eab0fd6c0a7cdbc7239a>
> Published: 2026-08-01 03:19:52+00:00

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