Browser Userscript Development (Tampermonkey / Greasemonkey)
Build userscripts that inject UI, filter content, or modify behavior on live websites. Covers both static sites and React/Vue/Angular SPAs.
When to use
- User asks for a Tampermonkey/Greasemonkey script
- User wants to modify a website's behavior (filter, hide, rearrange, add buttons)
- User wants per-site settings persisted across page loads
Script skeleton
// ==UserScript==
// @name Descriptive Name
// @namespace https://target-site.com/
// @version 1.0
// @description One-line description
// @match https://target-site.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
// GM_addStyle(`...`) for injected CSS
// Main logic here
})();
Pitfalls
1. SPA URL detection — the #1 failure mode
SPAs use client-side routing. location.pathname changes without a full page reload. Never match only one URL pattern — the site may have multiple routes for the same logical page.
// BAD: only matches one route
if (location.pathname.includes('/articles')) { ... }
// GOOD: match all known routes for the same page
function isTargetPage() {
const p = location.pathname + location.search;
if (/\/menus\/0(\?|$)/.test(p)) return true; // React SPA route
if (/\/articles/.test(p)) return true; // alternate route
if (/ArticleList\.nhn/i.test(p)) return true; // legacy route
return false;
}
Discovery method: Ask the user for the URL they're on, or use computer_use vision capture to read the address bar. Check for multiple route patterns in the site's navigation.
2. SPA navigation detection
SPAs don't fire load events on navigation. Hook history.pushState + popstate + a MutationObserver URL watcher:
const origPush = history.pushState;
history.pushState = function () {
origPush.apply(this, arguments);
setTimeout(onNav, 300);
};
window.addEventListener('popstate', () => setTimeout(onNav, 300));
let lastUrl = location.href;
new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
onNav();
}
}).observe(document.body, { childList: true, subtree: true });
3. DOM selectors for unknown React/Vue class names
React SPAs use hashed or semantic class names you can't predict. Use a multi-strategy cascade:
- Known structural classes — if user provided HTML (via Inspect → Copy outerHTML), use exact selectors first
- Sidebar/navigation links —
a[href*="menuId"],a[href*="/menus/"]etc. to discover entity names - Attribute-contains selectors —
[class*="article"],[class*="Item"](case variations) - Table fallback —
table tbody trfor list views - Text-content matching — match known labels against
textContentas last resort
Always include a debug mode (floating button that dumps detected rows, selectors, and DOM samples) so the user can report what the script sees without opening DevTools.
4. Getting actual DOM structure when DevTools is blocked
When you can't interact with DevTools via computer_use (Chromium AX tree timeouts, keyboard input not landing):
Ask the user to do this (takes 30 seconds):
- Right-click the target element → Inspect
- In Elements tab, navigate up 2-3 levels to a parent that includes the full region
- Right-click that parent → Copy → Copy outerHTML
- Paste into chat
This gives you exact class names, structure, and insertion points. One outerHTML paste is worth 10 failed computer_use attempts.
5. Per-site settings with GM_setValue
Namespace keys by site identifier (domain, cafe ID, etc.) so settings don't leak across sites:
function getSiteId() {
const m = location.pathname.match(/\/cafes\/(\d+)/);
return m ? m[1] : location.hostname;
}
const hidden = JSON.parse(GM_getValue('hidden_' + getSiteId(), '[]'));
6. UI insertion point
Don't search by text content ("개의 글") — it's fragile. Prefer:
- Exact class selectors from user-provided HTML:
document.querySelector('.sort_area') - Label/for attributes:
document.querySelector('label[for="isNoticeVisible"]') - Insert as first child of the container for left-side placement, or
appendChildfor right-side
7. SPA rendering delays
React/Vue render asynchronously. After navigation, the DOM may not be ready:
let retryCount = 0;
function tryBuild() {
buildUI();
const rows = getRows();
if (rows.length <= 2 && retryCount < 5) {
retryCount++;
setTimeout(tryBuild, 1500);
}
}
computer_use for userscript debugging
When the user has the target page open and wants you to inspect it:
- Try SOM capture first —
computer_use(action='capture', mode='som', pid=PID, window_id=WID) - If SOM times out (common with Chromium-based browsers: Comet, Edge, Brave — heavy UIA trees), fall back to vision mode —
mode='vision' - Don't try to type into DevTools — keyboard input to Chromium DevTools fails in both background and foreground delivery. It's a known limitation.
- Instead: use vision capture to read the page, then ask the user for outerHTML of specific elements (see Pitfall #4)
Verification checklist
- Script runs on the correct URL pattern (test with user's actual URL)
- UI appears at the correct position (user confirms via screenshot)
- Filter/toggle actually hides/shows target elements
- Settings persist across page reload (GM_setValue)
- Settings are per-site (namespaced keys)
- SPA navigation re-triggers the script
- Debug mode available for troubleshooting