The userscript manager that's actually yours.
OpenMonkey is a lightweight, open-source browser extension for running userscripts — built for people who are done handing their data to closed-source tools.
Tampermonkey — the standard for years. Closed source. Ships your data wherever it wants. You have no idea what it's doing in the background.
ViolentMonkey — open source, which is great. But there's still telemetry and data collection in the mix. "Open source" doesn't automatically mean "private."
OpenMonkey — built from scratch, owned by you, never uploaded to the Chrome Web Store, never phoning home. It lives on your disk, runs in your browser, and that's where it stays. No accounts. No analytics. No middleman.
- Run userscripts on any website matching
@matchpatterns - Full Greasemonkey/Tampermonkey-compatible script header format (
@name,@match,@exclude,@run-at,@description,@version) @run-at document-start/document-end/document-idle— proper lifecycle hooks@excludepattern support- Per-script
@max-retries— built-in retry guard viasessionStorageto prevent infinite login loops or lockouts - Global default retry setting in the popup
- Enable / disable scripts per-script
- Full in-browser script editor (no external tools needed)
- Zero network requests. All storage is
chrome.storage.local— never synced, never sent anywhere.
OpenMonkey is intentionally not on the Chrome Web Store. That's the point.
- Go to the Releases page and download the latest
.zip - Unzip it to a permanent folder on your machine
- Open
chrome://extensions→ enable Developer mode → Load unpacked → select that folder chrome://extensions→ OpenMonkey → Details → Allow User Scripts → ON ← required
To update: download the new release zip, replace the folder contents, click the reload icon on OpenMonkey in chrome://extensions.
Prerequisites:
- Node.js 22+ — check with
node --version. Install via nodejs.org ornvm install 22 && nvm use 22. - pnpm 11+ — check with
pnpm --version. Install withcurl -fsSL https://get.pnpm.io/install.sh | sh -. - Chrome 135+ — required for the
chrome.userScriptsAPI. Checkchrome://settings/help.
# 1. Clone the repo
git clone https://github.com/mrshappy0/open-monkey.git
cd open-monkey
# 2. Install dependencies
pnpm install
# 3. Build
pnpm build
# 4. Load into Chrome
# Open chrome://extensions → Enable "Developer mode" → "Load unpacked" → select .output/chrome-mv3/
# 5. Enable "Allow User Scripts" ← REQUIRED or userscripts will not run
# chrome://extensions → OpenMonkey → Details → scroll down → "Allow User Scripts" → ONTo update: git pull && pnpm install && pnpm build, then click the reload icon on OpenMonkey in chrome://extensions.
Warning
Scripts will NOT run without step 5. Chrome 135+ requires a per-extension "Allow User Scripts" opt-in before the chrome.userScripts API is available. The extension installs fine and the popup works, but nothing will be injected into any page until you flip that toggle. It does not enable itself automatically — you must set it once after loading unpacked.
For live development with hot-reload:
pnpm devWXT will automatically open Chrome with the extension loaded. Changes to any entrypoint or utility file rebuild and reload instantly.
OpenMonkey is built on WXT — the modern framework for browser extensions. It follows WXT's strict project layout conventions.
open-monkey/
├── entrypoints/
│ ├── background.ts # MV3 service worker — matches tabs → injects scripts
│ └── popup/
│ ├── index.html # Popup HTML shell
│ ├── main.tsx # React root mount
│ ├── App.tsx # Script list + editor UI
│ ├── App.css # Popup styles
│ └── style.css # Global reset/base
├── utils/
│ ├── storage.ts # Typed WXT storage items (scripts + settings)
│ ├── meta-parser.ts # Parses ==UserScript== header blocks
│ ├── match-pattern.ts # Chrome match-pattern URL matching
│ └── logger.ts # Dev-mode logger wrapper
├── assets/ # Processed assets (imported in code)
├── public/
│ └── icon/ # Extension icons (copied as-is to output)
├── wxt.config.ts # WXT config — manifest options, modules
├── web-ext.config.ts # Browser launch/dev config
├── tsconfig.json # TypeScript config (generated by WXT)
└── package.json
| Convention | What it means here |
|---|---|
entrypoints/background.ts |
Auto-registered as the MV3 service worker |
entrypoints/popup/ |
Popup entrypoint directory — index.html is the root |
utils/ |
Auto-imported by WXT — no import statements needed in most files |
public/ |
Static files copied verbatim to .output/ |
assets/ |
Processed by Vite — use for imported images, fonts, etc. |
defineBackground() |
WXT's entrypoint wrapper — keeps runtime code out of module scope |
storage.defineItem() |
@wxt-dev/storage typed, versioned storage items |
browser.* |
WXT's unified cross-browser API — works on Chrome and Firefox |
pnpm build → .output/chrome-mv3/ |
Standard WXT output directory |
OpenMonkey uses the standard Greasemonkey header format. Add scripts directly in the popup editor:
// ==UserScript==
// @name My Script
// @description Brief description of what it does
// @version 1.0.0
// @match https://example.com/*
// @exclude https://example.com/login
// @run-at document-end
// @max-retries 3
// ==/UserScript==
(function () {
'use strict';
// Your code here
})();| Directive | Description |
|---|---|
@name |
Script display name (required) |
@description |
Short description shown in the popup |
@version |
Semver version string |
@match |
URL pattern(s) to run on — supports Chrome match pattern syntax |
@exclude |
URL pattern(s) to explicitly skip |
@run-at |
document-start, document-end (default), or document-idle |
@max-retries |
Max injection attempts per tab session (overrides global setting) |
Multiple @match and @exclude lines are supported.
The background.ts service worker listens to browser.tabs.onUpdated. On each navigation event:
- Load all scripts from
chrome.storage.local - Parse each script's
==UserScript==header - Match the tab URL against
@match/@excludepatterns - Check
@run-atagainst the current navigation phase (loading→document-start,complete→document-end) - Wrap the script body in a
sessionStorage-based retry guard (ifmaxRetries > 0) - Inject via
chrome.userScripts.execute()into theUSER_SCRIPTworld — Chrome's dedicated sandbox for userscripts, with full DOM access
// Scripts run in Chrome's USER_SCRIPT world.
// Requires Chrome 135+ and the per-extension "Allow User Scripts" toggle (see Installation).
await chrome.userScripts.execute({
target: { tabId },
js: [{ code: codeToInject }],
world: 'USER_SCRIPT',
});The USER_SCRIPT world is Chrome's dedicated sandbox for userscripts — separate from the page's JS context and the extension context. CSP for this world is set to '' so eval and dynamic code work. If chrome.userScripts is unavailable (pre-Chrome 135, or the per-extension toggle is off), the background worker logs a warning and skips all injection silently.
Every userscript automatically has access to a set of persistent-storage functions, injected as a preamble by OpenMonkey. No imports or boilerplate needed — just call them directly.
// Available in every userscript — auto-injected, no import needed
const value = await GM_getValue('key', 'default'); // read (with fallback)
await GM_setValue('key', 'value'); // write (resolves after storage write completes)
await GM_setValue('apiKey', 'sk-...', true); // write as secret (masked in popup)
await GM_setValues({ key1: 'a', apiKey: 'sk-...' }, ['apiKey']); // atomic multi-write
await GM_deleteValue('key'); // delete one key
const keys = await GM_listValues(); // list all keys in this script's namespaceEvery script's data is stored under its own namespace (the script's ID). Scripts cannot read or write another script's data. This is intentional — it prevents a malicious script from exfiltrating secrets (e.g. API keys) set by a trusted script. There is no global cross-script storage.
| Layer | What it does |
|---|---|
background.ts |
Injects GM_* preamble before your script runs in the USER_SCRIPT world |
script-bridge.content.ts |
Handles om-store-* window events, reads/writes chrome.storage.local |
utils/storage.ts scriptStoreItem |
Typed chrome.storage.local item — all script data under local:script-store |
Keys are automatically namespaced to your script's ID. You cannot accidentally read another script's data.
The Script Data view (footer of the main popup) shows all stored values grouped by script name. It supports full CRUD:
- View — all keys and values listed, grouped by script; script names shown (not raw UUIDs)
- Pre-populate —
+ Add to…dropdown at the top lets you add data for any installed script, even before it has ever run - Create — click
+ Addon any script's row to add a key to that namespace - Edit — click a value or the ✎ button to edit inline; press Enter to save
- Delete — click ✕ next to any entry
- Reveal secrets — 👁 button temporarily reveals masked values
- Delete script — removing a script from the script list also wipes all its stored variables
Use GM_setValues when writing more than one key at once. Multiple separate GM_setValue calls run concurrently and can overwrite each other (read-merge-write race). GM_setValues does a single atomic read-merge-write.
GM_setValue, GM_setValues, and GM_deleteValue all return Promises that resolve after the chrome.storage.local write completes (via an ack event from script-bridge.content.ts). It is safe to await GM_setValue(...) and then immediately await GM_getValue(...) — the read will always see the written value.
All data lives in chrome.storage.local. Nothing is ever synced or sent anywhere.
// Defined in utils/storage.ts using @wxt-dev/storage
export const scriptsItem = storage.defineItem<UserScript[]>('local:scripts', {
fallback: [],
version: 1,
});
export const settingsItem = storage.defineItem<Settings>('local:settings', {
fallback: { maxRetries: 3 },
version: 1,
});@wxt-dev/storage handles typed reads/writes, reactive .watch() subscriptions (used by the popup's React state), and migration hooks for future schema changes.
# Install deps
pnpm install
# Dev mode (Chrome, hot-reload)
pnpm dev
# Dev mode (Firefox)
pnpm dev:firefox
# Production build
pnpm build
# Production build for Firefox
pnpm build:firefox
# Zip for distribution (sideload/share)
pnpm zip
# TypeScript type check
pnpm compileOpenMonkey includes an MCP (Model Context Protocol) server that bridges VS Code Copilot agents directly to your running Chrome extension. This lets Copilot read, create, edit, and delete your userscripts — and inspect the active browser tab — without any manual copy-paste.
VS Code Copilot → MCP stdio → native-host/index.ts → WebSocket (ws://127.0.0.1:7331) → background.ts
The native host is a Node.js process launched by npx. It hosts a WebSocket server on port 7331. The extension's background service worker connects to it on startup and reconnects automatically on drop.
Add this to your VS Code MCP config (~/Library/Application Support/Code/User/mcp.json on macOS, %APPDATA%\Code\User\mcp.json on Windows):
{
"mcpServers": {
"openmonkey": {
"type": "stdio",
"command": "npx",
"args": ["-y", "github:mrshappy0/open-monkey"]
}
}
}No install required — npx fetches and runs the prebuilt server from GitHub.
| Tool | What it does |
|---|---|
list_scripts |
List all scripts with IDs, names, enabled state |
get_script |
Get full source of a script by ID |
create_script |
Create a new userscript (code must include ==UserScript== header) |
update_script |
Replace source of an existing script by ID |
delete_script |
Permanently delete a script by ID |
get_active_tab |
Get the URL and title of the active Chrome tab |
get_page_content |
Get the innerText of the active tab's body |
execute_script |
Run JavaScript in the active tab's MAIN world and return the result |
| File | Purpose |
|---|---|
native-host/index.ts |
MCP server + WebSocket bridge source |
native-host/dist/index.js |
Compiled output — committed to git, run directly by npx |
tsconfig.mcp.json |
TypeScript config for native-host (NodeNext, emits to native-host/dist/) |
After editing native-host/index.ts, run pnpm prepare to recompile and re-shebang the dist, then commit native-host/dist/index.js.
- Never published to any store. Load unpacked, own it completely.
- No telemetry, no analytics, no remote config. The extension makes zero outbound requests.
chrome.storage.localonly. No sync storage. No IndexedDB. No server.- MV3 by default. Modern Manifest V3 with a proper service worker background.
- Self-hosted and version-controlled. Fork it, modify it, make it yours.
| Tool | Role |
|---|---|
| WXT | Browser extension framework — build, dev, manifest generation |
| React 19 | Popup UI |
| TypeScript | Everywhere |
@wxt-dev/storage |
Typed, versioned chrome.storage.local wrapper |
| pnpm | Package manager |
| Vite | Bundler (via WXT) |
- CodeMirror syntax highlighting in the editor
-
@requiredirective — load and cache external libraries - Per-script execution log / error display in popup
- Import/export scripts as
.user.jsfiles - Options page for advanced settings
- Firefox AMO sideload support (already builds with
pnpm build:firefox)
MIT. Do whatever you want with it. That's also the point.