CODE & STREAM
ABOUT | CONTACT


Killing Rufus for my wife

This morning my wife was shopping for groceries on Amazon.com. As is often the case, it was not going smoothly. Like so much of the technology industry lately software quality continues to decline. All my wife wanted to do was put in her grocery order as she has done many many times before. But today whenever she tried to search for an item, Amazon's Rufus AI Chat bot would pop-up take over half of her screen, and then start streaming related results in a jarring nonstop waste of inference tokens. My wife would close the box, and then a little while later be searching for something else and Rufus would reappear once again taking over half her screen.

From some quick testing on my computer I couldn't repro this issue. In my case I would get a slightly different user experience where my search box would offer a dropdown offering to launch Rufus AI, but was thankfully an opt-in rather than requiring the user to repeatedly opt-out experience. My wife asked Rufus if there was a way to shut it off, it said no. My wife was clearly being A/B tested into an AI hell loop.

This is the type of problem that while I know in theory how to fix it, often I just don't have time. To properly deal with Rufus the easiest way is to analyze the code that is calling the chat bot and prevent it from running.

But with LLMs I can speed up a process that would take me a day of experimentation and compress it down to 30 minutes since there isn't really anything novel about the problem I am trying to solve

The first thing I did was sat down at my wife's laptop and pressed ctrl+U to open a new page with the full source for the page. (Fn+F12 to access the browser developer tools would work too but it has gotten so bloated that I didn't know off the top of my head how to get a clean copy of the entire page's source. The developer tools try focusing on giving you access to individual code blocks whereas I could see from searching through the code that rufus wasn't isolated to single block). So I copied the full page source and e-mailed it to myself.

Back on my own computer I quick tried running Opus 4.5 against the source to pick out the Rufus related pieces but the page source tokens exceeded the Anthropic model's context limit by a significant amount.

To get around the context limit I did a quick ctrl-F of the page source and extracted only the sections that contained Rufus related code. I then passed this code block to the latest claude model and asked it to analyze the code and write me a snippet of JavaScript code that would neutralize Rufus. Opus 4.5 generated the necessary code and I went back to my wife's computer kicked off the developer tools to open the brower's console interface and copied in and executed the code. It successfully killed that instance of Rufus, but the moment you made a new search Amazon would refresh the entire page resummoning Rufus anew, and blowing away my Rufus killing code.

To solve this I had Opus 4.5 instead port my code into a custom Google Chrome extension. The structure of a basic Chrome extension is quite simple so all I needed was two files: (manifest.json, and blocker.js)

manifest.json

{
  "manifest_version": 3,
  "name": "Amazon Rufus Blocker",
  "version": "1.0",
  "description": "Blocks Amazon Rufus AI assistant",
  "permissions": ["storage"],
  "host_permissions": ["*://*.amazon.com/*"],
  "content_scripts": [
    {
      "matches": ["*://*.amazon.com/*"],
      "js": ["blocker.js"],
      "run_at": "document_start",
      "all_frames": true
    }
  ]
}

blocker.js

