Browser Userscript Development (Tampermonkey / Greasemonkey)
Write userscripts that inject UI, filter content, or modify behavior on existing websites. This skill covers the full lifecycle: initial script → DOM discovery → iterative debugging → stable release.
When to load
- User asks for a Tampermonkey / Greasemonkey / Violentmonkey script
- User wants to modify a website's behavior (filter, hide, restyle, add buttons)
- User says "템퍼몽키", "유저스크립트", "userscript", "greasemonkey"
Script skeleton
Every userscript needs this header. @match must cover ALL URL
variants the site uses (see Pitfall #1):
// ==UserScript==
// @name <descriptive name>
// @namespace <site domain>
// @version 1.0
// @description <what it does>
// @match https://example.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @run-at document-idle
// ==/UserScript==
See templates/spa-userscript-skeleton.js for a full SPA-ready
boilerplate with navigation detection, debug panel, and per-site
storage.
Workflow
1. Discover the REAL URL patterns FIRST
Before writing any code, confirm the actual URL the user sees. Do NOT guess URL patterns from the site's marketing or old docs.
- Ask the user for the URL, OR
- Use
computer_use(action="capture", mode="vision")on their browser window and read the address bar from the screenshot.
Common trap: sites migrate to new URL schemes. A site might use
/articles in docs but /f-e/cafes/{id}/menus/0 in the actual SPA.
If your isTargetPage() check doesn't match, the script silently
does nothing — the #1 cause of "nothing changed" reports.
2. Discover DOM structure
For SPAs, you cannot inspect the server HTML — the DOM is built client-side. Options in order of preference:
- computer_use vision: Capture the browser window with
mode="vision", read the page structure from the screenshot. You can see class names in DevTools if the user has them open. - Ask the user: "F12 → right-click the element → Inspect → screenshot the HTML and send it."
- web_extract on the page URL: Sometimes works for server-rendered content, but SPAs return an empty shell.
When using computer_use on Chromium-based browsers (Chrome, Edge, Comet, Brave):
- UIA tree often times out (
Chrome_WidgetWin_1class) → usemode="vision"instead ofmode="som". - Background keystrokes may be blocked → you can't type into DevTools console. Read the URL and visible text from screenshots instead.
- If foreground delivery is also unsupported (older cua-driver), don't waste turns retrying — extract what you need from vision.
3. Build with a debug panel
Always include a debug toggle in v1. A floating 🔍 button that shows:
- Current URL and whether
isTargetPage()matched - Number of DOM rows/elements found
- Extracted names/categories
- First 3 elements'
outerHTML(truncated to 200 chars)
This lets the user send you a screenshot of the debug output instead of trying to describe what's wrong. See the template for implementation.
4. SPA navigation detection
SPAs don't reload the page. You need three hooks:
// 1. Intercept pushState
const origPush = history.pushState;
history.pushState = function () {
origPush.apply(this, arguments);
setTimeout(onNavigate, 300);
};
// 2. Back/forward buttons
window.addEventListener('popstate', () => setTimeout(onNavigate, 300));
// 3. URL change via MutationObserver (catches replaceState too)
let lastUrl = location.href;
new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
onNavigate();
}
}).observe(document.body, { childList: true, subtree: true });
Add a retry loop for initial load — SPA content renders asynchronously:
function tryBuild() {
buildUI();
if (getRows().length <= 2 && retryCount < 5) {
retryCount++;
setTimeout(tryBuild, 1500);
}
}
5. Per-site persistent storage
Use GM_setValue / GM_getValue keyed by site ID so settings
don't leak across sites:
function loadSettings(siteId) {
try { return JSON.parse(GM_getValue('settings_' + siteId, '{}')); }
catch { return {}; }
}
Pitfalls
URL pattern mismatch (THE #1 BUG): The script's page-detection regex doesn't match the actual URL. The script loads on every page of the domain (
@match https://site.com/*) but the internalisTargetPage()check fails silently. ALWAYS verify against the real URL from the user's browser, not from documentation.SPA content not rendered yet:
document-idlefires before React/Vue finishes rendering. UsesetTimeout(1500–2500ms) plus a retry loop, not just a single delay.Class names are hashed in React/CSS-modules: Class names like
Layout_CafeLayout__nUhAinclude hashes that change between deploys. Prefer[class*="article"]substring selectors over exact class matches. Better yet, match on text content orhrefpatterns.Board/category extraction: In list views, the category label is often a small
<span>or<a>with anhrefcontaining a menu/board ID. Extract fromhrefattributes first, fall back to text content. Cross-reference with sidebar navigation links for known category names.Dark mode / extensions: Dark Reader and similar extensions inject
data-darkreader-*attributes and override CSS variables. Your injected UI should use its own hardcoded colors, not inherit from the page.Insertion point: Don't append to
document.bodyfor inline UI. Find a stable anchor element (a heading, a toolbar, a known text string) and insert relative to it. Search by text content:const allEls = document.querySelectorAll('span, div, h1, h2, h3'); for (const el of allEls) { if (el.textContent.includes('known text')) { el.parentElement.insertBefore(myWidget, el); break; } }
Verification
After delivering a script:
- Ask the user to refresh the target page
- If "nothing changed" → the debug panel (🔍) should still appear.
If not even the debug button shows, the
isTargetPage()check is failing — get the real URL. - If debug button shows but no filter UI → DOM selectors are wrong. Ask for a screenshot of the debug panel output.
Site-specific references
references/naver-cafe-dom.md— Naver Cafe (신형/구형) URL patterns, DOM structure, board name extraction strategies.