Browser extension
The extension puts a Run button where you already are: next to the branch dropdown on a repository page, in a pull request header, and on every row of the branch list.
It does that on github.com, gitlab.com, bitbucket.org and Azure DevOps (dev.azure.com and the older <org>.visualstudio.com).
A GitLab or an Azure DevOps Server of your own can be added in the extension's options: its address and which of the two it is. The browser then asks once whether QuickRun may run on that address, and on no other. That is why it is not asked at install: an extension that may run on every site shows "read and change all your data on all websites", for everybody, for the sake of the few with a server. Bitbucket Server is not supported yet. For it, paste the clone URL into QuickRun directly.
A Bitbucket Cloud pull request has no ref git can fetch, so its button runs the pull request's source branch, from the fork it came from if there is one. On the other hosts the button runs the pull request itself.
Without the extension there is still the README badge: a link a repository owner puts in their own README, which reaches the same local window through an https page.
Why it needs the local listener
A browser cannot be asked whether a URL scheme has a handler. That is deliberate — it would be a fingerprinting vector — and there is no API for it. So an extension whose only channel is quickrun:// can never know whether QuickRun is installed, and can never receive progress back from a run it triggered.
The localhost listener is therefore the main channel, and quickrun:// has exactly one job left: starting a daemon that is installed but not running.
| Ping result | The button shows |
|---|---|
| QuickRun answers | Run this — clicking prepares a run |
no answer, then quickrun:// works | Starting QuickRun…, then Run this |
| no answer at all | Install QuickRun — links to the download page |
Button states
- ready — QuickRun answered. Click to prepare a run.
- running — the current phase and, where a real number exists, a percentage.
- done — the run finished.
- error — hovering shows why.
The confirmation window
Clicking Run does not start anything. QuickRun checks the repository out, builds the plan, and the extension opens a window listing the repository, ref, resolved commit and the exact commands that will run. Only the button in that window starts them.
One window per plan. A second repository handed over while the first is still on screen gets its own window rather than replacing what is there — a run you started, with its log and its Stop, is not something a later click should push out of the way. Handing the same plan over again raises the window it already has, and five open at once is the ceiling: past that the oldest takes the new plan, which is the one least likely to still be being read.
While it checks the repository out there is nothing to show yet, so the window shows the mark and what it is doing. If that ends in a reason rather than a plan — no config, a branch that is not there — the reason is what stays on screen. It used to be an empty window.
It also comes to the front when it appears. Windows does not let a background program take the foreground — it blinks the taskbar button instead, which is easy to miss — so the window is put above the others for a moment and then behaves like any other window. Keep a plan's window above other windows in Settings makes that permanent for anyone who would rather not look for it at all.
And it closes when there is nothing left in it: Cancel answers the plan with no, Remove takes the finished run away, and either way this window was opened for that one run. Standing there empty is worse than either outcome.
The window also shows the description from the config when it has one, the folder the repository was checked out into, and — once a task reports one — the address it is listening on, as a link.
And the config itself. Under the source it names the file the plan came out of by its full path — the quickrun.yml in the checkout, the config you saved for that repository, QuickRun's collected copy on your disk, or the address a published one was read from — with a View button beside it that shows the file. Which of the sources was used does not say which file it was, and the four produce plans that read exactly the same; the commands are the answer to what will run, and the file is the answer to who wrote this. In QuickRun's own window that dialog also has Open in the config builder, which takes the config into the builder in that window, repository and ref filled in, nothing saved until you save it.
If the config declares inputs, the window asks for them before there is anything to approve: labels, descriptions, defaults, a dropdown for a select, a checkbox for a bool, a masked field for a password. A changed value applies itself a moment later and the command list is rebuilt from it, so the list on screen is always the one those values produce — what you approve is what runs, and it stays one click on Run. The button says Continue only while required values are still missing, because until they are there is no command list to approve. Values marked with an env are passed to the run as that environment variable, and a secret is never sent back to the window.
Once the run starts the form steps aside, and the answers stay: under Answered the window says which values this run was started with, and the card in QuickRun's own window says the same. A choice appears as its option's text rather than its value, because the value is often a flag while the text is the sentence you read. A password reads as hidden and never as a value - the daemon does not send one.
While a run of that branch is going, the button is the way into it rather than a way to start a second one. Clicking it opens the actions:
- Show log — brings the log window back, or opens a new one attached to the run. Closing that window never stopped anything, so getting back to it has to be possible
- Open … — the address the run reported, once it has reported one
- Stop — stops the run, and ends what it left running if a task exited and left a server behind
The button finds its run again after a page reload: the tab forgets, the daemon does not. A branch that is already running therefore cannot be started twice by accident.
After you approve, that window stays open and becomes the run's log: the checkout with its real progress counters, every setup step, and everything the repository's own commands print. The button on the page shows only a percentage and a coarse phase — a toolbar button is no place for a hundred lines of build output.
Finding the lines that matter
QuickRun reads every line as it arrives and says how bad it is, and the window shows that: errors in red, warnings in amber, everything else as it is. If everything is coloured, nothing is — so a line only counts when it says something went wrong, and the lines that merely contain the word do not: 0 Error(s) at the end of a build, found 0 vulnerabilities, compiling ErrorPage.tsx, and the echo of the command itself, which contains whatever the config author wrote.
Above the log:
- Everything / N error lines / N warnings — filters the log to that kind. The chips appear only when there is something of that kind to show.
- Show them — every error line of the run in one dialog, ready to copy. Including the ones the log block has already dropped: it keeps the last few hundred lines because a restore prints thousands, and the error from minute one is usually the one you are looking for.
In the extension's window the count in the banner does the same thing: 3 error lines is a button now, and pressing it answers the question it raises.
Standard error is not treated as an error: docker, git, npm and every progress bar write their ordinary output there, and counting all of it is how "17 error lines" ends up above a run that worked perfectly. An unrecognised line from that stream is a warning, which is what it honestly is — not the normal output, and not necessarily a problem.
Once the run starts, every task gets a line of its own: what it is doing - starting, ready, exited - the address it reported, as a link, and the process id of what it started. For a desktop application that pid is the whole handle you have on it, and it is what a task with readyWhen: {window: true} is waiting on. "Running" for a whole run says nothing about which of five services came up.
Stop stops the run and says so: the button turns to Stopping… with a spinner the moment it is clicked, the banner turns to Stopped when the processes are gone, and the window closes itself a moment later. "When the processes are gone" is meant literally: if the run left something running in the background, the window says how many and stops again rather than calling it stopped - you asked for it to stop, not for a window to tidy up. It is only clickable while something is actually running: a run whose processes have all exited has nothing left to stop.
That window is an extension page, not part of the GitHub page, and that is on purpose: a web page can draw a convincing fake panel over its own content, and nobody should ever approve one set of commands while a different set runs.
Why no web page can drive it
There is nothing to pair. A browser attaches an Origin header to every cross-origin request and a page cannot change it, so QuickRun simply refuses anything that does not come from a browser extension. https://github.com is not on that list: a script running on GitHub itself cannot start a run.
A program on your own machine — curl, QuickRun's own CLI — sends no Origin and is allowed. It already runs with your privileges, so the daemon grants it nothing it did not have.
Options
| Setting | Default |
|---|---|
| Port | 9876 |
Try quickrun:// when QuickRun does not answer | on |
| Where the button appears | on every repository |
Installing it from QuickRun itself
QuickRun's own window has a Browser extension tab that lists the browsers it finds on this machine and says, for each one, whether the extension is there. An unpacked build counts: it is recognised by the folder it was loaded from, which is what the browser records.
The Install button goes as far as a browser allows, and no further. Where a store listing exists it opens that listing in that browser, and the last click is the browser's own Add button. Where none exists yet it downloads the packaged extension from the newest release, unpacks it, opens the browser's extensions page and the folder next to it - leaving "Load unpacked, pick this folder".
It stops one click short deliberately. Chrome removed inline installation in 2018, and the only way left for a program to put an extension into Chrome or Edge is an enterprise policy that force-installs it and takes away your ability to remove it. That is a thing malware does, and QuickRun does not do it. Firefox is the exception in the other direction: a temporary add-on can be loaded from about:debugging, but Firefox forgets it when it closes.
Where the button appears
QuickRun can start almost any repository: with no configuration it reads the files and works out a plan itself. That plan is a considered guess - a good one, and you always see the exact commands before anything runs - but you may prefer the button only where the repository has said how it wants to be started.
- On every repository. Including the ones QuickRun would have to work out for itself.
- Where there are instructions to follow. A
quickrun.yml, or scripts written for Pinokio. - Only with a quickrun.yml. The repository has committed a QuickRun configuration.
The last two ask QuickRun on your machine whether those files exist, once per repository and cached for half an hour. The extension does not reach GitHub itself for this: that would need a permission for raw.githubusercontent.com, and the daemon can answer without one. When QuickRun is not running the check cannot be made, and the button appears anyway - vanishing because a check failed would be indistinguishable from the extension being broken.
The confirmation window names the source for every run, so you always know which of the four you are looking at: the repository's own quickrun.yml, a config you saved for that repository, another launcher's scripts, or QuickRun's reading of the files.
Running a pull request means running the branch it comes from, fetched as refs/pull/<n>/head. That works for pull requests from forks too, and it is what the button on a pull request page does.
Starting from a link
A GitHub address can ask for the plan by itself:
https://github.com/acme/app?executeQuickRun
https://github.com/acme/app/tree/preview?executeQuickRun=true
https://github.com/acme/app?executeQuickRun=ci/demo.ymlThe extension does what pressing the button does: it prepares the run and opens the confirmation window with the command list. It does not start anything. That is deliberate - a link that ran commands by itself would be a way to spend someone else's afternoon, or their machine, on one click. The window is still read and Run is still pressed by whoever owns the machine.
With a file name, that config is used instead of the one the repository would have chosen - useful where a repository carries several, one per demo or per environment. The name has to be a .yml or .yaml file inside the repository, written relative to its root: ci/demo.yml is a config, while ../secrets.yml, /etc/anything.yml and a URL are not, and are refused. The extension checks that, and QuickRun checks it again before opening anything - a value out of a web address is a stranger's string on both sides.
It works wherever the page has a button of its own, and it prepares exactly what that button would:
| Address | What is prepared |
|---|---|
/acme/app?executeQuickRun | the repository's default branch |
/acme/app/tree/preview?executeQuickRun | the branch preview |
/acme/app/tree/feature/some/name?executeQuickRun | the branch feature/some/name |
/acme/app/pull/42?executeQuickRun | pull request 42, fetched as refs/pull/42/head |
/acme/app/branches?executeQuickRun | nothing - see below |
The branch list is the one exception. Every row there gets its own button, so there is no single run the address could mean, and "whichever branch the list happens to show first" is not an answer worth acting on. Link to the branch itself - /tree/<branch> - and it works.
?executeQuickRun=false is the same as not asking, so a link can be built with the parameter switched off. It acts once per address: GitHub renders through pushState, and clicking around the repository afterwards does not prepare a second run.
Building it yourself
cd extension
sh build.shOne source tree, three builds: dist/chromium for Chrome, Edge and Opera, dist/firefox, and dist/safari. They differ only in their manifests.
Safari
Safari has no "load unpacked" that survives a restart. An extension there is delivered inside a native macOS app, and building that app needs a Mac with Xcode — so the Safari build is the one you cannot produce on Windows or Linux beyond the folder itself.
Trying it, on a Mac, with no Xcode and no Apple account. Safari can load the built folder directly:
cd extension && sh build.sh- Safari → Settings → Advanced → Show features for web developers
- Settings → Developer → tick Allow unsigned extensions
- Settings → Developer → Add Temporary Extension… → pick
extension/dist/safari
Step 4 takes a zip as well as a folder, so quickrun-extension-safari.zip from any release works without building anything.
Safari drops a temporary extension when you quit it, and Allow unsigned extensions resets with it, so this is for a look rather than for daily use.
Building the app. On a Mac with Xcode:
sh scripts/build-safari.shThat runs the extension build, hands extension/dist/safari to Apple's xcrun safari-web-extension-packager, and builds the generated Xcode project into safari/build/Release/QuickRun.app. Run that app once and the extension appears in Safari's Extensions list. Unsigned it still needs Allow unsigned extensions; signed and notarised with a Developer ID it does not, from Safari 18.4 onwards.
The script refuses to run anywhere but macOS, because the packager exists only inside Xcode.
What Safari does differently
- You have to grant the extension access to github.com. Safari does not run a content script on a site until you allow it, from the extension's button in the toolbar. Until then there is no Run button on the page.
127.0.0.1needs the same grant, under Settings → Extensions → QuickRun. - Safari 16.4 or newer. The manifest says so. Older versions lack
storage.session, which is where a prepared run waits while the confirmation window opens. - macOS only. The confirmation window is the whole point of QuickRun, and iOS Safari cannot open an extension window at all — so an iOS build would offer a Run button that can approve nothing.
- A window already open is brought forward but does not bounce in the Dock. Safari ignores
drawAttention. - The background page is an event page rather than a service worker, because Safari does not give a service worker the loopback access that the manifest asks for. Same
background.js, same as Firefox.
Publishing it
This part needs an Apple Developer Program membership (99 USD a year); it does not necessarily need a Mac.
- Without a Mac: App Store Connect has a Safari Web Extension Packager under the Xcode Cloud tab. Create an app record, upload the extension files —
quickrun-safari-<version>.zipout ofextension/distis exactly that — and it assembles, signs and submits the app for you, spending Xcode Cloud minutes from the membership. - With a Mac: archive the project from
scripts/build-safari.shin Xcode and upload it, or sign it with a Developer ID and notarise it to distribute the app outside the App Store.
Either way an App Store submission is reviewed by a person, and there is no upload API to automate the way Chrome, Edge and Firefox have. The release workflow therefore builds and signs the Safari app as a CI artifact and stops there; the submission is a deliberate manual step.
When GitHub changes its DOM
The button is anchored on data-testid attributes and ARIA labels where GitHub provides them, and every lookup fails silently: a missing button is acceptable, a broken GitHub page is not. GitHub does redesign these pages, so a missing button usually means the extension needs an update rather than that something is wrong with your setup.