(function() {
    'use strict';

    // === PART 1: Block sessionStorage for Rufus BEFORE page scripts run ===
    const originalGetItem = Storage.prototype.getItem;
    const originalSetItem = Storage.prototype.setItem;

    Storage.prototype.getItem = function(key) {
        if (this === sessionStorage && key && key.startsWith('rufus:')) {
            return null;
        }
        return originalGetItem.call(this, key);
    };

    Storage.prototype.setItem = function(key, value) {
        if (this === sessionStorage && key && key.startsWith('rufus:')) {
            return;
        }
        return originalSetItem.call(this, key, value);
    };

    // === PART 2: Clean up any existing Rufus storage ===
    try {
        ['rufus:panel:dockedState', 'rufus:panel', 'rufus:previous-page-url', 'rufus:reactStyles'].forEach(key => {
            sessionStorage.removeItem(key);
        });
    } catch(e) {}

    // === PART 3: CSS to hide Rufus immediately ===
    const style = document.createElement('style');
    style.textContent = `
        #nav-rufus-disco,
        #nav-flyout-rufus,
        .rufus-panel-container,
        .rufus-teaser-cx-container,
        .nav-rufus-disco,
        [id*="rufus"],
        [class*="rufus"]:not(body),
        [data-component-type*="rufus"] {
            display: none !important;
            visibility: hidden !important;
            width: 0 !important;
            height: 0 !important;
            position: absolute !important;
            left: -9999px !important;
            pointer-events: none !important;
        }
        body.rufus-docked-left,
        body.rufus-docked-right {
            padding-left: 0 !important;
            padding-right: 0 !important;
            padding-top: 0 !important;
        }
    `;

    // Insert CSS as early as possible
    const insertCSS = () => {
        if (document.head) {
            document.head.appendChild(style);
        } else if (document.documentElement) {
            document.documentElement.appendChild(style);
        }
    };
    insertCSS();

    // === PART 4: Function to remove Rufus elements ===
    const removeRufus = () => {
        // Remove all Rufus-related elements
        const selectors = [
            '#nav-rufus-disco',
            '#nav-flyout-rufus',
            '.rufus-panel-container',
            '.rufus-teaser-cx-container',
            '.nav-rufus-disco',
            '[id*="rufus"]',
            '[class*="rufus"]:not(body)',
            '[data-component-type*="rufus"]'
        ];

        document.querySelectorAll(selectors.join(',')).forEach(el => {
            try {
                el.remove();
            } catch(e) {}
        });

        // Fix body styles
        if (document.body) {
            document.body.classList.remove('rufus-docked-left', 'rufus-docked-right',
                'rufus-docked-opening-transition', 'rufus-docked-closing-transition');
            document.body.style.paddingLeft = '';
            document.body.style.paddingRight = '';
            document.body.style.paddingTop = '';
            document.body.style.removeProperty('--total-rufus-panel-full-width');
            document.body.style.removeProperty('--total-rufus-panel-half-width');
        }
    };

    // === PART 5: Watch for DOM changes and remove Rufus ===
    const observer = new MutationObserver((mutations) => {
        let shouldClean = false;
        for (const mutation of mutations) {
            for (const node of mutation.addedNodes) {
                if (node.nodeType === 1) {
                    const id = node.id || '';
                    const className = node.className || '';
                    if (id.includes('rufus') || className.toString().includes('rufus')) {
                        shouldClean = true;
                        break;
                    }
                }
            }
            if (shouldClean) break;
        }
        if (shouldClean) {
            removeRufus();
        }
    });

    // Start observing as soon as possible
    if (document.documentElement) {
        observer.observe(document.documentElement, {
            childList: true,
            subtree: true
        });
    }

    // === PART 6: Run cleanup at various stages ===
    document.addEventListener('DOMContentLoaded', () => {
        insertCSS();
        removeRufus();
    });

    window.addEventListener('load', () => {
        removeRufus();
    });

    // Also run periodically for the first few seconds
    let cleanupCount = 0;
    const cleanupInterval = setInterval(() => {
        removeRufus();
        cleanupCount++;
        if (cleanupCount > 20) {
            clearInterval(cleanupInterval);
        }
    }, 250);

    // === PART 7: Block P.declare for Rufus ===
    let pValue = window.P;

    const wrapPDeclare = (P) => {
        if (P && P.declare && !P._rufusBlocked) {
            const origDeclare = P.declare;
            P.declare = function(name) {
                if (name && typeof name === 'string' && name.toLowerCase().includes('rufus')) {
                    console.log('[Rufus Blocker] Blocked P.declare:', name);
                    return;
                }
                return origDeclare.apply(this, arguments);
            };
            P._rufusBlocked = true;
        }
        return P;
    };

    if (pValue) {
        wrapPDeclare(pValue);
    }

    Object.defineProperty(window, 'P', {
        configurable: true,
        enumerable: true,
        get: function() {
            return pValue;
        },
        set: function(val) {
            pValue = wrapPDeclare(val);
        }
    });

    console.log('[Rufus Blocker] Extension loaded successfully');
})();

