Writing ScriptCat userscripts
ScriptCat runs Tampermonkey-style userscripts and adds two kinds of its own: background scripts and scheduled
scripts, which run in the extension's background sandbox instead of a web page. Most Tampermonkey knowledge
carries over; the differences below are the ones that break scripts.
Authority. The API surface is
— in this repository
, otherwise
https://raw.githubusercontent.com/scriptscat/scriptcat/main/src/types/scriptcat.d.ts. Working examples live in
https://github.com/scriptscat/scriptcat/tree/main/example. The developer docs at
https://docs.scriptcat.org/docs/dev/
can lag behind the types; when they disagree, trust the types.
1. Pick the script type
| The script must… | Type | Key metadata |
|---|
| Change, read or automate a web page | Page script | / , |
| Keep running while the browser is open, with no page (listeners, polling, a menu in the popup) | Background script | |
| Run on a schedule (check-in, sync, report, periodic fetch) | Scheduled script | |
Background and scheduled scripts have
no web page: no
of any site, no
page, no
/
. To touch a page from the background, open it with
and let a page
script do the DOM work, or fetch data with
instead of scraping a rendered page.
2. Start from the right skeleton
These match the templates ScriptCat's own "new script" uses.
js
// ==UserScript==
// @name Example page script
// @namespace https://example.com/your-namespace
// @version 0.1.0
// @description What it does, in one line
// @author You
// @match https://example.com/*
// @grant none
// @noframes
// ==/UserScript==
(function () {
"use strict";
// ...
})();
js
// ==UserScript==
// @name Example background script
// @namespace https://example.com/your-namespace
// @version 0.1.0
// @description What it does, in one line
// @author You
// @background
// @grant GM_xmlhttpRequest
// @grant GM_notification
// ==/UserScript==
return new Promise((resolve, reject) => {
// ... resolve() when the work is really finished, reject(err) on failure
});
A scheduled script is the background skeleton with
(or another expression) in place of
. Background and scheduled bodies are wrapped in a function by ScriptCat, so a top-level
is legal there.
3. Rules that break scripts when ignored
Identity and updates
- + identify a script. Keep both stable once the user has installed it; changing either
makes a different script. Bump on every change you hand back.
- A page script cannot become a background or scheduled script (or the reverse) under the same +
: ScriptCat rejects that install with a script type mismatch. Give a temporary script of the other
type its own .
Permissions
- Every API needs a line (, …). An ungranted API is simply undefined at
runtime. Granting also exposes and vice versa — only when the names match exactly:
needs its own ( does not expose it).
- needs no grant. needs no grant either, but only exists in the sandbox (any grant
other than , or no line at all); is harmless.
- runs the script without the sandbox: is the page's own window, and the only API is
/ — no other GM API and no (referencing it throws a ).
Use it only for scripts that need nothing but the page.
- With any other grant — and also with no line at all, unlike Tampermonkey — the script is sandboxed:
its is not the page's. Reach page globals (a site's own JS objects, variables it defines) through
.
- Declare every host you request with (it also covers the host's subdomains;
covers subdomains of the page's host; requests to exactly the page's host need nothing). For a host that is not
covered, is refused ("This domain is not a part of the @connect list") as soon as the
script has any line, unless the user allowed that host before; only a script with no at
all asks the user instead. in native mode always asks, and throws for a host outside
.
Page timing and SPAs
- : (as early as possible), (DOM parsed), (the
default; everything loaded), (wait until exists), (run when the user
picks the script from the page's right-click menu). Only the first value counts, and an unknown value silently
falls back to the default.
- Add together with when the script must beat the page's own scripts, and wait
for before relying on the full API.
- Single-page apps change the URL without reloading, so a page script runs once and then misses later views.
Listen with
@grant window.onurlchange
and window.addEventListener("urlchange", e => …)
, and use a
for content that renders late. Never assume an element exists at injection time — wait
for it.
- keeps the script out of iframes; without it, a match also runs inside every iframe.
- runs in the content-script world: unaffected by the page's CSP, but
is then not the page's . The default is .
- and bypass the page's CSP; prefer them over creating / by
hand. Exception: on Firefox a injected with is still subject to the page's CSP.
Storage
- stores JSON-like data only (no class instances). Setting deletes the key
(Tampermonkey stores it instead).
- Writes are asynchronous. Before or the end of a background run, /
so the write is not lost.
- lets several scripts share one value store;
GM_addValueChangeListener
sees writes
from the other scripts (and from other tabs) — a simple cross-script channel.
Background and scheduled scripts
- Return a and settle it after the last asynchronous step. Once it resolves or rejects, ScriptCat
treats the run as finished and later GM calls from that run are not guaranteed to work. ScriptCat wraps the
body in an function, so a body that returns nothing is marked complete as soon as its synchronous part
(and any top-level ) finishes — pending callbacks and timers do not keep the run open.
- is logged as success, as an error; the script list then shows the run status
as error, and the message is in the log.
- To retry,
reject(new CATRetryError("reason", seconds))
(or a ). is undefined under
; it exists with any other grant or with no line. The retry queue is checked every 5
seconds, so a retry fires up to 5 seconds late; keep run time plus retry delay shorter than the cron interval
so runs do not overlap.
- Write exactly one line. Every line is scheduled at runtime, but installation validates and the script
list shows only the first, and one invalid extra line stops all of the script's jobs. Use 5 fields (
min hour day month weekday
); a 6-field form with seconds exists but is not recommended. The extension limits a
field's period to one run, counted when the run starts — a failed run uses up the period too, so retry
failures with : = at most once a day, = at most once an hour,
= once in each hour from 9 to 17. Full cron and rules:
references/background.md.
- Log with
GM_log(message, "debug" | "info" | "warn" | "error")
: it lands in ScriptCat's log, the only place
a background script's output shows up — ScriptCat's Logs page (filter by the script's or ), or
. The script list shows only the run status (running / error / stopped) and a scheduled script's
next run time. Release builds do not store , so log anything someone must read at or above.
User configuration
- A
/* ==UserConfig== … ==/UserConfig== */
YAML block after the header gives the script a settings form in
ScriptCat. Read a value with . Format: references/userconfig.md.
4. API lookup
references/api.md lists every GM and CAT API ScriptCat implements, with the grant name, the
cases where ScriptCat differs from Tampermonkey, and which APIs are unavailable in the background. Read it
before using an API you are unsure about, then check the exact signature in
.
references/patterns.md has ready-made building blocks: waiting for an element,
single-page-app route changes, a persistent menu switch, cross-origin requests, styles, localisation, and a
background run with retry.
5. Install and verify
Writing the file is half the job; the other half is seeing it run.
First run the static check bundled with this skill (Node 18+, no dependencies; paths are relative to this
skill's directory):
sh
node scripts/check.mjs my-script.user.js # exit 1 = errors that will break the script at runtime
It reports
error|warn <rule> <file>:<line> <message>
: APIs used without their
, GM APIs or
under
, a page script without
, invalid
expressions, request hosts
missing from
, and similar. Fix every error; read each warning and fix it or say why it does not apply.
It is a static check — it cannot tell whether the script does what the user asked.
- sctl is available ( shows ScriptCat connected): install it into the user's ScriptCat and
verify it in their browser, following references/dev-loop.md. It also covers
scouting the target page before you write code, iterating on a local file without an approval per change, and
running background and scheduled scripts and reading their logs ( / ). That loop runs inside the user's real browser, so the sctl skill's rules apply
too — read it first if it is installed.
- No sctl: save the code as and tell the user to install it — drag the file onto the
ScriptCat dashboard, or create a new script in ScriptCat and paste the code. Say which page to open (page
script) or where to look (background: the run status in the script list, and the script's output on
ScriptCat's Logs page).
Whichever path you take, report what you actually observed: "installed and the button appears on
example.com" is a result; "should work" is not.