Erick Alfaro
← Back to blog

The Power of Tampermonkey

Tampermonkey is a browser extension that runs your own JavaScript on any page you visit. No build step, no deploy, no permissions dance — drop a script in, match it against a URL, and the browser will execute it every time that page loads.

It is the cheapest way to bend the web to your will.

Install in 60 seconds

  1. Install Tampermonkey for your browser. Chrome, Firefox, Edge, Safari, Brave — all supported.
  2. Enable Developer mode. On Chromium-based browsers (Chrome, Edge, Brave) this is the one non-obvious step. Open chrome://extensions/, flip the Developer mode toggle in the top-right corner. Without it, Manifest V3 blocks user scripts from running and Tampermonkey will sit there doing nothing. Firefox and Safari users can skip this.
  3. Click the monkey icon in your toolbar → Create a new script.
  4. Replace the default scaffold with the script you want to run.
  5. Save with Cmd/Ctrl + S.
  6. Visit a page that matches the script's @match URL. It runs automatically.

That is the entire workflow. From idea to "running on every page I visit" in under a minute.

Every script starts with a metadata header. The @match line decides which URLs the script runs on, and @grant declares the privileged APIs you want access to (clipboard, network, DOM injection, etc.):

// ==UserScript==
// @name         My First Script
// @match        https://example.com/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function () {
  console.log("hello from tampermonkey");
})();

That's enough scaffolding to do real damage. Two examples below.

Example 1: Turn Hacker News into a dataset

Hacker News is the perfect first target. The HTML hasn't changed in fifteen years, so the selectors will not betray you. The script below scrapes the front page on every load — title, URL, score, author, age — and renders a small floating panel in the corner showing what it captured, with a button to copy the whole thing as JSON.

// ==UserScript==
// @name         HN Front Page Scraper
// @match        https://news.ycombinator.com/*
// @grant        GM_setClipboard
// @run-at       document-idle
// ==/UserScript==

(function () {
  "use strict";

  const stories = [...document.querySelectorAll("tr.athing")].map((row) => {
    const titleEl = row.querySelector(".titleline > a");
    const meta = row.nextElementSibling?.querySelector(".subtext");
    return {
      id: row.id,
      title: titleEl?.innerText,
      url: titleEl?.href,
      score: parseInt(meta?.querySelector(".score")?.innerText) || 0,
      author: meta?.querySelector(".hnuser")?.innerText,
      age: meta?.querySelector(".age a")?.innerText,
    };
  });

  if (!stories.length) return;

  const panel = document.createElement("div");
  Object.assign(panel.style, {
    position: "fixed", top: "20px", right: "20px", width: "340px",
    padding: "14px", background: "#fff", border: "1px solid #ccc",
    borderRadius: "6px", boxShadow: "0 8px 24px rgba(0,0,0,0.12)",
    zIndex: 999999, fontFamily: "system-ui, sans-serif",
  });

  panel.innerHTML = `
    <div style="font:600 13px system-ui;color:#ff6600;margin-bottom:10px;">
      Scraped ${stories.length} stories
      <span id="hn-x" style="float:right;cursor:pointer;color:#999;">×</span>
    </div>
    <ol style="margin:0;padding-left:22px;font:12px system-ui;color:#333;max-height:280px;overflow:auto;">
      ${stories.slice(0, 10).map((s) => `
        <li style="margin-bottom:6px;">
          <a href="${s.url}" style="color:#000;text-decoration:none;">${s.title}</a>
          <span style="color:#999;">· ${s.score} pts</span>
        </li>`).join("")}
    </ol>
    <button id="hn-copy" style="margin-top:12px;width:100%;padding:7px;
      border:1px solid #ddd;background:#fafafa;cursor:pointer;font:12px system-ui;">
      Copy all ${stories.length} as JSON
    </button>
  `;

  document.body.appendChild(panel);
  panel.querySelector("#hn-x").onclick = () => panel.remove();
  panel.querySelector("#hn-copy").onclick = () => {
    GM_setClipboard(JSON.stringify(stories, null, 2));
    panel.querySelector("#hn-copy").innerText = "Copied ✓";
  };
})();