From here I just had to copy the files to a directory on my wife's computer and run the following:

  1. Open Google Chrome
  2. Type this in the address bar and press Enter:
    chrome://extensions
  3. In the top-right corner, turn ON Developer mode (toggle switch)
  4. Click the Load unpacked button (appears after enabling Developer mode)
  5. Navigate to and select your rufus-blocker folder
  6. Click Select Folder
  7. You should see "Amazon Rufus Blocker" appear in your extensions list with a toggle to enable/disable it

And with that Rufus was dead.

As an added bonus, my wife was happy that unlike when I normally say I can fix something quickly, it actually took about how long I said it would. (Historically household projects involving electricity, plumbing, mounting of flat screen tvs, and repairs of farm tractors require taking the provided estimate, and adding 1-10 hours.)

Why Does This Happen?

So why does this happen? Why is Amazon doing this to its' customers? This doesn't feel like the kind of design a company that claims customer obsession is their number one leadership principle would adopt. While I can't say for certain, having spent much of my professional life working for a different member of the magnificent 7 who all have an incestuous habit of swapping employees and having observed similar behavior I have a fairly good idea of how you can get to a place where you are pushing this type of shitty experience on customers.

First for a little background, Amazon describes Rufus as "Amazon's next-gen AI assistant for shopping is now even smarter, more capable, and more helpful." (So helpful in fact that it will just keep appearing unbidden and won't go away.)

They also say that Rufus:

The rationale behind Rufus is two-fold, they want to use Rufus to showcase the amazing apps that enterprise customers and startups can build with generative AI using AWS and Amazon Bedrock, and also want to show that the AI stuff that they and the rest of the industry at large are spending so much money on is actually, maybe, kinda helping you make more money.

The Incentive Problem

So let's imagine for a moment a PM or team of PMs who own the Rufus AI agent. These might be senior PMs with lots of experience, they might be junior PMs eager to show their value, it doesn't really matter. Also imagine that Amazon is desperate to show that they are a leader in AI, and let's just say that things aren't really going as well as they could be right now.

The first goal of Rufus as a showcase of their AI prowess is a little squishy and harder to objectively measure. How often is a user of Rufus so impressed that this then translates into them making a huge AWS or Amazon Bedrock commitment - probably never - but ultimately hard to quantify. So naturally being a good data driven technical organization they focus on things they can measure like how many users engaged with Rufus and maybe create an innocent little boolean flag in the backend system that if a user interacted with Rufus and then made a purchase that this counts as AI influenced revenue rather than regular non AI revenue.

The bonus and promotion budget for this PM also happens to be tied directly to AI revenue for this quarter. This is where things typically start to go horribly wrong. When you have a single or small set of metrics that you are strongly incentivized to make number go up at all costs, and these metrics also happen to be strongly decoupled from questions like: Is this something our customers actually want?, Or is good for our customers?, you find yourself at a place where it might make sense to repeatedly harass a customer with an incessant pop-up that takes up half their screen.

If you truly walk in the darkness you might even go so far as to carefully define AI influenced revenue in such a way that it doesn't actually matter if the customer selected and purchased anything that Rufus suggested. The simple act of interacting (closing) Rufus, or Rufus being present on half the screen, counts as engagement, and everything in that cart is now AI influenced revenue.

To reiterate this is purely hypothetical, I have never seen this exact scenario at any of the companies I have worked at nor would I have any inkling of the internal workings of Amazon's systems. It's just once you see a certain amount of data driven shenanigans to validate a particular hypothesis or to make number go up, you start to develop a knack for intuiting the ways the system is going to be gamed.

You might be thinking that this type of massaging of the data couldn't possibly last, which is somewhat correct. However, the class of lost souls who engage in this type of behavior - if they survive in the industry - often become extremely adept at collecting their bonus/promotion and then moving on to a new role (usually externally) prior to everything coming crumbling down.