# Browser Userscripts

> Use when building Tampermonkey/Greasemonkey userscripts.

- Skill: `wcpaka-lgtm/browser-userscripts` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add wcpaka-lgtm/browser-userscripts`
- Raw SKILL.md: https://api.skillmd.com/api/skills/wcpaka-lgtm/browser-userscripts/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: wcpaka-lgtm (https://skillmd.com/u/wcpaka-lgtm)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/wcpaka-lgtm/browser-userscripts

---


# 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

```javascript
// ==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.

```javascript
// 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:

```javascript
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**:

1. **Known structural classes** — if user provided HTML (via Inspect → Copy outerHTML), use exact selectors first
2. **Sidebar/navigation links** — `a[href*="menuId"]`, `a[href*="/menus/"]` etc. to discover entity names
3. **Attribute-contains selectors** — `[class*="article"]`, `[class*="Item"]` (case variations)
4. **Table fallback** — `table tbody tr` for list views
5. **Text-content matching** — match known labels against `textContent` as 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):
1. Right-click the target element → **Inspect**
2. In Elements tab, navigate up 2-3 levels to a parent that includes the full region
3. Right-click that parent → **Copy → Copy outerHTML**
4. 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:

```javascript
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 `appendChild` for right-side

### 7. SPA rendering delays

React/Vue render asynchronously. After navigation, the DOM may not be ready:

```javascript
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:

1. **Try SOM capture first** — `computer_use(action='capture', mode='som', pid=PID, window_id=WID)`
2. **If SOM times out** (common with Chromium-based browsers: Comet, Edge, Brave — heavy UIA trees), **fall back to vision mode** — `mode='vision'`
3. **Don't try to type into DevTools** — keyboard input to Chromium DevTools fails in both background and foreground delivery. It's a known limitation.
4. **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

