GM API Support Across Userscript Managers

Oct 26, 2026Li LaiqunLi Laiqun

About the author: Tooling engineer, formerly at Qualcomm and MediaTek. Builds MonkeyMom, an AI userscript factory for desktop and mobile managers.

A probe of 57 GM APIs across four userscript managers — Violentmonkey, Via, Stay for Safari and Userscripts — plus what MonkeyMom implements itself.

Every userscript manager implements the GM API from its own reading of the spec. The names look the same, so a script that works in one manager is assumed to work in the others. In practice it often does not, and the failures are quiet: a function exists but returns nothing, a callback never fires, a listener has no id so it can never be removed.

MonkeyMom has to run scripts it did not write, on managers it does not control. Before the child-script bridge could be designed, we needed to know what the ground actually looks like. So we probed 57 API names across four managers.

How the probe works

What each probe records

What is checkedHowWhat failure looks like
Existence and typetypeof on the identifierundefined, or a function that is a silent no-op
Call resultThe call is made and the value capturedA promise, an element, undefined, or a thrown error
Async callbacksFire the call, wait with a timeoutThe callback never fires, or fires after the timeout
Return-value typeThe resolved value is inspectedA promise where an element is expected, or the reverse

Why documentation is not enough

The documentation describes intent; what a script sees is the implementation. One name can behave four different ways.

APIViolentmonkeyViaStay for Safari
GM.openInTabhandle with close()undefinedPromise resolving to a tab object with no close()
GM.addStylePromise → <style><style> elementselector string
GM_xmlhttpRequestcallback stylecallback stylecallback style
GM_listValuesthis script's keyskeys from every scriptthis script's keys

Reading the docs would have produced one assumption and three surprises.

Probe data

The probe covers 57 API names. Below are the per-manager summary, the support matrix, the return-type differences and what MonkeyMom synthesizes, kept as a reference for writing scripts.

Summary

Totals per manager, with the full name list of every API that passed.

ManagerPassed / 57APIs that passed
暴力猴48 / 57GM_setValue
GM_getValue
GM_deleteValue
GM_listValues
GM_setValues
GM_getValues
GM_deleteValues
GM_addValueChangeListener
GM_removeValueChangeListener
GM_addStyle
GM_addElement
GM_openInTab
GM_setClipboard
GM_notification
GM_download
GM_registerMenuCommand
GM_unregisterMenuCommand
GM_xmlhttpRequest
GM_webRequest
GM_cookie.set
GM_cookie.list
GM_cookie.delete
GM_audio.removeStateChangeListener
GM_getResourceText
GM_getResourceURL
GM_info
GM_log
unsafeWindow
GM.info
GM.setValue
GM.getValue
GM.deleteValue
GM.setValues
GM.getValues
GM.deleteValues
GM.listValues
GM.addStyle
GM.addElement
GM.notification
GM.download
GM.registerMenuCommand
GM.unregisterMenuCommand
GM.getResourceUrl
GM.getResourceText
GM.openInTab
GM.closeTab
GM.setClipboard
GM.xmlHttpRequest
Via41 / 57GM_setValue
GM_getValue
GM_deleteValue
GM_listValues
GM_setValues
GM_getValues
GM_deleteValues
GM_addStyle
GM_addElement
GM_openInTab
GM_setClipboard
GM_notification
GM_download
GM_registerMenuCommand
GM_unregisterMenuCommand
GM_xmlhttpRequest
GM_webRequest
GM_audio.removeStateChangeListener
GM_getResourceText
GM_getResourceURL
GM_info
GM_log
unsafeWindow
GM.info
GM.setValue
GM.getValue
GM.deleteValue
GM.setValues
GM.getValues
GM.deleteValues
GM.listValues
GM.addStyle
GM.addElement
GM.notification
GM.registerMenuCommand
GM.unregisterMenuCommand
GM.getResourceUrl
GM.openInTab
GM.closeTab
GM.setClipboard
GM.xmlHttpRequest
Stay for Safari44 / 57GM_setValue
GM_getValue
GM_deleteValue
GM_listValues
GM_addValueChangeListener
GM_removeValueChangeListener
GM_saveTab
GM_getTab
GM_getTabs
GM_addStyle
GM_addElement
GM_openInTab
GM_setClipboard
GM_notification
GM_download
GM_registerMenuCommand
GM_unregisterMenuCommand
GM_xmlhttpRequest
GM_webRequest
GM_cookie.set
GM_cookie.list
GM_cookie.delete
GM_audio.removeStateChangeListener
GM_getResourceText
GM_getResourceURL
GM_info
GM_log
unsafeWindow
GM.info
GM.setValue
GM.getValue
GM.deleteValue
GM.listValues
GM.addStyle
GM.addElement
GM.notification
GM.registerMenuCommand
GM.unregisterMenuCommand
GM.getResourceUrl
GM.openInTab
GM.closeTab
GM.setClipboard
GM.saveTab
GM.xmlHttpRequest
Userscripts for Safari22 / 57GM_setValue
GM_getValue
GM_deleteValue
GM_listValues
GM_saveTab
GM_addStyle
GM_openInTab
GM_setClipboard
GM_xmlhttpRequest
GM_info
GM.info
GM.setValue
GM.getValue
GM.deleteValue
GM.listValues
GM.addStyle
GM.openInTab
GM.closeTab
GM.setClipboard
GM.saveTab
GM.getTab
GM.xmlHttpRequest

