Skip to content

Repository files navigation

🐒 OpenMonkey

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.


The Origin Story

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.


What It Does

  • Run userscripts on any website matching @match patterns
  • 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
  • @exclude pattern support
  • Per-script @max-retries — built-in retry guard via sessionStorage to 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.

Installation (Load Unpacked)

OpenMonkey is intentionally not on the Chrome Web Store. That's the point.

Option A — Download a release (no build toolchain needed)

  1. Go to the Releases page and download the latest .zip
  2. Unzip it to a permanent folder on your machine
  3. Open chrome://extensions → enable Developer modeLoad unpacked → select that folder
  4. chrome://extensions → OpenMonkey → DetailsAllow User Scripts → ON ← required

To update: download the new release zip, replace the folder contents, click the reload icon on OpenMonkey in chrome://extensions.

Option B — Clone and build (developers)

Prerequisites:

  • Node.js 22+ — check with node --version. Install via nodejs.org or nvm install 22 && nvm use 22.
  • pnpm 11+ — check with pnpm --version. Install with curl -fsSL https://get.pnpm.io/install.sh | sh -.
  • Chrome 135+ — required for the chrome.userScripts API. Check chrome://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" → ON

To 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 dev

WXT will automatically open Chrome with the extension loaded. Changes to any entrypoint or utility file rebuild and reload instantly.


Project Structure

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

WXT Conventions Used

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

Userscript Format

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
})();

Supported Directives

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.


How Script Injection Works

The background.ts service worker listens to browser.tabs.onUpdated. On each navigation event:

  1. Load all scripts from chrome.storage.local
  2. Parse each script's ==UserScript== header
  3. Match the tab URL against @match / @exclude patterns
  4. Check @run-at against the current navigation phase (loadingdocument-start, completedocument-end)
  5. Wrap the script body in a sessionStorage-based retry guard (if maxRetries > 0)
  6. Inject via chrome.userScripts.execute() into the USER_SCRIPT world — 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.


Persistent Script Storage — GM_* API

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 namespace

Namespace isolation

Every 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.

How It Works

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.

Script Data in the Popup

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 + Add on 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

GM_setValues vs. multiple GM_setValue calls

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.

Writes are properly awaitable

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.


Storage

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.


Development

# 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 compile

MCP / Copilot Integration

OpenMonkey 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.

Architecture

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.

Setup

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.

Available Tools

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

Relevant Files

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.


Philosophy

  • 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.local only. 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.

Tech Stack

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)

Roadmap

  • CodeMirror syntax highlighting in the editor
  • @require directive — load and cache external libraries
  • Per-script execution log / error display in popup
  • Import/export scripts as .user.js files
  • Options page for advanced settings
  • Firefox AMO sideload support (already builds with pnpm build:firefox)

License

MIT. Do whatever you want with it. That's also the point.

About

Lightweight, privacy-respecting userscript manager Chrome extension.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages