A web browser extension that lets you tamper with web pages, live and/or with userscripts.
Epupp has two modes of operation:
- Live REPL connection from your editor to the web page, letting you inspect and modify the page on the fly, with or without the assistance of an AI agent.
- Userscripts: Tampermonkey style. Target all websites, or any subset of the web's pages, with prepared scripts that modify or query information from the page. Userscripts can be configured to start before the page loads (
document-start), when the DOM is ready but resources are still loading (document-end), or after everything has settled (document-idle).
The two form a powerful pair. The live REPL connection, while happily supporting one-off changes or data extractions, is also a very efficient and fun means to interactively develop userscripts.
Note
To make this easier to get started with, and using, I have created a template project with some configuration and instructions for humans and AIs:
But please read this below first.
Custom Data Dashboards:
- Problem: Some web page you often visit keeps updated data, but doesn't present it aggregated the way you want it.
- Solution: A userscript automatically aggregates the data the way you want it and presents it the way you want it, every time you visit the page.
One-off Data Extraction:
- Problem: Some web page you visit one time has information you want to summarize (or just find).
- Solution: Connect your editor and/or AI agent and poke around the DOM of the web page until you understand enough to create a function that collects the data you need.
Print-friendly Pages:
- Problem: Some web page you visit is hard to print cleanly on your printer.
- Solution: Connect your editor and/or AI agent and poke around the DOM of the web page until you understand enough to create a function that isolates only the part you want to print. (This was the use case that made me create Epupp in the first place.) This can be generalized in a userscript that lets you use your mouse to point at the element you want to isolate on any web page.
Missing UI Controls:
- Problem: Some web app you often use lacks a button or input widget that would make your workflow convenient.
- Solution: A userscript automatically adds the buttons and widgets for you every time you use the app.
AI-powered Web Inspection:
- Problem: You want to show your AI agent some web app, in a way that it can read things and inspect whatever aspect of it you are interested in.
- Solution: Give the agent access to the page using the live REPL connection.
AI-assisted Web Development:
- Problem: You want your AI agent to help you with a page/app you are developing.
- Solution: Give the agent access to the page using the live REPL connection. While you and the agent are updating the page, the agent always has instant access to the DOM, styles, and everything to gather feedback on the changes. It can test that the app works as it should, and fulfill development tasks with much less help from you in manual testing.
When it comes to userscript use cases, a lot of things that you would use Tampermonkey for, you can use Epupp for instead. Tampermonkey can probably handle more use cases, but Epupp lets you develop userscripts in a much more dynamic way, with the shortest possible feedback loop.
With the live REPL connection, you will discover use cases you may not ever have thought about before, or thought about, but dismissed.
- Install Epupp from the Chrome and Firefox extension/addon stores.
-
Chrome Web Store: Epupp: Live Tamper Your Web
-
Firefox Browser Addons: https://addons.mozilla.org/firefox/addon/epupp/ (Please note that I haven't yet figured out all Firefox nuances, so some things may not work. Please file issues for things you note not working.)
-
Safari
I'm still pondering wether I should submit to Safari App Store. Apple doesn't exactly love developers... But you can still use Epupp with Safari:
Grab the extension zip file(s) from the Epupp repository, latest release. In the case of Safari, download
epupp-safari.zip. Then in Safari:- Open Settings -> Developer
- Click Add Temporary Extension...
Please note that I haven't yet figured out all Safari nuances, so some things may not work. Please file issues for things you note not working.
-
- Pin Epupp to always be visible in the browser location bar. I also recommend to allow Epupp in Private Browsing for maximum utility. The Extension does not collect any data whatsoever.
- Navigate away from the extension store, these pages can't be scripted.
Create a userscript and run it in some different ways:
- Open the Developers Tools panel in your browser
- Select the Epupp tab
- Last in the script text area, add a new line and enter:
(js/alert "Hello World!")
- Click Eval Script to see the alert happen (and the browser console will print things too, assuming you kept the default script.)
- Save the script using the Save Script button.
- Open the Epupp popup by clicking the Eppup extension in the browser toolbar. You will see the script there, named
hello_world.cljs. - Click the
Playbutton on the script. The alert shows again. - Reload the page. The alert show again. The default example code includes a script manifest that will make the script trigger on domain you are currently visiting.
- Navigate to another domain and the alert will not show. Navigate back to the previous domain, and the alert shows.
- Annoyed by the alert? Either delete the script or edit it to not alert. The Epupp popup lets you both delete the script and load it in the panel for editing.
While the Epupp panel let's you script the page, Live Tampering comes to life when you are powered by your favorite development environment, which could be a code editor, an AI agent harness, or both. As the creator of Calva I choose to describe how it can be done using VS Code + Calva, and with VS Code Copilot and Calva Backseat Driver. Please see connecting-to-epupp README for info about connecting editors and AIs.
- Install Babashka and VS Code. In VS Code, install the Calva extension
- On a GitHub page (this one will do fine), open the Epupp popup and, copy the browser-nrepl command line, using the default ports
- Paste the command line in a terminal and run it
- From the Epupp popup, click Connect
- In VS Code create a file
hello_world.cljs - Click the REPL button in the VS Code status bar and select Connect to a running REPL in your project
- Select scittle from the Project Types menu
- In the file, type:
And press alt+enter
(js/alert "Hello World!")
(The alt key is sometimes labeled opt or option.) - Replace the contents of the file with:
Press alt+enter. The GitHub icon should start spinning.
;; Make the GitHub logo spin! (when-let [logo (js/document.querySelector ".octicon-mark-github")] (set! (.. logo -style -animation) "spin 2s linear infinite") ;; Add the keyframes for spinning (let [style (js/document.createElement "style")] (set! (.-textContent style) "@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }") (.appendChild js/document.head style)))
Note: The alt+enter shortcut evaluates the current top level form/expression, which in the above cases happen to be everything in the file. If you are new to Clojure, for now you can think about it as executing the code in the connected browser tab. Please visit calva.io for how to use and get started with Calva (and Clojure).
As the creator of Calva, and Calva Backseat Driver I chose to desceibe how to connect an AI agent using VS Code, Copilot and Calva Backseat Driver. See https://github.com/PEZ/my-epupp-hq for a growing list of instructions for other AI harnesses.
- In addition to the above, install Copilot and Calva Backseat Driver to VS Code.
- Ask Copilot to try the Epupp REPL to do something fun.
Note
Copilot will need to know some basics about Epupp to be really effective with it. Consider copying the https://github.com/PEZ/my-epupp-hq template project and use that as a start for you and your AI to explore the Epupp REPL with.
An Epupp userscript is just a text file which starts with a script manifest and some code. You can install scripts in three ways:
- Pasting/typing a script in the Epupp panel and clicking Save Script.
- The Web Userscript Installer script. The extension has a built-in script that will identify Epupp scripts on code-hosting pages and add an Install button near them. On whitelisted domains (GitHub, GitHub Gists, GitLab, Codeberg, localhost) it can install scripts into Epupp. On other sites, it shows copy-paste instructions instead. Try it on this gist: https://gist.github.com/PEZ/3b0fdc406e7593eaaef609b6fb4a687d (It's the script created in the demo video.)
- Using the REPL. There's a
epupp.fsnamespace for listing/reading/writing/renaming scripts in the Epupp extension storage.
The UI has three main components:
- The extension popup. You access this from the Epupp extension icon. The popup hosts the REPL connection UI, lists userscripts, and provides accesss to Epupp settings.
- A browser Developement Tools panel. The panel is for inspecting and editing userscripts, creating simple usercripts, and for dynamic interaction with the visited web page.
- Your favorite editor and/or your favorite AI agent harness. This is enabled by the live REPL connection.
The popup has the following sections:
- REPL Connect. Shows how to connect the current tab's REPL to your editor and/or AI agent. Also shows which tabs are currently connected.
- Userscripts sections:
- Manual/on-demand. Scripts that do not auto-run on any page, use the play button to run them.
- Auto-run for this page. Scripts that has an
:epupp/auto-run-matchpattern than matches the current page. - Auto-run not matching this page. Scripts that auto-runs on some other pages, but not the current one.
- Special. Built-in scripts that has some special way of being triggered to start. (Currently only the Web Userscript Installer)
- Settings. Click the gear icon.
Use the eye icon to load a script into the panel editor. From there it's the usual loop: edit, save, test.
Rename by changing :epupp/script-name in the manifest and saving. Copying is the same trick: give :epupp/script-name a new name and save, and you get a new script.
Built-in scripts are readable but not editable in place. Inspect one, copy it, rename it, then make it yours.
Auto-reconnect is per tab: if a tab was connected before reload/navigation, Epupp re-establishes the connection. Tabs that were never connected stay that way.
Auto-connect REPL makes Epupp try to connect right away on all pages you open.
Allow REPL FS Sync for this tab turns on epupp.fs for that tab. Enabling sync gives access to code on that tab to install userscripts. Only enable this on a page you trust. See REPL FS Sync for the API.
Diagnostics logging adds noisy logs for when things are weird.
Export / Import is for backups and moving scripts/settings between browser profiles or machines.
The Browser Development Tools panel you can create and experiment with userscripts.
New clears the editor, with a safety prompt if you have unsaved changes. Save Script stores the script, as long as it starts with a valid manifest map.
Use Eval Script to run the whole editor buffer. Ctrl/Cmd+Enter / Cmd+Enter runs selection if you have one, otherwise the whole script. This is the fastest feedback loop in Epupp (without editor connection): define a helper, select just the call, press Ctrl/Cmd+Enter, inspect the result, repeat.
Results show up below the script textarea - both what ran and what came back (or the error). Use Clear when you want a clean slate.
The REPL connection is there so that you can connect your code editor and/or AI agent harness to Epupp and live tamper the connected tabs. The system has these components:
- The Epupp REPL (a program running in the browser that can evaluate/execute Epupp/Scittle code in the connected tab's environment). The Epupp REPL listens to messages over a WebSocket.
- The browser-nrepl relay. This is a program you run on your computer that relays between the REPL client (using the nREPL protocol) and the connected browser tab (the WebSocket).
- The REPL client. Really the nREPL client. A program connecting software such as editors and AI agent harnesses to an nREPL server (the browser-nrepl relay, in this case). In the REPL: Hello World example above the nREPL Client is Calva.
The procedure to connect a browser tab to your editor is:
- Start the browser-nrepl relay (you can copy the command from the Epupp extension popup)
- Connect the browser tab: Click Connect in the Epupp popup
- Connect your editor/AI harness: This will depend on what editor/harness you use. TL;DR: You need a Clojure plugin/extension for your coding editor, and/or some Clojure hook or MCP server for your AI agent. (See above for using VS Code and Calva.)
See https://github.com/PEZ/my-epupp-hq for a template project that you can use to keep the Epupp REPL in easy reach.
The popup shows currently connected tabs in the REPL section. Each one gets a Reveal button so you can jump straight to connected tabs. The toolbar icon mirrors connection status of the current tab: white when it's not connected, gold when it is.
You can keep multiple tabs connected at once. Run multiple browser-nrepl relays (different port pairs), then point each tab at the relay you want. Handy when you want GitHub and GitLab live at the same time.
An Epupp userscript starts with a manifest map, followed by ClojureScript code:
{:epupp/script-name "github_tweaks.cljs"
:epupp/auto-run-match "https://github.com/*"
:epupp/description "Make the GitHub logo spin"
:epupp/run-at "document-idle"
:epupp/inject ["scittle://replicant.js"]}
(ns github-tweaks)
(when-let [logo (js/document.querySelector ".octicon-mark-github")]
(let [style (js/document.createElement "style")]
(set! (.-textContent style)
"@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }")
(.appendChild js/document.head style))
(set! (.. logo -style -animation) "spin 2s linear infinite"))The manifest is a plain Clojure map at the top of the file. Your code runs in the page context with full DOM and JavaScript environment access.
| Key | Required | Default | Description |
|---|---|---|---|
:epupp/script-name |
Yes | - | Filename, auto-normalized to snake_case.cljs. Cannot start with epupp/ (reserved for built-in scripts). |
:epupp/auto-run-match |
No | - | URL glob pattern(s). String or vector of strings. Omit for manual-only scripts. |
:epupp/description |
No | - | Shown in the popup UI. |
:epupp/run-at |
No | "document-idle" |
When to run: "document-start", "document-end", or "document-idle". |
:epupp/inject |
No | [] |
Scittle library URLs to load before the script runs. |
Scripts with :epupp/auto-run-match start disabled. Enable them in the popup for auto-injection on matching pages. Scripts without this key only run when you click the Play button in the popup.
Script names are auto-normalized to valid ClojureScript file names: snake_case.cljs (so "My Cool Script" becomes my_cool_script.cljs).
Scripts can also be managed programmatically via the FS API.
:epupp/auto-run-match uses glob syntax. * matches any characters:
;; Single pattern
{:epupp/auto-run-match "https://github.com/*"}
;; Multiple patterns
{:epupp/auto-run-match ["https://github.com/*"
"https://gist.github.com/*"]}
;; Match both http and https
{:epupp/auto-run-match "*://example.com/*"}Scripts can run at different points during page load:
"document-idle"(default) - After the page has fully loaded."document-end"- At DOMContentLoaded. DOM exists but images/iframes may still be loading."document-start"- Before any page JavaScript.document.bodydoes not exist yet.
If your script is using document-start, you need to wait for the DOM if your code needs it:
{:epupp/script-name "early_intercept.cljs"
:epupp/run-at "document-start"}
;; This runs before any page scripts
(set! js/window.myGlobal "intercepted")
;; Wait for DOM if needed
(js/document.addEventListener "DOMContentLoaded"
(fn [] (js/console.log "Now DOM exists")))Note
Safari does not support early script timing. Scripts always run at document-idle regardless of :epupp/run-at.
Using Scittle Libraries
Userscripts can load bundled Scittle ecosystem libraries via :epupp/inject:
{:epupp/script-name "replicant_widget.cljs"
:epupp/auto-run-match "*"
:epupp/inject ["scittle://replicant.js"]}
(ns replicant-widget
(:require [replicant.dom :as r]))
(r/render
(doto (js/document.createElement "div")
(->> (.appendChild js/document.body)))
[:h1 "Hello from Replicant!"])Available libraries:
| Require URL | Provides |
|---|---|
scittle://pprint.js |
cljs.pprint |
scittle://promesa.js |
promesa.core |
scittle://replicant.js |
Replicant UI library |
scittle://js-interop.js |
applied-science.js-interop |
scittle://reagent.js |
Reagent + React |
scittle://re-frame.js |
Re-frame (includes Reagent) |
scittle://cljs-ajax.js |
cljs-http.client |
Dependencies resolve automatically: scittle://re-frame.js loads Reagent and React.
Note
No Script Modularity
Epupp userscripts are currently self-contained. You cannot split code across multiple scripts or create shared library modules of your own. This may change in the future.
From a live REPL session, you can load libraries at runtime with epupp.repl/manifest!:
(epupp.repl/manifest! {:epupp/inject ["scittle://pprint.js"]})
(require '[cljs.pprint :as pprint])
(pprint/pprint {:some "data"})Safe to call multiple times - already-loaded libraries are skipped.
There is no Epupp Userscript Gallery yet. Epupp is brand new! But I have made two quite useful scripts that you can test and explore.
Isolate elements on crowded web pages for printing. (It's the script created in the demo video.)
Install pez/element_printing.cljs from this gist: https://gist.github.com/PEZ/3b0fdc406e7593eaaef609b6fb4a687d
(This is the use case I created Epupp to solve, to begin with.)
Ever been reading a post on LinkedIn, only for the feed to be refreshed and the post gone forever? Ever recalled that post you read earlier today, or yesterday, but can't find it when you want to show it to your friend? LinkedIn Squirrel, en Epupp userscript, to the rescue!
Install pez/linkedin_squirrel.cljs from this gist: https://gist.github.com/PEZ/d466a85f98ec3f5f2e9aea7a13b89007
LinkedIn Squirrel has two main features:
- Lets you reinsert the posts you were seeing when the feed got refreshed
- Hoards posts you engage with. Let's you access them from an always available list, which has filtering and instant search. Engagement is that you post something, like, expand read “More...”, repost, and so on. You can also explicitly pin a post to hoard it.
On non-SPA sites, setting js/window.location (or clicking a link) from a REPL eval can tear down the page and its REPL. The eval response never returns - the connection hangs until you reconnect the repl. Very disruptive!
Fix: Defer navigation with setTimeout:
;; BAD - eval never completes, connection hangs
(set! (.-location js/window) "https://example.com/page")
;; GOOD - returns immediately, navigates after response completes
(js/setTimeout
#(set! (.-location js/window) "https://example.com/page")
50)
;; => timeout ID returned instantlyAfter navigation, wait for the new page to load and REPL to reconnect. All prior definitions will be gone - redefine utilities or bake them into a userscript.
navigator.clipboard.writeText is often blocked, needing user gesture, due to permissions policy. Use a textarea workaround:
(defn copy-to-clipboard! [text]
(let [el (js/document.createElement "textarea")]
(set! (.-value el) text)
(.appendChild js/document.body el)
(.select el)
(js/document.execCommand "copy")
(.removeChild js/document.body el)))The extension fails at adding a Development Tools panel at any chrome:// page, and also at the Extension Gallery itself. These are pages from where you may have installed Epupp the first time. Please navigate to other pages and look for the panel.
Check that the relay server is running and ports match between terminal and popup. If it still won't connect, try restarting the relay server.
- Is auto-run enabled? (the checkbox in the popup)
- Does the pattern match the URL? Check for typos.
- Open DevTools console for error messages.
Some sites have strict Content Security Policies. Epupp mostly should do things in ways that are allowed by these oliciies, but still, check the console for CSP violation messages.
Epupp only asks for the permissions it strictly needs, even if the nature of the extension is such that it needs you to permit things like scripting (duh!). These are the permissions, and for what they are used:
scripting- Inject userscripts<all_urls>- Inject on any sitestorage- Persist scripts/settingswebNavigation- Auto-injection on page loadactiveTab- DevTools panel integration
Epupp is powered by Scittle, which allows for scripting the page using ClojureScript, a dynamic language supporting Interactive Programming.
The extension does not collect any data whatsoever, and never will.
(Free to use and open source. 🍻🗽)
To build and hack on the extension, see the development docs.
Epupp is created and maintained by Peter Strömberg a.k.a PEZ, and provided as open source and is free to use. A lot of my time is spent on bringing Epupp and related software to you, and keeping it supported, working and relevant.
- Please consider sponsoring Epupp.