Support matrix

APIViolentmonkeyViaStay for SafariUserscripts
GM_setValue✅✅✅✅
GM_getValue✅✅✅✅
GM_deleteValue✅✅✅✅
GM_listValues✅✅✅⚠️ returns {} (GM.listValues works)
GM_setValues✅✅❌❌
GM_getValues✅✅❌❌
GM_deleteValues✅✅❌❌
GM_addValueChangeListener✅❌✅❌
GM_removeValueChangeListener✅❌✅❌
GM_saveTab❌❌✅✅
GM_getTab❌❌✅⚠️ timeout
GM_getTabs❌❌✅❌
GM_addStyle✅✅✅✅
GM_addElement✅✅✅❌
GM_openInTab✅✅✅✅
GM_setClipboard✅✅✅✅
GM_notification✅✅✅❌
GM_download✅✅✅❌
GM_registerMenuCommand✅✅✅❌
GM_unregisterMenuCommand✅❌✅❌
GM_xmlhttpRequest✅✅✅✅
GM_webRequest⚠️ no-op⚠️ no-op⚠️ no-op❌
GM_cookie.set✅❌✅❌
GM_cookie.list✅❌✅❌
GM_cookie.delete✅❌✅❌
GM_audio.setMute❌❌❌❌
GM_audio.getState❌❌❌❌
GM_audio.addStateChangeListener❌❌❌❌
GM_audio.removeStateChangeListener⚠️ joint test⚠️ joint test⚠️ joint test❌
GM_getResourceText✅✅✅❌
GM_getResourceURL✅✅✅❌
GM_info✅✅✅✅
GM_log✅✅✅❌
unsafeWindow✅✅✅❌
GM.info✅✅✅✅
GM.setValue✅✅✅✅
GM.getValue✅✅✅✅
GM.deleteValue✅✅✅✅
GM.setValues✅✅❌❌
GM.getValues✅✅❌❌
GM.deleteValues✅✅❌❌
GM.listValues✅✅✅✅
GM.addStyle✅✅✅✅
GM.addElement✅✅✅❌
GM.notification✅✅✅❌
GM.download✅❌❌❌
GM.cookie⚠️ timeout❌⚠️ timeout❌
GM.registerMenuCommand✅⚠️ cannot unregister✅❌
GM.unregisterMenuCommand✅❌✅❌
GM.getResourceUrl✅✅✅❌
GM.getResourceText✅❌❌❌
GM.openInTab✅✅✅✅
GM.closeTab✅✅✅✅
GM.setClipboard✅✅✅✅
GM.saveTab❌❌✅✅
GM.getTab❌❌⚠️ throws✅
GM.xmlHttpRequest✅✅✅✅

Totals: Violentmonkey 48/57, Stay for Safari 44/57, Via 41/57, Userscripts 22/57.

Return types

What a function returns differs as much as whether it exists. For cross-manager scripts this table matters more than the support matrix.

APIViolentmonkeyViaStay for Safari
GM.addStylePromise → <style>Promise → <style>Promise → selector string
GM_addStylePromise → <style><style> element<style> element
GM.addElementPromise → <div>Promise → <div><div> directly
GM_addElementPromise → <div><div> element<div> element
GM.xmlHttpRequestPromise (never await)undefined{abort} object
GM_xmlhttpRequest{abort} objectundefined{abort} object
GM.openInTab{onclose, closed, close}undefinedPromise → tab object
GM_openInTab{onclose, closed, close}undefinedundefined
GM_download{abort} objectno callbackundefined

GM.* versus GM_

New scripts should prefer GM.*; the synchronous form only matters for old scripts.

Async formSync form
GM.setValueGM_setValue
GM.getValueGM_getValue
GM.deleteValueGM_deleteValue
GM.listValuesGM_listValues
GM.addStyleGM_addStyle
GM.addElementGM_addElement
GM.setClipboardGM_setClipboard
GM.xmlHttpRequestGM_xmlhttpRequest
GM.getResourceUrlGM_getResourceURL
GM.infoGM_info
GM.openInTabGM_openInTab
—GM_download
—GM_getResourceText

What MonkeyMom synthesizes

These APIs do not depend on the manager; MonkeyMom provides them inside the child-script environment.

APIMonkeyMom implementation
GM_getResourceTextReads the prefetched @resource cache
GM_getResourceURLWraps cached text in a page-side blob URL
GM.getResourceUrlCache hit returns immediately, miss fetches through the bridge
GM_registerMenuCommandRegisters a button under the script row in the panel
GM_unregisterMenuCommandRemoves the button by command id
GM_notificationIn-page toast with onclick / ondone