Save the script, refresh news.ycombinator.com, and a panel appears in the top-right showing the first ten stories with their scores. Click Copy all as JSON and the full dataset — every story on the page — lands on your clipboard. Paste it into a file, pipe it through jq, push it into a database, feed it to a model. Whatever you want.

The pattern is general. Swap the selectors and you can do the same thing on Reddit, Wikipedia, Goodreads, your bank's transaction page, your team's internal dashboard — anything with structured HTML. Every site you visit is implicitly a dataset; this is the cheapest way to start collecting it.

Why this works on JavaScript-heavy sites too

Hacker News is a static-HTML target, which is why it's a clean first example. But most of the modern web — X, Reddit, Notion, your bank dashboard, every Next.js / React app — renders content with JavaScript long after the initial HTML loads. Server-side scrapers like curl and requests choke on these: they fetch the page and find an empty <div id="__next"> with no actual data inside it.

Tampermonkey doesn't have that problem because it runs inside your browser, not against the network. By the time @run-at document-idle fires, every API call has resolved, every component has hydrated, every state update has rendered. You're querying the page the way the user sees it.

When content loads even later — infinite scroll, lazy components, route-level transitions — you reach for MutationObserver:

new MutationObserver(() => {
  document.querySelectorAll("[data-testid='tweet']").forEach(capture);
}).observe(document.body, { childList: true, subtree: true });

The nuclear option is to intercept the network calls themselves. Single-page apps talk to JSON APIs internally; you can patch window.fetch and grab those responses before the page even gets a chance to render them — no DOM parsing required.

const origFetch = window.fetch;
window.fetch = async (...args) => {
  const res = await origFetch(...args);
  if (String(args[0]).includes("/api/items")) {
    res.clone().json().then((data) => console.log("captured:", data));
  }
  return res;
};

And because it all runs inside your browser session, you inherit your cookies, auth tokens, and geo. Paywalls, login walls, regional restrictions: if you can see it as a logged-in user, your script can scrape it.

Example 2: Live charts on every cashtag you hover

Scraping is the passive use case. The active use case is rewriting the page itself — adding UI, replacing content, injecting behavior. Here is a script that watches for cashtags like $NVDA on any page, and when you hover one, pops a live TradingView chart next to your cursor.

// ==UserScript==
// @name         Universal Cashtag Hover Chart
// @match        *://*/*
// @grant        GM_addElement
// @run-at       document-idle
// ==/UserScript==

const CASHTAG_REGEX = /^\$([A-Z][A-Z0-9.]{0,10})$/i;

function getSymbol(el) {
  const text = (el.closest("a")?.innerText || el.innerText || "").trim();
  const match = text.match(CASHTAG_REGEX);
  return match && !/^\d+$/.test(match[1]) ? match[1].toUpperCase() : null;
}

document.addEventListener("mouseover", (e) => {
  const symbol = getSymbol(e.target);
  if (symbol) showChart(symbol, e.clientX, e.clientY);
}, true);

The regex is the interesting part. $1,000 and $NVDA both look like cashtags to a naive matcher, so the rule is: first character after the $ must be a letter, and a token that is all digits after the $ gets rejected. That alone kills almost every false positive on a page like X or Reddit.

The full script — with the floating iframe, hide timers, and edge-of-screen positioning — is here:

Full script on GitHub Gist →

What else this unlocks

Once you accept that you can run code on any page, ideas start showing up:

  • Scrape Redfin while you browse. As you click through listings, dump price, sqft, lot size, and DOM history to a JSON blob or your own endpoint. Build a personal dataset out of a casual house-hunt.
  • Auto-expand X posts. Click every "Show more" the moment it enters the viewport. No more truncated threads.
  • Strip tracking redirects. Rewrite Google and LinkedIn link wrappers back to the real URL before you click.
  • Add keyboard shortcuts to any site. Bind j/k to navigate posts, o to open the first result, whatever you want — even on sites that ship zero shortcuts.
  • One-key download for any embedded video. Pull the <video src> and trigger a save.

None of this needs a server. None of it needs anyone's permission. You write the script, you run the script, you own the behavior.

That is the whole pitch.