Deliberately not implemented: GM_webRequest, unsafeWindow, GM.closeTab, GM_audio.*, GM.cookie.

Where the managers actually differ

The spread is not random. It clusters by feature family, and each family fails in its own way.

Storage

BehaviourViolentmonkeyViaStay for SafariUserscripts
Basic four (get / set / delete / list)✅✅✅✅
Batch forms (setValues / getValues / deleteValues)✅✅❌❌
GM.listValues returns this script's keys✅❌ shared across scripts✅✅
GM_listValues (classic sync form)✅✅✅⚠️ returns {}

Via's storage is shared across all scripts: two scripts using the key state overwrite each other unless they namespace it by hand. On Userscripts the split runs inside a single manager: the async GM.listValues returns the real key names, while the classic GM_listValues returns {} — worse than absent, because it looks supported.

Listeners and tabs

BehaviourViolentmonkeyViaStay for Safari
GM_addValueChangeListener returns an id✅❌ undefined✅
GM_removeValueChangeListener accepts that id✅❌✅
GM_saveTab / GM_getTab / GM_getTabs❌❌✅

A listener without an id is registered forever. That alone is enough reason for the bridge not to depend on the manager's listener API.

Style and element injection

BehaviourViolentmonkeyViaStay for SafariUserscripts
GM_addStyle✅✅✅✅
GM_addElement✅✅✅❌ missing
GM.addStyle returnsPromise → <style>Promise → <style>Promise → selector stringnot probed
GM_addStyle returnsPromise → <style><style> element<style> elementnot probed
GM.addElement returnsPromise → <div>Promise → <div><div> directlynot probed

Userscripts has no GM_addElement at all. That is fatal for scripts that load code dynamically on a strict Content Security Policy: without the manager's help there is no DOM path around CSP.

Network, download, clipboard and tabs

BehaviourViolentmonkeyViaStay for Safari
GM_xmlhttpRequest✅ callback style✅ callback style✅ callback style
GM.download✅ returns {abort}❌ missing; callbacks never fire✅ returns undefined
GM.setClipboard✅✅✅
GM.openInTab opens a background tab✅✅✅
GM.openInTab returnshandle with close()undefinedPromise → tab object

Menus, notifications and cookies

BehaviourViolentmonkeyViaStay for Safari
GM_registerMenuCommand✅✅✅
GM_unregisterMenuCommand✅❌✅
GM_notification actually shows✅✅✅ (Edge needs the permission)
GM.cookie.set / list / delete✅❌✅
GM_getResourceText when the resource is missingundefinednullundefined

What MonkeyMom implements itself

The bridge runs the child script as an injected script with a synthetic GM object. What the manager does reliably is passed through; what is missing, broken or inconsistent is implemented by MonkeyMom.

APIWhat the managers doWhat MonkeyMom does
GM_getResourceText / GM_getResourceURLNeed an install step to download @resource filesParses the metadata, prefetches through GM.xmlHttpRequest with a 15 s cap, reads the cache synchronously
GM.getResourceUrlSame split, plus null versus undefinedCache hit resolves at once; a miss fetches through the bridge
GM_registerMenuCommand / GM_unregisterMenuCommandNo menu to click on mobile; Via cannot unregisterA button under the script row; the returned id really unregisters
GM_notificationDepends on the browser notification permissionIn-page toast, both call forms, onclick and ondone supported
GM_getValue / GM_setValue / GM_deleteValue / GM_listValuesPer-script in some managers, global in ViaPer-script storage by script id, synchronous cache plus async persist
GM_addElement on WebKitInserted elements are still blocked by the page CSPReuses the page nonce and falls back to GM_addStyle for styles

Injection that survives strict CSP

On WebKit — Safari, Stay, and every browser on iOS — extension-inserted elements do not bypass the page CSP. The bridge reads the page's own nonce, watches the DOM for one appearing later, and probes whether inline script and style are blocked. Styles fall back to GM_addStyle, which is exempt; scripts warn when there is no nonce to reuse.

What it deliberately does not support

APIWhy it is left out
GM_webRequestThe child script already runs in the main world; no interception layer is needed
unsafeWindowIn the main world the page's window is simply window
GM.closeTabMissing or broken in every manager probed
GM_audio.*Unimplemented everywhere it was probed
GM.cookieAn injected script can only touch cookies that are not HttpOnly; full access would be a lie

Writing scripts that survive every manager

Use the narrow path

Prefer GM.getValue and friends over the sync form, GM.xmlHttpRequest in callback style and never read its return value, and GM_addStyle over GM.addStyle when the element handle does not matter. If you need an element back from GM.addElement, wrap it in Promise.resolve() before awaiting.

Keep storage namespaced

Even inside MonkeyMom, where isolation is guaranteed, prefixing keys costs nothing and makes a script portable to managers that share storage between scripts.

Export is the portability test

A child script can be exported as a standard userscript: the header carries @grant lines for the union of the allow-list and the compatibility list, and @resource declarations travel with it. If the exported file runs in Tampermonkey, Violentmonkey or Stay, the portability work paid off.