There are two ways to get Local Test, both from the downloads area of this site: download a ready-made build, or download the source archive and run it yourself. Both give you the same app. The build is faster to get going; running it yourself avoids the unsigned-app warning below.
macOS: the first launch #
- Download the
.dmg file from the downloads area of this site.
- Open it and drag Local Test into your Applications folder.
- The first time only: do not double-click it. Right-click (or hold Control and click) the app, choose Open, and then click Open again in the dialog that appears.
That is a one-time step. From then on it opens like any other app.
The honest reason for that step: macOS checks downloaded applications for a Developer ID signature, and Apple only issues those certificates to members of its paid developer program. Local Test is free to download and use, and maintained by one student, so it is not enrolled and the app ships unsigned. Right-click → Open is macOS's own way of saying "I know where this came from", it is not a workaround, it is the documented path for unsigned apps.
Two things follow from that, and they are worth knowing before you install: the same warning appears on Windows in a different form, and updates replace the app in place rather than installing quietly in the background. A copy you build yourself from source opens normally from the start.
Windows #
The Windows .exe installer is in the downloads area, for Windows 10 and 11. It is an ordinary installer rather than a one-click one: you can choose where it goes, and it installs for your user account only, so there is no administrator prompt. It creates Desktop and Start Menu shortcuts and registers a normal entry in Add or remove programs.
It is unsigned, for the same reason the macOS build is, so Windows stops it the first time. SmartScreen halts any installer it does not recognize and shows Windows protected your PC. The way through is three steps:
- Run the installer. SmartScreen appears.
- Click More info, which reveals a button.
- Click Run anyway.
That is the documented path for an unrecognized installer, not a workaround, and it is worth doing the checksum check first if you want more than the site's word for what you downloaded.
The Windows build has been through a validation pass on real hardware: installing, launching, deploying a project and serving it over HTTP with live reload, running a command project, cloning a repository, and uninstalling again, which removes the shortcuts, the app data, and the registry entry. The uninstaller leaves an empty folder behind, which is ordinary behavior for this kind of installer rather than a fault.
One Windows note worth stating plainly: if you turn on network sharing, the next deploy may raise the Windows firewall prompt. That is expected, and allowing it on private networks is what makes the shared address answer. See the firewall prompt.
Running it from the source code #
This takes about five minutes and works even if you have never opened a terminal. A terminal is just a window where you type one command, press Enter, and wait for it to finish.
- Install Node.js. Go to nodejs.org, click the large LTS button, run the installer, and accept the defaults. Node.js is the engine Local Test runs on.
- Get the source archive. Download the source ZIP from the downloads area of this site, then unzip it somewhere you can find again, the Desktop is fine. Double-clicking the file unzips it on both macOS and Windows.
- Open a terminal. On a Mac: Applications → Utilities → Terminal. On Windows: open Command Prompt from the Start menu.
- Point it at the folder. Type
cd followed by a space, then drag the unzipped folder onto the terminal window (its location appears as text) and press Enter.
- Install and start. Run these two commands, one at a time, waiting for each to finish.
npm install
npm start
npm install downloads Electron, which is a large download, so give it a few minutes the first time. npm start opens the Local Test window. To run it again another day, repeat steps 3 and 4 and type npm start on its own.
While it is running that terminal window belongs to the app. Closing the window, or pressing Ctrl‑C in it, quits Local Test.
Checking a download is genuine #
Local Test is not signed by Apple or Microsoft, so your computer cannot
vouch for who made the file. What you can do instead is confirm the file you
received is byte-for-byte the one that was published. Every release lists a
SHA-256 checksum on the download page and in
SHA256SUMS.txt.
On macOS, in Terminal, from the folder holding the download:
shasum -a 256 Local-Test-1.8.0-arm64.dmg
On Windows, in PowerShell:
Get-FileHash Local-Test-1.8.0-setup.exe -Algorithm SHA256
Compare the result with the checksum on the download page. They must match
exactly. If they do not, delete the file and do not open it, either the
download was corrupted, or it is not the file that was published.
On macOS and Linux you can check several files at once: download
SHA256SUMS.txt into the same folder and run
shasum -a 256 -c SHA256SUMS.txt. That file names every artifact in
the release, so the ones you did not download are reported as missing, not as
wrong. The files you do have should each say OK.
Windows has no shasum, and no built-in equivalent that reads a sums
file, so there is no one-step version of that on Windows. The
Get-FileHash line above is the check, run once per file, and since a
download is normally one file that is the whole job.
The app performs this same check on itself: before installing an update it
verifies the downloaded file against the checksum the manifest publishes, and
refuses to install if they differ or if no checksum was published at all. An
update that fails this check is deleted rather than installed.
Worth being clear about the limit: a checksum published on the same server as
the file proves the download was not corrupted or swapped in transit, but not
that the server itself is honest. Only code signing does that, and it costs
money yearly on both platforms, which this project does not currently spend.
Add a project #
Click + Add Project in the sidebar and pick a folder, the one holding your index.html, if you have one. You can also drag a folder from Finder or Explorer straight onto the window. The folder becomes a project, named after itself, and it stays in the list between launches.
Adding a folder only records where it is. Local Test reads your files in order to serve them; the only writes are ones you ask for, saving in the built-in editor, creating, renaming, or trashing a file, inserting an image, or a git action. Adding a folder that is already a project just selects the existing one instead of making a duplicate.
Each new project gets a port: the app default (8000 out of the box), stepped up past any port another project already claims and past anything the operating system reports as busy.
Deploy #
Select the project and press Deploy. A block appears with your address, http://localhost:8000, with buttons to copy it or open it, and your browser opens there automatically unless you turn that off in settings. The sidebar dot turns green and the header reads LIVE.
What the browser shows depends on the folder: the index.html at its root if there is one, otherwise a plain generated list of the files, which you can click through.
What a port is #
localhost means this computer. The number after the colon is the port, think of it as a numbered door on your machine. A web address needs both: which computer, and which door.
Only one program can hold a door at a time, which is why two projects need two ports, and why deploying onto a port something else is already using fails. When that happens Local Test tells you and offers the next free port as a one-click fix. Ports at 1024 and below are reserved for the system and need administrator rights, so stay above them.
Change a project's port in the field next to the Deploy button. If the project is live when you change it, it is redeployed on the new port immediately.
Stop #
Press Stop, or Cmd/Ctrl D, or use the menu-bar icon. The address stops working at once, open connections are cut so the port is free again straight away.
On macOS, closing the window does not stop anything: the app stays in the Dock and your sites keep serving, which is the platform convention. Quit the app to stop everything. On Windows, closing the window quits the app and stops the servers.
Starting from a template # changelog
If you do not have a folder yet, press New beside + Add Project (or Cmd/Ctrl N), pick a starter, and choose an empty folder to put it in. Local Test writes the files, adds the project, and it is ready to deploy.
The five starter templates.
| Template | What you get |
| Blank page | An index.html and a stylesheet, nothing else. |
| Landing page | Hero, three feature cards, a call to action, and a README. |
| Docs site | Two pages with sidebar navigation, plus a 404.html. |
| Single-page app | A small client-side router. SPA fallback is switched on for the project automatically. |
| Portfolio | A heading and a grid of project cards. |
| Spigot plugin | A Maven project a Minecraft server loads: a pom.xml, a plugin.yml and a main class. Maven rather than Gradle so it opens and builds in IntelliJ with nothing else installed. There is nothing here to Run or Deploy, because a plugin is a jar a server loads rather than a site or a program. |
Templates refuse to write over anything. If the folder already contains a file the template would create, nothing is written and you are told which file clashed.
Running software instead of serving files # changelog
Some folders are not a website sitting on disk, they are a program that produces one. For those, put a command in the project's Command field. As soon as that field has anything in it, the Deploy button becomes Run: Local Test starts that command inside the project folder, through a shell, and streams everything it prints into the Activity tab. Stop ends it.
In this mode Local Test serves nothing itself. Your program does the serving; Local Test is the launcher, the log window, and the off switch.
What gets detected automatically #
When you add a folder, Local Test looks at it once and fills the Command in for you if it recognizes the project. The checks run in this order and stop at the first match.
Detection order when a folder is added.
| Found in the folder | Command filled in | Called |
package.json with a start script | npm start | Node app |
package.json with a dev script | npm run dev | Node app |
index.html | nothing, served as a static site | , |
Cargo.toml | cargo run | Rust app |
go.mod | go run . | Go app |
manage.py | python3 manage.py runserver | Django app |
app.py | python3 app.py | Python app |
main.py | python3 main.py | Python app |
Detection happens only at the moment the folder is added. You can edit the command yourself at any time, and clearing it returns the project to plain folder-serving.
Ports for commands #
This one catches people out, so it is worth being blunt: Local Test does not tell your command which port to use, and does not check that anything is listening. The address shown on the Overview tab is built from the project's Port field and nothing else, the app is guessing on your behalf, and it says so.
So set the project's port to the port your program actually listens on, or make your program follow the project by passing the port yourself as an environment variable.
Environment variables #
The project's Settings tab has an environment variables box: one KEY=value per line. They are handed to the command on top of the environment the app already has.
PORT=3000
API_URL=http://localhost:8000
NODE_ENV=development
Names must start with a letter or underscore and contain only letters, digits, and underscores. A matching pair of surrounding quotes is stripped, so NAME="value" and NAME=value are the same thing. Lines that do not look like an assignment are ignored.
These values are stored as plain text in the app's config file, alongside your project list. That is fine for a port or a local URL; it is not the place for a credential you would mind sitting unencrypted on your own disk.
How Stop kills things #
Development commands usually start other processes, a bundler starts a server, a script starts a watcher. Local Test starts your command in its own process group and, on Stop, terminates the whole group, then forces it three seconds later if anything is still alive. On Windows it uses taskkill with the tree flag. So Stop takes the children with it, rather than leaving a stray process holding your port.
Output is cleaned of terminal color codes and each line is cut at 500 characters before it reaches the Activity feed.
Finding npm, python, and git #
An app launched from the Dock or the Start menu inherits a minimal search path, which is why GUI apps often cannot find tools you installed yourself. At startup Local Test asks your login shell what its real path is and adopts it, and adds /usr/local/bin and /opt/homebrew/bin if they are missing. That is what makes npm start, python3, and git work the same from the app as they do in your terminal.
How the server behaves # changelog
For projects without a command, Local Test runs a small static file server rooted at your folder. Its behavior is modeled on GitHub Pages, with a few conveniences that only make sense while you are still working on a site.
Which folder gets served #
A repository is not always a website at its root, often the site is a build output in a subfolder. Deploy resolves the folder to serve in this order:
- The project's Serve folder setting, if you filled it in. It must be a real subfolder of the project; if it does not exist, the deploy fails rather than quietly serving something else.
- The project root, if it contains
index.html or index.htm.
- Otherwise the first of
_site, dist, build, public, out that contains an index.html.
- Otherwise the project root anyway, which usually means you get a directory listing.
When a build folder is chosen for you, the Activity feed records it and the live block says which folder it picked.
Local Test serves what is on disk. It never runs your build. If the built site is stale, run the build yourself, or make the build the project's run command.
index.html, redirects, and 404.html #
A URL that points at a directory without a trailing slash gets a 301 redirect to the version with one, so relative links inside the page resolve correctly. With the slash, the server looks for index.html, then index.htm.
If a request matches nothing and there is a 404.html at the root of the served folder, that page is returned with status 404, exactly like GitHub Pages. Without one you get a plain built-in 404 page.
Directory listings #
On by default. Any folder without an index page renders a generated listing: folders first, then files, alphabetically, with a link back up. Dot-files and node_modules are left out, because a listing should be about the site rather than the toolchain. Turn the setting off per project and folders without an index page produce a 404 instead, which is what GitHub Pages does.
SPA fallback #
Off by default. With it on, any request that matches no file and has no file extension is answered with the root index.html at status 200. That is what a client-side router needs so that loading /about directly works instead of 404ing. Requests that do have an extension still 404, so a missing stylesheet still looks missing rather than returning HTML.
Live reload #
On by default. Local Test watches the served folder and tells open pages to reload themselves when a file changes. It works by appending a few lines of script just before the closing </body> of each HTML page as it is sent, which opens an event stream at /__localtest/reload. Your files on disk are never modified, the injection happens on the way out.
Changes inside .git and node_modules, and to .DS_Store, are ignored, and a burst of changes collapses into a single reload about 150 milliseconds later. Paired with the editor's auto-save, this is what makes the page in your browser update as you type.
Live reload relies on recursive folder watching, which is not available on every system. Where it is unavailable, serving still works normally; only the automatic reload is missing.
Range requests, caching, and methods #
- Range requests are supported, so video and audio scrub properly and interrupted downloads can resume. A range that starts past the end of the file gets a
416.
- Caching: every response carries
Cache-Control: no-cache. Browsers revalidate on each load, so an edit always shows up on refresh. This is a development server; freshness beats cache performance.
- Methods:
GET and HEAD only. Anything else gets a 405 with an Allow: GET, HEAD header. A form that posts to the server has nothing to post to.
- Content types are set correctly for the usual web formats, HTML, CSS, JavaScript, JSON, SVG, PNG/JPEG/GIF/WebP/AVIF/ICO, WOFF and WOFF2, MP4/WebM/MP3/WAV/OGG, WASM, PDF, Markdown, XML, ZIP, with
charset=utf-8 on text. Anything unrecognized is sent as application/octet-stream.
- Every response is logged to the project's Activity feed with its status and how long it took:
GET /index.html → 200 (2ms).
Staying inside the folder #
Every requested path is resolved against the served folder and checked twice: once literally, to stop ../ and encoded traversal, and once after following symbolic links, so a repository that ships a link pointing somewhere else on your disk cannot read through it. Anything landing outside is refused with 403. The Files tab, the project search, and the editor use the same guard, so nothing in the app reaches outside the project folder.
Local Test also refuses to add, serve, or run its own installation folder.
What it binds to #
The server binds to 127.0.0.1 (this machine and nothing else) unless you turn on network sharing in app settings, in which case it binds ::, which takes IPv6 and IPv4 together, and the live block lists every address other devices can open. Saving that setting re-listens the sites that are already running, so it takes effect at once in both directions. It has a section of its own: sharing on your network.
Either way, the server answers only to the names it is genuinely reachable under: localhost and 127.0.0.1, plus this machine's own network addresses and its network name when sharing is on, plus anything you listed under other hostnames this machine answers to. A request arriving under any other name gets a 403. That is what stops a page on the internet pointing a domain it owns at your machine and reading your project through your own browser.
Three things then differ by who is asking, and all three are decided per request rather than once at deploy time. Folder listings are generated for this machine and never for another device. A network client is handed only the file types a web page is made of, unless the project says otherwise. A request from outside your own local network is refused before anything is read, unless the project says otherwise. Requests from localhost pass all three untouched: on this machine, your folder is your folder.
Project settings #
Each project has its own Settings tab. Nothing here affects any other project.
Per-project settings, with the value you get if you never touch them.
| Setting | Default | What it does |
| Name | folder name | Editable in the header. Display only, the folder is not renamed. |
| Port | 8000 | Taken from the app default when the project is added, stepped past ports other projects hold and ports the system reports busy. Any whole number from 1 to 65535. |
| Command | empty | Empty means serve the folder. Anything else means run that command instead. Filled in automatically for recognized project types. |
| Environment variables | empty | One KEY=value per line, passed to the command. |
| Live reload | on | Refresh open browser tabs when a file in the served folder changes. |
| Directory listings | on | Generate a file list for folders that have no index page. Only ever for this machine: other devices on the network get a 404 for such a folder, whatever this is set to. |
| SPA fallback | off | Answer unknown extensionless routes with the root index.html. |
| Serve every file type on the network | off | Off, another device gets only the file types a web page is made of and a 403 for the rest. On, it gets every file in the served folder, the same as this machine. Nothing to do with what this machine sees, which is always everything. |
| Answer requests from outside your own network | off | Off, a request from an address that is not on your own local network is refused before anything is read. On, it is answered. Only reachable at all because sharing listens on IPv6, which has no NAT in front of it. Read this first. |
| Serve folder | empty (auto) | Which subfolder is the website. Empty means the root, or a detected build folder. Must stay inside the project. |
| GitHub repository | empty | An owner/name pair (or a full git URL) for Sync now. |
| Pull before every deploy | off | Fetch the latest from the linked repository each time you deploy. Switched on automatically for projects added through Add from GitHub. |
Changing the port, the serve folder, live reload, listings, or SPA fallback while a static project is live redeploys it immediately so the change takes effect, no stop-and-start needed.
The same tab holds Duplicate project (a second entry pointing at the same folder, on its own port) and Remove project, which takes the project out of Local Test's list after a confirmation and never touches the folder on disk.
App settings #
The gear in the top right opens the app-wide settings, with explicit Save and Cancel buttons. Theme changes preview live while the dialog is open; Cancel puts them back.
App-wide settings, with defaults.
| Setting | Default | Notes |
| Theme | Default | Ten themes: Default, Midnight Harbor, Neon Drift, Amber Terminal, Rose Quartz, Matcha Cream, Glacier, Paper & Ink, Pocket Green, and Berry Pocket. Default follows your system's light or dark setting. Each is shown as its two colors with its name set in its own font. |
| Font size | Default | Compact, Default, Comfortable, or Large, 12, 13, 14, and 16 pixels. |
| Default port for new projects | 8000 | The starting point for the port of the next project you add. |
| Open browser automatically after Deploy | on | Applies to deploys that serve a folder. Running a command never opens a browser. |
| Allow devices on my network (LAN) | off | Serves your sites to the network as well as to this machine, and lists every address to use. Applies to sites already running, not just the next deploy. No password, so anyone on that network can read a deployed project. May raise the firewall prompt the first time. See sharing on your network. |
| Other hostnames this machine answers to | empty | One hostname per line, for names Local Test cannot discover on its own: a hosts file entry, or a tunnel address. Applies whether or not sharing is on. A line that is not a hostname is refused when you Save, with the reason. See other hostnames. |
| Check for updates when the app opens | on | One request a few seconds after launch to see whether a newer version exists. Nothing appears unless there is one. |
| Clone folder | Documents/Local Test | Where Add from GitHub clones repositories. |
| Personal access token | empty | Optional. Raises GitHub's rate limit and lists your private repositories. Kept on this machine, shown masked afterwards, left out of backups. See the optional token. |
| License key | empty | Removes the five-project limit. See unlocking VIP. |
| AI provider | Anthropic | Which of the three providers AI mode talks to: Anthropic, OpenAI, or Ollama running on this machine. |
| Model | the provider's default | Which model that provider is asked for. The hosted lists are built in with their prices; Ollama's is read off this machine. |
| API key | empty | Your own key with the chosen provider, used by AI mode and nothing else. Kept on this machine, shown masked afterwards, left out of backups, never logged, sent only to that provider. Ollama needs none. See where the key lives. |
The same dialog holds Export and Import for your projects and settings, and the Uninstall button described under updating and uninstalling.
Sharing on your network # changelog
By default a deployed site is yours alone. The server listens on 127.0.0.1, which is the address a machine uses to talk to itself, so a phone or a laptop on the same Wi-Fi asking for that port is not refused a page, it is refused a connection: the browser says it cannot connect, because nothing on that network is listening.
Allow devices on my network (LAN) to view deployed sites, in the gear menu, changes that. With it on, every deployed site also listens on the addresses this machine holds on real networks, and the live block under each project gains a list of links that look like http://192.168.1.20:8000/. Open one on your phone and you get the site, live reload and all.
This is the only setting in Local Test that lets something other than your own machine read your files, so the rest of this section is about where it stops. Several of the behaviors below are refusals rather than features, and they are the reason the setting is safe enough to offer at all.
Opening a site on your phone #
- Put the phone on the same Wi-Fi as the computer. Not the guest network, not cellular data. This is the step that goes wrong most often.
- Open the gear in the top right, tick Allow devices on my network (LAN) to view deployed sites, and Save.
- If your Mac or Windows machine asks whether to allow incoming connections, allow it. See the firewall prompt below.
- Look at the project's Overview tab. Under the localhost link there is a line reading On your network, with every address the machine answers on.
- Type one of them into the phone's browser, including the port. There is nothing to install on the phone.
All of the links are ordinary text you can copy. If typing four numbers on a phone is unappealing, mail yourself the link, try the machine's own name, or use the Phone and Tablet buttons in the same block to preview at those sizes in a window on the computer instead, which needs no network at all.
Why there is a list of addresses rather than one #
A computer usually has more than one network address. A VPN adds one. Docker adds one. So do VirtualBox, Parallels, WireGuard, Tailscale, and the private radios macOS keeps up for AirDrop. They all report themselves to the operating system exactly like a real network card, and only one of them is the Wi-Fi your phone is on.
Local Test used to print the first address the system happened to list, which is correct only on a laptop that has exactly one. With a VPN up it confidently printed a tunnel address no phone could ever reach, and sharing looked broken. Now every candidate is ranked and all of them are shown, best guess first:
- A real network card holding an ordinary home or office address (
10.x, 192.168.x, 172.16 to 172.31, or the IPv6 equivalent).
- A real network card holding some other address.
- A virtual interface: a VPN tunnel, a container or VM bridge, an AirDrop radio.
Within a card, the IPv4 address sits just ahead of the IPv6 address beside it, because a phone joining a home Wi-Fi always gets an IPv4 address and does not always get an IPv6 one. Every address in the list genuinely works, so if the first one does not load, try the next.
Two addresses are treated specially. An IPv4 address in 169.254.x means nothing ever assigned this interface an address, so it is dropped rather than offered. Its IPv6 counterpart, fe80::, is always present on every interface and does genuinely work between two devices on the same wire, so it is kept, sorted last, and shown only when nothing better exists, which on an IPv6-only network is the difference between an address and none at all. It carries a zone index (fe80::1c2b%en0) saying which interface it means, because the same address exists on all of them at once.
IPv6 addresses are shown wrapped in brackets, as URLs require: http://[fd00::a1b2]:8000/. A zone index has its percent sign written %25 in a URL, or the browser reads what follows as an escape and rejects the whole address.
What the server binds to #
With sharing off, the socket binds 127.0.0.1 and only this machine can open it.
With sharing on it binds ::, which takes both IPv6 and IPv4 on every platform this app runs on. It used to bind 0.0.0.0, which means "every interface" for IPv4 and nothing whatsoever for IPv6, so on a network handing out no IPv4 the setting went on, the socket listened, and not one device could reach it. Where a kernel refuses IPv6 outright, Local Test falls back to 0.0.0.0 rather than failing the deploy over it.
Binding both families is what makes sharing work at all on a modern network, and it is also what makes the restriction below necessary, because IPv6 addresses are not behind NAT the way IPv4 addresses are.
The name your machine answers to #
Numbers are unpleasant to type on a phone, and they change when the router hands out a new lease. Your machine also has a name. Bonjour publishes it as name.local on macOS and on any network with an mDNS responder, and Windows resolves the bare name on its own network.
Local Test accepts both spellings, so http://studio.local:8000/ works from a phone that can resolve it. Whether a given phone can resolve it is not something this machine controls: iOS and macOS do, recent Android does, and plenty of networks quietly block mDNS. If the name does not load, the numeric address still will.
Every request carries the name it was made under, and Local Test refuses any name it is not genuinely reachable under with a 403. That check is what stops a page on the internet pointing a domain it owns at 127.0.0.1 and reading your project through your own browser, so it cannot be relaxed. It also means the app has to know every legitimate name, and there are two it cannot possibly discover:
- A line you added to your hosts file, pointing a made-up name like
myapp.test at this machine.
- The public name of a tunnel, such as
quiet-fox-42.ngrok-free.app or a Cloudflare Tunnel hostname.
Other hostnames this machine answers to, in app settings, is where those go, one per line. It is app-wide rather than per project, because a hosts file entry or a tunnel is a property of the machine. It applies whether or not network sharing is on, because the commonest entry by far points at 127.0.0.1 and needs no network at all.
Each line is checked when you press Save, and a line that is not a hostname is refused with the reason rather than quietly dropped: That is not a hostname: http://myapp.test. One name per line, with no scheme, port or path. A dropped entry looks exactly like a working one right up until the phone says Forbidden, and then there is nothing to read.
Naming a tunnel here makes Local Test answer it. It does not create the tunnel, and it does not make the site safe to leave on one. Read this is not a way to put a site on the internet before you point anything public at a deployed project.
What another device is allowed to read #
To this machine the served folder is your own folder. To another device it is a website, and those are different amounts of trust. Two rules follow from that, and both are decided per request from where the request came from, not once at deploy time.
Folder listings are never generated for a network client. On this machine they follow the project's own Directory listings setting. To another device, a folder with no index page reads as missing, whatever that setting says. A listing is a convenience for you and an inventory of the project for everyone else on the Wi-Fi.
Only the file types a web page is made of travel. Another device is served pages, styles, scripts and source maps, WebAssembly, plain text, XML, PDF, web manifests, images, fonts, video and audio. Everything else gets a 403 with a page saying which switch turns it on. The list, exactly:
What a network client may be handed, by extension.
| Kind | Extensions |
| Pages and code | .html .htm .css .js .mjs .map .wasm |
| Text and documents | .txt .xml .pdf .webmanifest |
| Images | .png .jpg .jpeg .gif .webp .avif .svg .ico .bmp |
| Fonts | .woff .woff2 .ttf .otf .eot |
| Video and audio | .mp4 .webm .ogv .mov .m4v .mp3 .wav .ogg .oga .m4a .aac .flac |
| JSON, conditionally | .json |
JSON is the awkward one, as likely to be configuration as to be part of the site, so it travels only out of a folder that has a page in it, on the reasoning that a page there could plausibly fetch it. A folder of nothing but JSON is somebody's data directory, not a website. A file with no extension at all is refused too, because nothing a page links to looks like that.
The refusal is decided from the URL before anything is read off disk, so it says nothing about whether the file exists: /db.sqlite and /nothing-like-it.sqlite get the same answer. The 403 page names the file, says why, gives you the localhost URL that still works on this machine, and names the switch, because the person hitting it is usually the owner holding their own phone and wondering why their own file will not load.
That switch is Serve every file type on the network, in the project's Settings tab, off by default. On, another device gets every file in the served folder, the same as this machine already does. It is per project, so turning it on for a folder of demo videos does not turn it on for the folder with the database in it.
Requests from outside your own network #
"Let the other devices on my Wi-Fi see this" is what the setting says and what people mean by it. On IPv4 that is what binding every interface amounts to in practice, because a home or office machine sits behind a router doing NAT and nothing outside can address it directly.
IPv6 has no NAT. A machine holding a routable IPv6 address is reachable from the internet at that address, so binding both families would have quietly turned the setting into "let anyone who learns this address see this". That is not a change to make on somebody's behalf, so the bind stays dual stack and the client's own address decides whether it is answered.
A request from outside your local network is refused with a 403 before anything is read and before any path is resolved, which matters: a refusal that happens after a file is checked tells whoever is asking whether that file exists. Counted as local are loopback, the private IPv4 ranges (10.x, 192.168.x, 172.16 to 172.31), IPv4 link-local (169.254.x), the carrier-grade NAT range 100.64 to 100.127 that Tailscale also uses, and the IPv6 link-local and unique-local ranges (fe80::/10 and fc00::/7).
The per-project switch Answer requests from outside your own network lifts that, and it is off by default. Read the next heading before you use it.
This is not a way to put a site on the internet #
Said plainly, because the two switches above make it technically possible and nothing about them makes it wise. Local Test has no accounts, no passwords, no rate limiting, and no HTTPS. It is a development server reading a folder on somebody's desk, and it was not built to face the open internet.
The refusals described on this page are also narrower than they look. What is refused to everyone, including you, is a list of names: anything beginning with a dot (which covers .env and .git), node_modules, and obvious key material (id_rsa, id_ed25519, *.pem, *.key, *.p12, *.pfx). A name is a poor thing to defend a folder with. secrets.txt, db.sqlite, backup.zip, dump.sql and config.json are not on that list and never can be, because the list would have to know every name anybody ever gives a file.
On loopback that is fine: the only person who can ask is the person who owns the folder. On a network it is not, which is exactly why a network client gets the allow list instead, naming what a page is built out of and refusing the rest. Turn Serve every file type on the network on and you are back to the name list, on a folder that anyone who can reach the port can read.
If you want a site on the internet, publish it somewhere built for that. If you want to show one person one page for ten minutes through a tunnel, that works, and the honest description of it is a hole you opened deliberately and should close afterwards. Stop is how you close it.
What you are opening up #
Worth saying rather than burying: there is no password. While a project is deployed and sharing is on, anyone on that network can read it by opening the address. On your own Wi-Fi at home that means your household. On a cafe or an office network it means everyone else on it.
What they can read is the served folder, filtered to the file types a page is made of, with no folder listings to browse from. Dotfiles, node_modules and private key files are refused to everyone including you. The path jail still holds, so nothing outside the project folder is reachable either way, however a link inside it points.
Live reload is bounded on the same reasoning. A project holds at most 24 open reload connections, of which at most 8 may come from the network, and when the two compete the network client is the one dropped. Without that, a visitor with twenty reconnecting tabs could hold every slot, and the only symptom on this machine would be that saving a file quietly stopped refreshing the browser.
The firewall prompt #
The first deploy after turning sharing on may raise your operating system's firewall prompt, asking whether to allow incoming connections. That is the firewall doing its job, not something going wrong. Allow it, or the address will be advertised and never answer.
On Windows the prompt names Local Test and offers private and public networks; private is the one you want. On macOS the prompt appears when the built-in firewall is switched on, which it is not by default; if you dismissed it once and the address stopped working, the allow list lives in System Settings under Network, Firewall, Options. Because Local Test is not code signed, macOS may ask again after an update.
When it takes effect, and what it skips #
Saving the setting applies it to sites that are already running, not only to the next deploy. Each running static project is stopped and started again on the new addresses within the same click, keeps its port, and does not count as a deploy. Turning it off does the same in reverse, so a site comes off the network immediately rather than staying there until you remember to stop it.
The Activity feed says what happened, including when the answer is nothing: with sharing on and no network attached, it records Network sharing is on, but this machine has no network address right now, so there is nothing to share yet. Silence there would read as the setting having failed.
Projects that run a command are not affected at all. What those listen on is decided by the software you are running, not by Local Test, and most development servers bind to localhost unless told otherwise. Their own flag is usually --host. Nothing on this page applies to them: not the file type filter, not the address check, not the listings rule.
If you want a site off the network right now and do not want to touch settings, Stop is enough: nothing is served when nothing is deployed.
A Minecraft server is the third thing Local Test can start for you. The first is a folder served as a website. The second is a command you wrote. This is neither: Local Test fetches or builds the server software, writes the eula.txt and server.properties that have to sit beside it, and runs it with Java, in a folder you choose. It sits in the sidebar like any other project, and Stop ends it the same way.
Everything above this heading has been about HTTP. This part is not. A Minecraft server speaks its own protocol over TCP, so there is no page to open and no link to click, and Local Test does not offer one. Connecting, below, is about Minecraft's own Add Server box instead.
All five flavours are servers for Minecraft: Java Edition, which is the version of the game this whole feature is about. Bedrock, which is what runs on phones, consoles, and the edition sold in the Microsoft Store, speaks a different protocol and cannot join any of them. Everybody connecting needs Java Edition.
What the app asks for is a flavour, a Minecraft version, a folder, and how much memory the server may have. Almost everything that happens next is decided by the first of those, which is why the flavours come first.
The five flavours #
The useful thing about these four is not their feature lists. It is that two of them are a download and two of them are not, which is the difference between a server that is ready in under a minute and one that is ready in twenty.
The five flavours, and how each one arrives on your machine.
| Flavour | How it arrives | What it is |
| Vanilla | Download from Mojang | Mojang's own server, exactly as they ship it. No plugins at all. |
| Paper | Download from PaperMC | A faster Spigot, and what most people now mean by a plugin server. Runs Spigot and Bukkit plugins unchanged. |
| Purpur | Download from PurpurMC | Paper with several hundred extra settings turned into options: ridable mobs, tweakable drops, and a lot of small rules. Runs the same plugins. |
| Spigot | Compiled on your machine | The plugin server Paper is a fork of. Runs the same plugins. Five to twenty minutes to build, and it needs git. |
| CraftBukkit | Compiled on your machine | The original plugin server, built the same way and for the same reason. Older plugins that will not run on Paper sometimes want it. |
Paper is the sensible default, and the reason is worth stating rather than leaving as a preference. Somebody who says "I want a Spigot server" today almost always means a Paper one: Paper is a fork of Spigot, it runs Spigot and Bukkit plugins unchanged, and it is markedly faster once there are players or redstone in the world. It is also a download, so it is ready in the time it takes to fetch a file. If you have no particular reason to pick something else, pick Paper.
Vanilla is the right answer when you want the game as Mojang ship it and no plugins at all. CraftBukkit is the right answer roughly once a year, when a plugin is old enough that nothing else will load it. Spigot itself is still on the list because some people need exactly it, and because the reason it cannot be downloaded is worth understanding before you pick it by accident.
Spigot and CraftBukkit cannot be distributed as a binary. Not by this app, and not by anybody: they contain decompiled Mojang code, and Mojang's licence does not permit that to be handed out as a file. What SpigotMC publish instead is BuildTools, a jar that fetches the pieces on your own machine and compiles them there. The finished server never travels, because it is made where it is used, and that is the only lawful route to a Spigot server. An app that handed you a ready-made Spigot jar would be putting you on the wrong side of that licence, quietly, on your behalf.
So choosing Spigot or CraftBukkit does not download a server. It downloads BuildTools and then compiles one, and on an ordinary machine that takes five to twenty minutes.
This is the part to know before you pick it. Twenty minutes with nothing obvious happening looks exactly like an app that has hung, and the reasonable thing to do to an app that has hung is force quit it, which throws away the build and leaves a half-finished folder behind. Local Test says how long it will take before it starts, and the compiler's own output goes into the Activity tab as it runs, so there is something moving to watch. It is compiling. It has not stopped.
BuildTools needs git on the machine, because the first thing it does is clone the repositories it builds from. On a Mac, git arrives with the Xcode command line tools: run xcode-select --install in a terminal once and accept the prompt. On Windows it comes with Git for Windows, which is the same git the rest of Local Test's git features use. Without it the build cannot begin, and the app says so rather than starting and failing several minutes in.
What you are left with is a jar named for what was built and which version it is, spigot-1.21.8.jar or craftbukkit-1.21.8.jar, sitting in the server folder. The compile happens once for that version, not once per start: starting the server afterwards is as quick as any other flavour.
Paper exists partly to avoid all of this. It is a download, it runs the same plugins, and it is faster. Twenty minutes of compiling to end up with something slower than the alternative is a choice worth making deliberately.
The EULA, and why the app will not accept it for you #
Mojang require their end user licence agreement to be accepted before a server will run. That is Mojang's rule, not this app's. Every Minecraft server keeps a file called eula.txt next to it and refuses to start until the line inside it says true.
Local Test writes that file, with the line set to false, and it will not set it to true for you. This is exactly what it puts on disk:
# Mojang require you to accept their EULA before a server will start.
# https://aka.ms/MinecraftEULA
# Local Test wrote this file. It did not accept anything for you: the line
# below says what you chose in the app.
eula=false
The app then asks, once, with the real address in the question: https://aka.ms/MinecraftEULA, which is Mojang's own address for the agreement rather than a summary of it written here. Say yes and the line becomes eula=true and the server starts. Say no and it does not, which is a correct outcome rather than a fault.
The reason for the refusal is not squeamishness about paperwork. That agreement is between the person running the server and Mojang. It is not this app's to sign, and there is no version of "I ticked it for you" that is honest: an app that accepts a licence in your name has agreed to something you may not have read, and the fact that nearly everyone would have agreed anyway is not what makes it somebody else's decision to take.
If the Activity tab ever shows You need to agree to the EULA in order to run the server and the server exits a second after starting, that is Minecraft's own message, and it means the line still says false.
Java #
A Minecraft server is a Java program. Java is a separate thing from Minecraft, Local Test does not bundle it, and a machine that has never run one will not have it. If it is missing, the app says Java is not installed, and a Minecraft server is a Java program. and points at where to get it.
Minecraft is strict about which version, and unhelpful about saying so: start a modern server on an old Java and what comes back is a stack trace about an unsupported class file version, which tells you nothing unless you already knew. So the check happens up front, before anything is downloaded, and the message is a sentence: This version needs Java 21, and the Java on this machine is 17.
Local Test does not guess which Java a version wants. Mojang publish that number per version, in the same manifest the download itself comes from, and the app reads it from there and compares it with the Java it can actually see on this machine. Paper follows the Minecraft version it was built for, so the authority is the same manifest there too. A hard-coded table in an app is a table that is wrong the week after a release; this one cannot go stale, because it is not a table.
As a rough guide, if you want to know before you pick a version:
What each era of Minecraft wants. The app reads the real answer per version rather than using this.
| Minecraft version | Java it asks for |
| 1.20.5 and newer | 21 |
| 1.18 to 1.20.4 | 17 |
| 1.17.x | 16 |
| 1.16.5 and older | 8 |
Where to get it: Adoptium publish a free build of Java for every platform at adoptium.net, which is the address the app's own message names. Take the version the app asked for, or a newer one.
That message ends Install Java 21 or newer from adoptium.net. Both can be installed at once. and the last sentence is the one people hesitate over, so it is worth expanding. Having two versions of Java installed at the same time is fine, and normal. They live in separate folders and do not overwrite each other; installing 21 for a modern server does not disturb the Java 8 that something else on the machine still needs, and plenty of machines carry three without trouble. What can change is which one the bare word java means, which your system path decides. That is why the app checks the version it can actually see rather than assuming the newest one installed is the one that will run.
What is checked, and the one download that is not #
Three kinds of file travel over the network here: a server jar from Mojang, a server jar from PaperMC, and BuildTools from SpigotMC. Two of the three are published with a checksum by the people who built them. The third is not, and that difference is stated here rather than smoothed over.
What each download arrives with, and what is verified before anything runs.
| Flavour | Published checksum | Checked before it runs |
| Vanilla | SHA-1, from Mojang's own manifest | Yes. A jar that does not match is not kept. |
| Paper | SHA-256, from the PaperMC build record | Yes. A jar that does not match is not kept. |
| Purpur | MD-5, which is all PurpurMC publish | Yes, and the app says what that does and does not catch: a damaged download, yes; a file built on purpose to match, no. |
| Spigot | None published | No. BuildTools has no checksum to check it against. |
| CraftBukkit | None published | No. The same BuildTools download. |
The app says the same thing in the same words when you pick one of the two that compile: SpigotMC publish no checksum for BuildTools, so this download is protected by HTTPS and the pinned host and nothing further.
Saying that out loud is the point of this heading. A page claiming everything here is verified would be describing an app that does not exist, and it would be claiming it about the one download that then runs a compiler on your machine. What that download does have is a fixed address written into the app rather than read out of any manifest, HTTPS, and a host on the list below. That is real protection and it is not the same as a checksum, and the difference belongs in front of you rather than in a footnote.
The host allowlist
Before any request is made, the address it would go to is checked against a list of hosts. The check is on the URL that is about to be fetched, not on the document that suggested it, and that is the whole idea: a manifest is data from the internet. It says where the jar is. If a manifest were tampered with, a mirror compromised, or a DNS answer swapped, the natural way to attack this is to make the app fetch the jar from somewhere else, and it would look entirely normal while doing it. An address on any other host is refused, nothing is downloaded, and the app says which host it refused.
Every host Local Test will fetch a Minecraft server file from. There are no others.
| Host | What comes from it |
launchermeta.mojang.com | Mojang's list of every Minecraft version. |
piston-meta.mojang.com | The details of one version, including which Java it wants and the jar's SHA-1. |
piston-data.mojang.com | The Vanilla server jar itself. |
fill.papermc.io | PaperMC's API: which versions and builds exist. |
fill-data.papermc.io | Paper jars. |
api.papermc.io | PaperMC's older API, which still answers for some versions. |
api.purpurmc.org | The Purpur build list, and the jars it serves. |
hub.spigotmc.org | BuildTools. |
A plain http address is refused as well, whatever host it names. Everything on that list speaks HTTPS, and a jar fetched over http could be rewritten by anything sitting between this machine and the publisher.
The honest limit, the same one as for checking a download of Local Test itself: the allowlist stops a download being pointed somewhere else, and a checksum stops a file being altered on the way to you. Neither can tell you that the publisher's own build was sound. That is a different problem and nothing in this app solves it.
Connecting to it #
A Minecraft server does not speak HTTP. It has its own protocol on its own port, and a browser pointed at that address gets nothing it can display, so there is no link to click and Local Test does not offer one. It also does not poll the address for a status code the way it watches a website: a perfectly healthy Minecraft server would not answer that either, and reporting a working server as broken is worse than saying nothing.
The address goes into Minecraft itself. In the game: Multiplayer, then Add Server, then type the address into the Server Address box.
What to put in Minecraft's Server Address box.
| Where you are playing | What to type |
| On this machine | localhost |
| Another device, network sharing on | 192.168.1.20 |
| On any port but the default | localhost:25570 |
The address in the middle row is an example. Use whichever address the app lists for your own machine, which will not be that one.
The app puts it plainly on the project: Port 25565 is the default, so the port can be left off when adding the server in Minecraft. Minecraft assumes that port when the address does not name one, which is why the everyday case is a single word. Change the port and the address has to carry it, and the app says that instead.
Sharing with somebody else in the house works exactly the way sharing a site does, and for the same reason: turn on Allow devices on my network (LAN) in app settings, and the machine's network addresses appear beside the local one, best guess first. The address to give a friend on the same wifi is the network one. Handing them localhost hands them their own machine, where nothing is running.
Everything on this page about the firewall prompt applies here too, and so does the warning underneath it: that reaches your own network, not the internet. Putting a Minecraft server on the internet needs a port forwarded on your router or a tunnel of some kind, Local Test does neither, and a server on the open internet is somebody else's to find. Read this is not a way to put a site on the internet, which is the same argument with a different protocol.
Memory, and why most of the machine is the wrong answer #
Local Test suggests half of what the machine has, rounded down, never less than 1 GB and never more than 8 GB. You can change it. The suggestion is deliberately unambitious, and the instinct it is arguing with is the one that says a server will be faster with more.
It will not, past a point, and it gets worse in two separate ways. The operating system, your browser, your editor and, if you are playing on the same machine, Minecraft itself all need memory of their own, and the client alone wants a couple of gigabytes. A Java server told it may have nearly everything will take nearly everything, and the moment the machine starts swapping to disk to keep up, all of it slows down, the server included. The second way is quieter: a very large heap makes the garbage collector's pauses longer, and a pause in a Minecraft server is a lag spike for everyone on it. Past about eight gigabytes the returns have gone, for the size of world this app is for.
Less is a real answer too. A server will run in 1 GB and will not enjoy it. Two gigabytes is comfortable for a few people on a small world, four is comfortable for a handful with plugins, and a server that genuinely needs more than eight is a server that wants a machine of its own.
This is the command Local Test runs, with the memory you chose written into it:
java -Xms1024M -Xmx4096M -jar server.jar nogui
-Xms is the heap it starts with, -Xmx is the ceiling it may grow to. The ceiling is your number.
-jar names the file to run, which is whatever ended up in the folder.
nogui matters more than it looks. Without it the server jar opens a small window of its own with a chat box in it, which is a second window nobody asked for, and which keeps the process alive after Stop has asked it to go.
Where the files go #
You choose a folder, and then whether the server lives in that folder or in a subfolder of it. Both are right in different situations, which is why the app asks rather than deciding:
- The folder itself, when you made the folder for this. A server is a tidy thing in an empty folder.
- A subfolder, usually called
server, when the folder is already a project of yours. A running Minecraft server writes a world, logs, and a dozen files of its own into wherever it lives, and none of that wants to be mixed in with your files.
The subfolder is one level and a plain name. A path there would be a way to write outside the folder you picked, and picking a folder is the point of asking, so it is refused with The subfolder has to be a plain name: letters, digits, dots, dashes and underscores. Not a path.
What ends up in the server folder, and who put it there.
| File or folder | What it is |
server.jar | The downloaded server, for Vanilla and Paper. |
spigot-1.21.8.jar | What BuildTools compiled, named for what was built and for the version. craftbukkit-1.21.8.jar for the other one. |
eula.txt | Written before the first start, with your answer in it. See the EULA. |
server.properties | Written before the first start, with the settings below. |
plugins | Where plugin jars go. Paper, Spigot and CraftBukkit only; Vanilla has no such thing. |
world | The world itself, made by Minecraft on the first start, along with world_nether and world_the_end. |
Minecraft writes a handful of others as it goes, logs and player lists among them. Those are its business and Local Test leaves them alone.
Before it writes anything, Local Test looks at the folder and tells you what is already in it. Installing on top of an existing server is the one mistake in this whole feature that cannot be undone: a world folder written over is somebody's building gone, and nothing afterwards brings it back. Point a new server at an empty folder, or at a subfolder of its own. The world folders are also the thing worth backing up, because everything else here can be downloaded or compiled again.
The settings it writes
Minecraft's own server.properties runs to around sixty keys. A generated file with sixty keys in it invites nobody to read any of them, so Local Test writes the eleven worth a decision and leaves the rest to Minecraft, which fills them in with its own defaults the first time the server starts.
The eleven settings Local Test writes, with the value you get if you change nothing.
| Setting | Default | What it does |
motd | A Local Test server | The line shown under the server's name in Minecraft's list. |
server-port | 25565 | The port the server listens on. 25565 is Minecraft's own default, and the one a client assumes when the address it is given has no port on it. |
max-players | 20 | How many people can be on at once. It has to be at least one. |
gamemode | survival | survival, creative, adventure, or spectator. |
difficulty | easy | peaceful, easy, normal, or hard. |
online-mode | true | Every player is checked against their Mojang account. Read the warning below before turning it off. |
pvp | true | Whether players can hurt each other. |
spawn-protection | 16 | The radius around spawn, in blocks, that ordinary players cannot build in. |
view-distance | 10 | How far players can see, in chunks, from 2 to 32. This is the setting that costs the most memory. |
level-seed | empty | The seed the world is generated from. Empty means a random one, and the key is left out of the file entirely rather than written blank. |
white-list | false | When true, only players you have listed can join. |
One of those gets a warning rather than a description, because it is the setting people turn off without knowing what it does: With this off, anybody can join as any username, including yours. It is for offline play on a network you trust, not for a server on the internet. With online-mode on, the server asks Mojang whether the person joining is who they say they are. With it off, it asks nobody.
Files, editing, and search # changelog
Browsing #
The Files tab lists the project folder with breadcrumbs and file sizes. At the project root the README is rendered underneath the listing, the way a code host does it, with spec.md as a fallback when there is no README.
Click a file to open it: text is shown as it is, Markdown is rendered, images are previewed. Files with no text in them are reported as binary rather than shown as noise. Practical limits: images over about 3 MB and text files over about 1.5 MB are not previewed, and a very long text preview is cut at 200,000 characters.
The editor #
Click the pencil on any text file (Markdown, HTML, CSS, JavaScript, JSON, and the rest) to edit it in place. There are three modes: Edit, Split, and Preview. Markdown files get a formatting toolbar: headings, bold, italic, strikethrough, inline code, links, quotes, bullet lists, task lists, code blocks, tables, and dividers.
- Cmd/Ctrl B, I, and K apply bold, italic, and a link. Applying the same wrap again removes it.
- Enter continues a list, keeping numbering and task boxes; pressing it on an empty item ends the list.
- Tab inserts two spaces.
- Cmd/Ctrl S saves. The foot of the editor shows word, character, and line counts, and a reading-time estimate.
- Auto-save, when ticked, writes the file about eight tenths of a second after you stop typing. With live reload on, that means the real page in your browser updates as you write.
- Find and replace is the Find button or Cmd/Ctrl Alt F. It matches plain text, case-sensitively, no regular expressions.
- Image copies a picture from anywhere on your computer into the project's
images folder and inserts the Markdown that links to it.
Saving is capped at 5 MB per file. Switching to another project with unsaved changes asks first, unless auto-save is on, in which case it saves and moves on.
Searching a project #
The box at the top of the Files tab (Cmd/Ctrl F) does two jobs at once: it filters the names in the current folder, and from two characters onwards it searches the text inside every file in the project. Results show the file, the line number, and the line itself; clicking one opens that file in the editor.
The search deliberately skips .git, node_modules, dist, build, _site, out, .next, coverage, and vendor, along with dot-files, files over 1 MB, and anything that is not a recognized text type. It stops after 200 matches.
Creating, renaming, deleting #
New file and New folder create inside the folder you are looking at; a new .md file opens in the editor with its name already set as a heading. Aa renames (a name without slashes, to move a file, use Finder or Explorer). The trash icon asks first and then moves the file to the Trash rather than deleting it outright, so a mistake is recoverable.
Commit and push #
If the project folder is a git repository, its Settings tab shows the branch, how many files changed and which, how far ahead or behind the remote you are, and the last commit. Write a message, press Commit all (which stages every change in the folder and commits it) then Push. The first push on a new branch sets its upstream for you.
Local Test runs the git already installed on your machine and uses the credentials it already has. It never asks for, stores, or transmits a password or token, and it never writes to a repository unless you press one of those buttons.
Syncing a project from GitHub #
Put owner/name in the project's Repository field and press Sync now. Into an empty folder that clones; into an existing clone it does a fast-forward-only pull. A folder that already has files but is not a repository is refused rather than overwritten. Tick Pull the latest before every deploy and the project updates itself every time you deploy.
Add from GitHub #
In app settings, choose a Clone folder, type any owner/name under Add from GitHub, and press Add. The repository is cloned into that folder as a new project with pull-before-deploy already switched on, and its project type is detected the same way as any other folder.
Private repositories work only if you are already signed in to git on this machine. Rather than hanging on an invisible password prompt, the attempt fails immediately with a message saying the repository may be private or misspelled.
A project added this way gets its own free port, stepped past the ports other projects already hold, so cloning several in a row does not leave them all fighting over the default.
Adding repositories in bulk # changelog
Add from GitHub takes one repository at a time. When you have a folder full of clones, or an account full of repositories, the Find repositories button in app settings (also in the command palette) opens a picker that adds as many as you tick. It has two panes, and both end in the same list of tick boxes.
Scanning a folder of clones you already have #
Already on this machine starts on your clone folder and lists every git repository inside it. Choose folder points it somewhere else, Rescan reads it again. It looks at the folder itself, at every folder directly inside it, and one level deeper, which covers both the GitHub/repo and the GitHub/owner/repo layouts. Folders whose name begins with a dot are skipped, so is node_modules, and the scan stops at 300 repositories and tells you it did.
Nothing is copied, moved, or run. To describe each repository Local Test reads two text files inside it, .git/HEAD and .git/config, rather than running git. That keeps a folder of two hundred clones fast, and it means a repository's own config never gets the chance to name a program for git to run.
Each row reports:
- the folder name;
- the current branch, or detached at plus the short commit id when the checkout is detached;
index.html when the folder has one, and runs plus the command when a start command was detected, the same detection that runs when you add any folder by hand;
- no page or run command found when it has neither, which is a warning that deploying it will show a file listing;
- where it came from: the
origin remote, shown as owner/name when it is a GitHub remote and as the raw URL when it is not.
Repositories that are already projects are greyed out rather than hidden: the row is dimmed, labelled already a project, its tick box is disabled, and a line above the list counts them. Seeing that a repository is already there is more useful than wondering where it went. Those rows sort to the bottom, and the rest are alphabetical.
The filter box matches on the name and the remote. Select all ticks every row currently visible that is not already a project. Add selected then adds them, up to 100 in one press. Each folder is checked again in the app's main process before anything happens to it, so the list on screen is a suggestion rather than the authority, and anything that turns out not to be a git repository is skipped with a reason. Local Test's own installation folder is never offered.
Adding a scanned repository is a bookmark, not a copy. The project points at the folder where it already sits, and nothing inside it changes.
Looking up a GitHub account #
From a GitHub account takes a username and lists the repositories that account owns, most recently pushed first. All of these forms work:
What you can type in the account box.
| Form | Example |
| A username | octocat |
| A username with an at sign | @octocat |
| A bare address | github.com/octocat |
| A profile URL | https://github.com/octocat |
| The URL of one of their repositories | https://github.com/octocat/Hello-World |
The owner is taken from whichever of those you paste, and the rest is thrown away. Anything that is not a valid GitHub username is refused before a request is made, with That is not a GitHub username or profile URL. A well-formed name that GitHub does not know comes back as GitHub has no account by that name.
The listing is fetched 100 repositories at a time, for at most three pages, so 300 at most; beyond that it says Stopped after the first 300. Each row shows the name and description, the main language, the default branch, how long ago it was last pushed to, and the labels private, archived, and fork where they apply. As in the other pane, anything already added is greyed out, matched here by its owner/name.
Ticking rows and pressing Clone and add clones each one into your clone folder, one at a time with progress as it goes, up to 50 in one press. Every one becomes a project with pull-before-deploy switched on and its own free port. A repository that fails to clone is reported by name rather than silently dropped.
When GitHub rate limits the lookup #
Account lookups go to GitHub's public API. Without a token that API allows 60 requests an hour from one machine, and a single lookup spends one request per page of 100 repositories, so one to three of them. Roughly twenty lookups an hour, in other words, which is more than a picker needs, until it is not.
When the limit is reached the lookup stops with this, and no repository list appears:
GitHub is rate limiting this machine. Without a token the public API allows 60 requests an hour, and this one resets at 4:15 PM. Add a personal access token in Settings to raise the limit, or try again later.
The reset time comes from GitHub's own response and is shown in your local time. It appears only when GitHub sends it; without it the sentence ends after the limit. Nothing else in Local Test is affected, because nothing else in Local Test talks to that API: the scan pane, deploying, running, and editing all keep working. Being offline says so instead, with Could not reach github.com. Check your connection: everything else in Local Test works offline.
The optional personal access token #
A token does two things: it raises that rate limit, and it lets your private repositories appear in the account list. Neither the scan pane nor anything else in the app needs one, and there is no prompt asking you to add one.
- Create a fine-grained token. At github.com/settings/personal-access-tokens, choose Generate new token, give it a name you will recognize later and an expiry you are comfortable with.
- Choose which repositories it can see. Under Repository access, all of them, or only the ones you want listed.
- Give it one permission. Under Repository permissions, set Contents to Read-only, and leave everything else at No access. That is all Local Test does with it: read the repository list and clone. With a read-only token it cannot push, delete, or change anything.
- Paste it into Settings. The Personal access token field in app settings, then Save.
The token is checked for shape before it is stored: 20 to 255 characters, letters, digits, underscores, and hyphens only. Anything else is refused at the setting, with That does not look like a GitHub token. Paste the whole thing, with no spaces., rather than being kept and failing later against GitHub.
After saving, the field shows only a mask, the first seven characters and the last four. The token itself never comes back out of the app's main process, so the window that draws the interface cannot read it and neither can anything reading over your shoulder. It lives in config.json (see where your settings live), which is written so that only your user account can read it, and it is deliberately left out of Export, so a backup you email yourself does not carry it. It is never written to the activity log. It goes to api.github.com in a request header and nowhere else.
Leaving the field empty when you save means leave what is stored alone. Erasing it is the Remove button beside it, which says so. If GitHub stops accepting it, the app says GitHub rejected the stored token. Remove it in Settings, or create a new one.
Private repositories appear only when the token belongs to the account you are looking up. Local Test asks GitHub who the token belongs to, and uses the signed-in listing only when that login matches the name you typed; otherwise it uses the public one. The note above the list tells you which happened: Signed in with your token, so private repositories are included. or Public repositories only. Add a token in Settings to see private ones.
There is no Sign in with GitHub button. A GitHub OAuth app needs a client secret and a callback address to be redirected to, which means a service running somewhere on your behalf. Local Test has none: every GitHub request it makes goes from your machine straight to github.com, which is the reason it can promise that and the reason it cannot offer OAuth.
A token you create yourself is the trade. It is more work for you once, and in exchange it needs no service in the middle, it can be narrowed to a single read-only permission and a chosen set of repositories, it expires on a date you pick, and you can remove it here or revoke it at GitHub without touching anything else.
The command palette #
Cmd/Ctrl K opens a search box over the window. Type a few letters of a project or an action, deploy, stop, open in browser, copy URL, switch project, open in your editor, open a terminal, duplicate, add from GitHub, settings, tutorial, shortcuts, check for updates, then use the arrow keys and Enter. It is the fastest route to everything the app does.
Menu bar and tray #
Local Test puts an icon in the macOS menu bar or the Windows tray. Its menu lists your projects (up to twelve) with a filled dot and a port number when they are live, and clicking one starts or stops it without opening the main window. There is also Stop All, Show Local Test, and Quit. On macOS the Dock icon carries a badge with the number of projects currently running.
Device previews #
When a site is live, the Phone, Tablet, and Desktop buttons open it in a window sized to that class of device, 390 × 844, 820 × 1180, and 1280 × 860 points. It is a real window at a real size, which is a better check of a layout than dragging a corner.
Editors and the terminal #
Local Test looks for sixteen editors: Cursor, VS Code, Windsurf, Zed, Sublime Text, Android Studio, Neovim, and the JetBrains family (IntelliJ IDEA, PyCharm, WebStorm, GoLand, PhpStorm, RubyMine, CLion, Rider and RustRover). It finds them three ways: the command-line tool on your PATH, the application in /Applications or your own Applications folder, or a JetBrains Toolbox install. Toolbox matters more than it sounds: it puts each IDE under its own versioned folder somewhere else entirely, and IntelliJ alone ships under four different bundle names depending on edition and how you installed it.
Anything else, by hand. The chevron beside the button opens a menu with Choose an application at the bottom. Pick any application or executable and it joins the list. That covers an editor nobody has heard of, a wrapper script of your own, or a second copy of one that is already there.
Per project, if you want. The same menu has a Just this project tick. On, the choice applies to that project alone; off, it applies to every project that has not chosen its own. A Minecraft server whose folder is jars and configuration is the case this exists for: it is not a codebase and does not necessarily want the same editor as one.
An editor you add by hand is launched directly rather than through a command shell, on every platform. The built-in ones on Windows go through cmd.exe, because their command-line tools are .cmd shims, and a path you chose could contain a quotation mark. Spliced into that command line it would run whatever followed it, so custom editors do not go near it.
Editors are detected once when the app starts. If you install one while Local Test is open, restart it to see the button.
Deploy all, stop all, and the sidebar #
With two or more projects, Deploy all and Stop all appear at the top of the sidebar (Cmd/Ctrl Shift D and Cmd/Ctrl Shift S). Deploy all skips anything already running and reports how many started and how many failed. You can also drag rows to reorder the list and type in the search box to filter it.
The Activity tab #
Each project keeps a feed: deploy and stop events, every HTTP request with its status and timing, every line your command prints, live reloads, git results, and editor saves. Above it are counts of events, requests, and errors (any 4xx or 5xx), plus uptime and current state. You can filter the feed, copy it, export it to a file, or clear it.
The feed lives in memory, holds the last 300 lines per project, and is gone when you quit. Export it if you need to keep it.
Backup and moving machines #
Export in app settings writes your settings and project list to a JSON file. Import on another machine adds any project it does not already have, giving each a fresh port, skipping duplicates, and telling you how many of the folders in the file do not exist on this machine. Folders themselves are not copied, the file records where they were.
Your GitHub token, your AI provider API key, your license key, and any environment variables set on a project are all deliberately kept out of that file, because a backup travels: it gets emailed, synced, and copied to a stick. Each is quick to enter again on the other machine, and impossible to un-leak. Import brings across projects, not settings, so the other machine keeps its own.
The built-in tutorial #
The Help menu, and the command palette, open a short beginner walkthrough in its own window. It covers the same first steps as this page in fewer words.
Nothing, and there is no limit on anything. No account, no card, no trial clock, no expiry, and no upgrade prompt in the app. Deploying, running software, the editor, project search, every theme, the templates, git, the repository browser, the Minecraft installer, live reload, network sharing, device previews, backup and import, updates and AI mode are all ungated and unmetered, and you may keep as many projects as you like.
AI mode is the one feature that can cost money, and none of it is ours. It runs on your own provider key, billed by that provider to your own account, with Local Test taking no cut and adding no markup. Ollama runs on your own machine and needs no account and no key at all. The next section covers all of it.
There was a limit, and it is off #
This is worth explaining rather than quietly dropping, because older writing about Local Test describes it and because the code for it is still there to read.
Local Test Free used to keep five projects at a time, and a paid tier called VIP lifted that. VIP is not on sale, and a limit whose only way past is something nobody can buy is not a limit: it is a wall with no door in it. So the count is off for everybody. The app reads one switch for both halves of this, so the offer and the limit cannot end up disagreeing with each other: while VIP is not being sold, projects are unlimited, and the day it goes back on sale the limit comes back with it.
You can check this claim in the source, which every release publishes as an archive next to the installers. src/main/license.js holds one function, projectLimit(), and every route in the app that can create a project asks it first. There is no second door.
Nothing about a project has ever been metered: not deploys, ports, requests, uptime, or file sizes. The old limit was on how many projects the app remembered, and removing a project has never touched the folder on your disk. A project is a bookmark to a folder.
If the limit ever comes back #
The rule that was written for it is still in the code, and it is worth knowing because it is the answer to the obvious worry.
Nothing would be taken away. The first time a profile opened a build that had the limit in force, Local Test stamped how many projects were there at that moment, once, in a way that cannot run a second time. If that number is above five, that is the limit for that install. An install with twenty projects in it would go on holding all twenty, indefinitely, and the limit would show up only at the twenty-first, saying exactly what it was doing:
Local Test Free keeps 5 projects. You had 7 before the limit existed, so all 7 are yours to keep. This is the one place it stops: adding a new one.
Import has always been treated separately: a backup can easily hold more projects than a limit allows, and refusing a whole file over that would be useless, so it imports as many as there is room for and says in the confirmation how many were left behind.
If VIP is ever sold again, it will arrive in a release, and the changelog is where every release is written down. Nothing on this site or in the app will start asking for a card without a version number attached to it.
Licence keys #
Keys are not being issued, and the app does not ask for one. Anybody who already holds one still redeems it, and it still does everything it ever did, so the box is still there in app settings and the reading of a key is unchanged.
A key looks like LT1-XXXXX-XXXXX-XXXXX-XXXXX: the prefix LT1, then four groups of five characters. The alphabet is Crockford base32, which is the ten digits plus the letters with I, L, O, and U left out, those being the ones people misread when copying a key off a screen. It is read forgivingly for the same reason: case is ignored, spaces are stripped, the dashes are put back if they were lost or the key wrapped across two lines, and I and L are read as 1 and O as 0. The L in the LT1 prefix is left alone, being a real letter.
Once accepted, the panel shows the tier, the date it was unlocked, and the key masked down to its first group and its last, LT1-XXXXX-•••••-•••••-XXXXX. That is enough to tell two keys apart and useless to anybody who reads it. The key itself stays on this machine and is never handed back out to the interface, and Remove key puts the install back on Free after asking. It never removes a project.
Keys are checked on this machine. Unlocking sends nothing anywhere, and nothing about your projects leaves the computer at any point.
Every project has an AI tab: a set of premade prompts that already carry what Local Test knows about the project you have open. It is not a tier feature and it is not metered by us. What it needs is somewhere to send a question, which means either your own key with a hosted provider or a copy of Ollama running on this machine. With neither, the tab says so and links to the setting.
Local Test charges nothing for AI mode and takes no cut of what you spend. There is no account with us, no key of ours in the path, and no service of ours in the middle. Requests go from your machine to the provider you picked, carrying your key, and that provider bills your account. If you use Ollama, nothing is billed by anyone and nothing leaves the computer.
The three providers #
Pick one under AI provider in app settings. The choice is app-wide, not per project, and it decides which key field you get and where requests go.
Where each provider sends requests, and what it needs from you.
| Provider | Requests go to | Needs |
| Anthropic | api.anthropic.com | An account and an API key beginning sk-ant-, with credit on it. |
| OpenAI | api.openai.com | An account and an API key beginning sk-, with credit on it. |
| Ollama, on this machine | 127.0.0.1:11434 | Ollama installed and running, with at least one model pulled. No account, no key, no card. |
Ollama is the only one allowed to be plain HTTP, and only on loopback. The two hosted providers are HTTPS and nothing else.
Anthropic
- Make an account at console.anthropic.com.
- Add credit under Billing. Five or ten dollars goes a long way at the volumes this app sends.
- Open console.anthropic.com/settings/keys and press Create Key.
- Copy it the moment it appears. It begins
sk-ant- and Anthropic will not show it to you again.
- Paste it into API key in Local Test's app settings and press Save.
If it leaks. Anyone holding it can spend your Anthropic credit until it is gone, and can read the usage on the key. It cannot change your password, add a payment method, or touch anything else in the account. Revoke it on the same keys page: find the key by its name, choose Delete, and it stops working within seconds. Then create a new one and paste that in. Nothing else in Local Test is affected while you have no working key.
Prices are dollars per million tokens, read and written separately, and are what this build prices its cost line against.
The Anthropic models Local Test offers. Claude Opus 5 is the default.
| Model | Read, per million | Written, per million |
| Claude Opus 5 (default) | $5 | $25 |
| Claude Sonnet 5 | $2 | $10 |
| Claude Fable 5 | $10 | $50 |
| Claude Haiku 4.5 | $1 | $5 |
OpenAI
- Make an account at platform.openai.com.
- Add credit under Billing. An API key with no credit behind it is refused on the first request, and the message says so.
- Open platform.openai.com/api-keys and press Create new secret key.
- Copy it the moment it appears. It begins
sk- and OpenAI will not show it to you again.
- Paste it into API key in Local Test's app settings and press Save.
If it leaks. The same shape of answer: whoever has it can spend your OpenAI credit and read the key's usage, and cannot do anything else to the account. Revoke it on the same API keys page with Delete, or, if you gave it a project scope, from that project's key list. A deleted key stops working at once.
The OpenAI models Local Test offers. GPT-4o is the default.
| Model | Read, per million | Written, per million |
| GPT-4o (default) | $2.50 | $10 |
| GPT-4o mini | $0.15 | $0.60 |
| GPT-4.1 | $2 | $8 |
| GPT-4.1 mini | $0.40 | $1.60 |
| GPT-5 | $1.25 | $10 |
| GPT-5 mini | $0.25 | $2 |
The cheap models are on the list on purpose. Someone who chose OpenAI over Anthropic often did it on price, and offering only the expensive one hides that.
Ollama, on this machine
This is the answer for anyone who cannot send code to a third party, and it is worth stating without hedging: Ollama needs no key, no account and no card, and nothing you send it leaves this computer. The model runs on your own hardware, the request travels to 127.0.0.1:11434, and there is no bill because there is no other party. If your employer's rules say source code does not go to outside services, this is the configuration that complies with that, and it is the same promise the rest of Local Test makes.
- Install Ollama from ollama.com/download.
- Pull a model. In a terminal, once:
ollama pull llama3.1. Anything else you pull works too.
- Leave Ollama running. It listens on
127.0.0.1:11434.
- Pick Ollama, on this machine in Local Test's app settings. The key field disappears, because there is nothing to put in it.
The model list here is not built in, because which models exist on a machine is a fact about that machine rather than about this build. Local Test asks Ollama what has been pulled and lists that, and Refresh asks again. If Ollama is not running you get the reason rather than an empty menu:
- Ollama is running on this machine, with 3 models pulled.
- Ollama is running on this machine, but it has nothing pulled yet. Pull a model first, for example "ollama pull llama3.1".
- Nothing is answering at 127.0.0.1:11434, so Ollama does not appear to be running.
- Ollama did not answer within 1.5 seconds, so Local Test cannot tell whether it is running.
- Something is answering at 127.0.0.1:11434, but it did not reply the way Ollama does, so Local Test cannot tell what is there.
The last one exists because saying "not running" about something that is plainly listening on the port would send you off to start a program that is already started.
The trade is quality and speed. A model small enough to run on a laptop is not the equal of a hosted one, and the first answer after a cold start is slow while the weights load. Everything else about the panel behaves identically.
A hosted provider's list is built in rather than fetched. The only request that would prove a key works is a request you are billed for, and spending your money to light up a status line without being asked is not something this app does. So the panel says what it honestly knows: a key is saved, and whether it still works is a thing only sending will tell you.
What a request actually costs, worked through #
Billing is by the token, which is about three quarters of a word, or roughly four characters. Both what the model reads and what it writes are counted, and writing costs several times more than reading. Two bounds are worth knowing, because between them sits everything this app sends.
A short exchange. Say a thousand tokens go up (a question plus a file list) and five hundred come back. On the default Claude Opus 5, at $5 per million read and $25 per million written:
1,000 read x $5 / 1,000,000 = $0.005
500 written x $25 / 1,000,000 = $0.0125
--------
$0.0175 about 1.8 cents
The largest exchange the app permits. A file goes in at no more than 60,000 characters, which is roughly 15,000 tokens, and a reply is capped at 4,000 tokens. Nothing this app sends can be bigger than that. Same model:
15,000 read x $5 / 1,000,000 = $0.075
4,000 written x $25 / 1,000,000 = $0.10
--------
$0.175 about 17.5 cents
So the honest sentence is: a short question costs a cent or two, and the most expensive thing you can do here costs under twenty cents. Those are the ceiling and the floor on the most expensive model offered. The same two bounds on the others:
A 1,000-in, 500-out exchange, and the 15,000-in, 4,000-out maximum, per model.
| Model | Short exchange | The app's maximum |
| Claude Opus 5 | 1.8 cents | 17.5 cents |
| Claude Sonnet 5 | 0.7 cents | 7 cents |
| Claude Fable 5 | 3.5 cents | 35 cents |
| Claude Haiku 4.5 | 0.4 cents | 3.5 cents |
| GPT-4o | 0.8 cents | 7.8 cents |
| GPT-4o mini | 0.045 cents | 0.465 cents |
| GPT-4.1 | 0.6 cents | 6.2 cents |
| GPT-4.1 mini | 0.12 cents | 1.24 cents |
| GPT-5 | 0.6 cents | 5.9 cents |
| GPT-5 mini | 0.13 cents | 1.18 cents |
| Ollama, any model | nothing | nothing |
You do not have to take any of it on trust. The exact figure for each answer is printed next to it once it comes back, priced against the model your request was actually sent with. Not the provider's default, and not whatever dated snapshot id the reply happens to name back: the models on one provider's list differ by a factor of ten, so pricing a Haiku answer at the Opus rate would put a number on screen that is wrong by an order of magnitude and looks just as authoritative. Under a cent it reads less than a cent rather than a row of zeroes.
Two caps bound all of this and neither can be raised from the interface: a reply is at most 4,000 tokens, and a request that has not answered within two minutes is given up on.
The premade prompts, and what each one reads #
There are nine. Picking one fills the box with its wording plus the context it names, gathered from the project you have selected, so there is nothing to paste by hand.
Every premade prompt, and what it puts in the box with it.
| Prompt | What it gathers |
| Explain this error | The recent activity log. |
| Write a README | The file list. |
| Suggest what to build next | The file list. |
| Review this file | The file open in the editor. |
| Generate a starter page | Nothing. You describe the page you want, and the prompt stops mid-sentence with the cursor where the description goes. |
| Explain this project to me | The file list. |
| Why is my page not loading right? | The file list and the activity log. |
| Check this file for accessibility | The file open in the editor. |
| Write a commit message | The changed files. |
Those four kinds of context are each bounded, and this is exactly what they contain:
- The file list is names and byte sizes, three folders deep, up to 250 entries, ending in a note when it had to stop there. Anything beginning with a dot is left out, and so are
.git, node_modules, dist, build, _site, out, .next, coverage, vendor, .cache, .venv, and __pycache__. File contents are not in it.
- The activity log is the last 120 lines of the feed for that project, then trimmed to the last 6,000 characters.
- The file open in the editor is that one file, up to its first 60,000 characters. Its size is measured before it is read, so pointing the Files tab at a log hundreds of megabytes long does not pull it into memory: anything over 1.5 MB, and anything that is not text, comes back as a note saying so instead. If no file is open, the prompt says that rather than quietly sending the wrong one.
- The changed files is the branch name and the list of changed file names with their state. The diff itself is not sent. If the folder is not a git repository, the prompt says that instead.
Nothing is sent until you press Send #
This is the part worth understanding, because it is the whole design of the panel.
Picking a prompt sends nothing. The app gathers the context that prompt names, hands the whole thing back to the window, and drops it into a large editable box together with the prompt wording. What is in that box is exactly what will be sent, and you can change every word of it. If there is a file, a path, or a log line in there you would rather not hand over, delete it and send the rest. The character count under the box tells you how much you are about to send, and Send takes what is on screen at that moment, not what was gathered a minute ago.
From there it goes to the provider you chose and nowhere else: over HTTPS to api.anthropic.com or api.openai.com, or over loopback to 127.0.0.1:11434, which is not a network request at all. Nothing about the request is written to the activity log, deliberately: the prompt can hold the contents of your files, and the key is a secret. A short standing instruction travels with every request, telling the model you are building a site or a small app locally in Local Test and asking for plain language and short code. It never changes, says nothing about you, and is printed in full in the panel's own explainer, so nothing goes out that you have not been shown.
Where the key lives #
An API key is a long password belonging to your account. Local Test treats it the way a password should be treated, and each of these is a separate promise:
- On this machine only. It is written to
config.json (see where your settings live), which is created readable by your user account and nobody else. It is never sent to us, because there is no us to send it to.
- Masked after you save it. The field then shows the first eleven characters and the last four, with dots between. That is enough to tell two keys apart and useless to anyone reading over your shoulder.
- Never handed back to the interface. The key is held in the app's main process, put into a request header there, and that is the only place it goes. The window that draws the interface has never had it, so it cannot appear in a devtools console or in a rendered page.
- Out of backups. An exported backup deliberately omits the API key, the GitHub token, the license key, and every project's environment variables. A backup you carry to another machine does not carry your key with it, and you paste it in again there.
- Never logged. Not in the activity feed, not in an error message, not in a crash. No code path prints it.
- Sent only to the provider it belongs to. Switching provider does not resend anything anywhere; it changes where the next request goes.
Something that does not look like a key is refused at the setting rather than stored, with That does not look like an API key. Paste the whole thing, with no spaces. The shape check is deliberately loose, since only the provider can really say, but it does catch the common wrecker: an invisible character, a zero-width or non-breaking space, riding along from a copy and paste. Caught at the field, that is a sentence while you are looking at it. Not caught, it is a mystery failure at send time.
What happens when it does not work #
Every one of these leaves your typing exactly as you left it. A failed request never clears the box, and the ones worth retrying come back with a Try again button rather than making you write the prompt over.
Every failure the panel distinguishes, and what it says.
| What happened | What you get |
| No key saved | Nothing is sent. The tab says Add your Anthropic API key in Settings first. AI mode uses your own account, not Local Test's. and links to the setting. Picking Ollama instead needs no key at all. |
| The key is rejected | Anthropic rejected that API key. Check it was copied whole, and that it has not been revoked, in your account at console.anthropic.com. The provider's own name and console address are in the sentence, so the OpenAI version names OpenAI and platform.openai.com. |
| No credit | Your Anthropic account has no credit left. Top it up at console.anthropic.com and try again. Checked ahead of the generic refusal, because an empty account often arrives as a plain "bad request" and that would send you hunting for a bug instead of topping up. |
| Rate limited | Anthropic is rate limiting your account. Wait a minute and send it again. If it keeps happening, check your usage limits at console.anthropic.com. |
| The provider is overloaded | Anthropic is overloaded right now. Try again in a moment. A genuine fault on their side reads had a problem on their side (HTTP 500) instead. |
| No network | Could not reach api.anthropic.com. You are offline, or something is blocking it. Everything else in Local Test works without a connection. |
| Ollama is not running | Nothing is answering at 127.0.0.1:11434. Ollama does not appear to be running: start it, then send this again. Everything else in Local Test works whether it is running or not. |
| Ollama has not pulled that model | Ollama, on this machine does not have that model yet. Pull it first, for example "ollama pull llama3.1", then send it again. |
| The key holds an unsendable character | Your API key contains a character that cannot be sent, usually an invisible one picked up when it was copied. Delete the key and retype it. |
| Too slow | After two minutes: The request took too long and was given up on. Try a shorter prompt. |
| The connection drops mid-reply | The connection dropped part way through the reply. Nothing was charged for a reply you did not get. Try again. |
| A model that provider does not have | The prompt is not thrown away over it. The provider's default answers instead, and a note beside the reply says which model really answered and why. That note appears whatever else happened to the request. |
| The model declines | That one was declined. Try rewording it. |
Nothing in that list affects any other part of Local Test. Deploying, running software, the editor, git, and everything else keep working with a dead key, an empty balance, or no connection at all.
The same list is in the app: open the command palette and choose Keyboard shortcuts.
| Action | macOS | Windows |
| Command palette | Cmd K | Ctrl K |
| Add a project | Cmd O | Ctrl O |
| New from template | Cmd N | Ctrl N |
| Deploy or stop the selected project | Cmd D | Ctrl D |
| Deploy all | Cmd Shift D | Ctrl Shift D |
| Stop all | Cmd Shift S | Ctrl Shift S |
| Search project files | Cmd F | Ctrl F |
| Save the open file | Cmd S | Ctrl S |
| Find and replace in the editor | Cmd Alt F | Ctrl Alt F |
| Bold, italic, link in the editor | Cmd B / I / K | Ctrl B / I / K |
| Overview, Files, Activity, Settings | Cmd 1 to 4 | Ctrl 1 to 4 |
| App settings | Cmd , | gear icon |
| Indent in the editor (two spaces) | Tab | Tab |
| Close a dialog or the palette | Esc | Esc |
Two rough edges worth knowing. Cmd , lives in the macOS application menu, so on Windows the gear is the way in. And in the Markdown editor Cmd/Ctrl K inserts the link markup and opens the command palette on top of it, press Esc to dismiss the palette and the link is waiting underneath.
Updating and uninstalling # changelog
Checking #
A few seconds after launch, Local Test checks whether a newer version exists, only that, and only when Check for updates when the app opens is on. Nothing appears unless there is one. You can check at any time by clicking the version badge in the bottom-left corner, or through the command palette.
What the check can tell you #
There are four answers, and the version chip beside the app's name shows which one you got. Knowing they exist is most of understanding the feature, because two of them are about the update server rather than about your copy.
The four answers an update check can give.
| The chip says | What it means |
| up to date | Nothing published is newer than what you are running. The commonest answer and the least interesting one. |
| 1.62.0 ready | Something newer exists, and an Install button appears. Nothing downloads until you press it. |
| feed is behind | The update server publishes a version older than the one you are running. Not an error, and not a sign you are out of date: the thing being asked has fallen behind the thing asking. This happens if you built the app yourself from source, and it happens if a release went out and the published manifest was not updated with it. Local Test says so rather than telling you that you are current, because current would be a guess it cannot make. |
| check failed | The request did not complete: no network, or the server did not answer. The reason goes in the problem log whether or not you were watching. |
If you are ever unsure what is actually published, the manifest the app reads is a plain file you can open yourself: /downloads/version.json. What it says under mac and windows is exactly what the app compares itself against.
Installing #
Choosing Install downloads the new version and, on macOS, unpacks it, moves the current app aside as a backup, puts the new one in its place, and reopens it, restoring the backup if the move fails. On Windows it downloads the installer and hands off to it. If a version has no build available for your platform, the download page opens in your browser instead.
Because the app is unsigned, this is a visible swap rather than a silent background update. Running from the source archive there is no installed app to replace, so Local Test opens the download page and leaves your folder alone; update it by downloading a fresh source archive and running npm install in it again.
Uninstalling #
App settings has an Uninstall button. After a confirmation it stops everything that is running, deletes the app's settings and project list, moves the app itself to the Trash, and quits. Your project folders are never touched, Local Test only ever held a reference to them.
On Windows, uninstall through Add or remove programs as usual; the installer registers an uninstaller that also removes the app's data.
Where your settings live #
Everything Local Test remembers is one JSON file called config.json, in the per-user application data folder.
Where the config file lives.
| Platform | Location |
| macOS | ~/Library/Application Support/Local Test/config.json |
| Windows | %APPDATA%\Local Test\config.json |
| Linux | ~/.config/Local Test/config.json |
Inside it are the app settings and the project list. Each project records where it is, which port it uses, and whichever of its options you changed.
{
"schemaVersion": 1,
"settings": {
"defaultPort": 8000,
"autoOpenBrowser": true,
"allowLan": false,
"extraHosts": "",
"theme": "default",
"fontSize": 13,
"githubDir": "",
"checkUpdatesOnLaunch": true,
"aiProvider": "anthropic",
"aiModel": ""
},
"projects": [
{
"id": "0c9f2f4e-…",
"name": "my-portfolio",
"path": "Sites/my-portfolio",
"port": 8000,
"createdAt": "2026-08-26T21:00:00.000Z",
"liveReload": true,
"spaFallback": false,
"serveAllFileTypesOnNetwork": false,
"serveBeyondLocalNetwork": false,
"serveDir": ""
}
]
}
Three things are stored here and shown above as absent, because they are secrets: your GitHub token, your licence key, and your AI provider API key. They are never handed back to the interface or written into an export.
They are also not in this file in the form you typed them. Each one is encrypted first, using your operating system's own store: the Keychain on macOS, DPAPI on Windows. What lands in config.json looks like encrypted:v1: and then base64. The file's permissions already stopped another account on the machine reading it, and stopped nothing else: a backup, a folder synced to a cloud drive, or a copied disk image handed all three to whoever held it. Now the file on its own is not enough.
The consequence worth knowing before it surprises you: copying your profile to another computer carries everything across except those three. That machine's keychain cannot open what yours sealed. Local Test says which keys it could not read and asks you to enter them again, rather than showing empty boxes that look like it lost them. Nothing is lost on the machine they came from.
On a system with no keyring for the app to reach, a key is stored the way it was stored before, in a file only your account can read, and the app says that instead of implying protection it does not have. macOS and Windows both have one.
The file is written atomically (to a temporary file that is then renamed) so a crash mid-save cannot leave you with half a config. If it parses as nothing at all it is renamed to config.json.bak and a fresh one is started, rather than the app refusing to open. If it exists but cannot be read, which is a different thing (a permissions problem, a disk going bad, a synced folder mid-sync), the app runs on defaults and refuses to save over it, saying so, because that is the failure that used to quietly destroy a settings file that was still perfectly intact. Settings from the app's earlier name, Test Run, are carried over automatically the first time.
Linux is expected to work, since the app uses only standard Electron and Node APIs, but it is untested and unsupported.
Privacy and network behavior #
Stated precisely, so you know exactly what the app does rather than having to take it on trust.
What binds to what #
A deployed project listens on 127.0.0.1 at its port, reachable from this machine only. With network sharing on it listens on :: instead, which is IPv6 and IPv4 together, and the app shows you every address to use. That is the only way anything else on your network can reach a site, it is off until you turn it on, and turning it off takes running sites back off the network straight away.
Listening on both families is what makes sharing work on a network that hands out no IPv4, and it is also why the address a request came from is checked as well as the name it used. IPv6 has no NAT in front of it, so a machine with a routable IPv6 address can be addressed from the internet directly. A request from outside your own local network is refused before anything is read unless that project's own switch says otherwise, so turning sharing on puts a site on your network rather than on the internet.
Every place the app touches the network #
- The update check. One HTTPS request asking whether a newer version exists. It happens a few seconds after launch when the setting is on, and whenever you ask for it. It sends nothing but the request itself.
- Installing an update. Downloading that version's file, and only after you choose Install.
- Git. Sync now, pull-before-deploy, Add from GitHub, and Push run the git program on your machine against the remote you configured. Local Test does not speak to GitHub itself here; git does, with your existing credentials.
- Opening a link. The live URL, the download page, and links inside rendered Markdown are handed to your default browser.
- Looking up a GitHub account. Only when you type a name into the account pane of the repository browser, and only to
api.github.com. It asks for that account's repository list and sends nothing about you, unless you have stored a token, which travels in the request header so that your private repositories are included. Scanning a folder of clones is entirely local and makes no request at all.
- Sending an AI prompt. Only when you press Send in the AI tab, and only to the provider you picked:
api.anthropic.com, api.openai.com, or 127.0.0.1:11434 for Ollama, which is this machine talking to itself and leaves no network at all. What goes is the text sitting in the box in front of you, plus your own API key in the request header where there is one. Nothing is gathered in the background and nothing is sent while you are still reading it.
- Asking Ollama what it has. When the AI provider is Ollama, one request to
127.0.0.1:11434 to list the models pulled on this machine, and another when you press Refresh. Both stay on the machine. A hosted provider is never asked this, because the answer is built in and asking would cost a round trip before you had pressed anything.
That is the complete list. The last three happen only on a deliberate press, and only if you have gone looking for them. There is no telemetry, no analytics, no crash reporting, no account, and no check-in of any kind. Nothing about your projects, files, paths, or settings is transmitted anywhere on its own.
Inside the app #
- The window that draws the interface runs with context isolation on and Node disabled, under a content security policy that permits only the app's own files and inline images. Every privileged action (reading a file, starting a server, running git) goes through a short list of checked handlers.
- File access of every kind is confined to the project folder, checked both literally and after resolving symbolic links.
- The git commands behind the status, commit, and push buttons run with the repository's own hooks, pager, editor, and credential helpers disabled, so opening someone else's clone cannot make your git run their code.
- Environment variables you set are stored as plain text in
config.json.
- The GitHub token, the license key, and your AI provider API key are held in that same file, which is written so that only your user account can read it. None of the three is ever handed back to the window that draws the interface, which sees a mask and nothing more, and none of the three is ever written to the activity log. All three are left out of an exported backup, along with any environment variables you have set on a project, since that is where a database password usually ends up.
- A config file that cannot be read at startup is never written over. An unreadable file used to look exactly like a fresh install, and the next save then replaced the real one with defaults. Since exports deliberately omit the three secrets above, there was nothing to restore from. Now the app says why it will not save, and your settings stay on disk.
- Local Test refuses to serve or run its own installation folder, and drops such a project if one is ever in the list.
What Local Test does not do #
- No build step. It serves files as they are on disk. Run your build yourself, or make it the project's run command.
- No HTTPS, no custom domains, no proxying. Plain HTTP on localhost, or on your own network when you turn sharing on.
- Not a way to put a site on the internet. No accounts, no passwords, no rate limiting, no TLS. Network sharing reaches the devices on your own network, and the two switches that widen that are documented with the reasons not to use them. Publish somewhere built for publishing.
- No backend.
GET and HEAD only; a form that posts has nothing to post to. If you need one, run it as a command project.
- No compression. Files are sent as they are.
- The builds are unsigned, hence the macOS first-launch step, the Windows SmartScreen warning, and updates that swap the app rather than installing quietly. Signing on either platform needs a paid certificate.
- No sign in with GitHub. The account lookup in the repository browser uses a token you create yourself, or nothing at all. OAuth would need a service in the middle, and there is not one.
- Linux is untested. It should work, since the app uses only standard Electron and Node APIs, but nobody has verified it.
- Live reload needs recursive folder watching, which is unavailable on some systems. Serving still works there; automatic reloads do not.
- Find and replace is literal and case-sensitive, no regular expressions.
- Project search stops at 200 matches and skips build folders, dot-files, files over 1 MB, and non-text types.
- Run commands are not told which port to use. The port on the project is Local Test's idea of where your program will listen, not an instruction to it: set the same number in your own configuration, or in an environment variable.
- Not a Minecraft host. A Minecraft server it sets up runs on your machine, for you and for the devices on your own network. Somebody on the other side of the internet cannot reach it unless you arrange that yourself, and Local Test does not arrange it for you.
Troubleshooting #
Each entry starts with the message as the app prints it, because that is what you are most likely to be searching for.
Deploying #
Port 8000 is already in use.
Something else already holds that door, another project of yours, or an unrelated program. The error comes with a button reading Use 8001 & Deploy, which moves the project to the next free port and deploys in one click; that is almost always the right answer. If you specifically need that port, quit whatever is holding it first.
Port 80 needs elevated permissions, use a port above 1024.
Ports at 1024 and below are reserved for the operating system and need administrator rights. Local Test will not ask for them. Pick something higher, 8000, 3000, 5173, anything you like above 1024.
The folder no longer exists:
Followed by the path the project points at. You moved, renamed, or deleted the folder; the project is still pointing at where it used to be. Remove the project from Local Test and add it again from its new location. Removing never deletes anything on disk.
The configured serve folder does not exist: dist
The project's Serve folder setting names a subfolder that is not there, usually because the build has not been run yet, or the folder is called something else. Run the build, correct the name, or clear the field to go back to automatic detection.
That folder is Local Test itself, it cannot deploy its own installation.
You picked the application, or the folder containing it. Pick your project folder instead. (Running the Local Test source code as a project is fine, that is a different folder from the installed app.)
Running a command #
Stopped: exited with code 127
127 is the shell's way of saying it could not find the command. Check the spelling in the Command field, and check that the tool is installed, try the same command in a real terminal in the same folder. If it works there but not here, the app is not seeing the same search path; restarting Local Test makes it re-read your shell's path. As a last resort, use the tool's full path in the Command field.
Stopped: exited with code 1
Your program started and then failed on its own terms. The lines directly above this one in the Activity tab are its own output, that is where the actual reason is. The same applies to any other non-zero code.
Stopped: killed (SIGTERM)
Normal. That is what Stop, Stop all, and quitting the app look like from the program's side.
The command runs but the address shows nothing
Local Test never tells your program which port to use. The address on the Overview tab is built from the project's Port field, so if your program listens on 3000 and the project says 8000, the link goes nowhere. Read the port out of your program's own output in Activity and set the project's port to match, or pass PORT to it through the environment variables box.
Git and GitHub #
git is not installed on this machine.
Local Test uses the git you have rather than bundling its own. Install git, on macOS, running git --version in a terminal offers to install the developer tools; on Windows, git-scm.com, then restart Local Test.
Couldn’t reach owner/name, the repo may be private or misspelled. Sign in with git (or GitHub Desktop) first, or check the name.
Either the name is wrong or the repository is private and this machine is not signed in. Local Test deliberately fails fast here instead of hanging on a password prompt you cannot see. Check the spelling first; if it is private, sign in with the git command line or GitHub Desktop once, then try again.
Folder has files but is not a git repository, sync would overwrite it.
Cloning into a folder that already has content would destroy that content, so it is refused. Point the project at an empty folder, or run git init and add the remote yourself in the existing one.
Nothing to commit, no changes.
Git sees no difference between your files and the last commit. If you expected changes, check that you saved the file, and that it is not excluded by .gitignore.
Git needs your name and email first: git config --global user.email "you@example.com"
Git stamps every commit with an author and has never been told who you are. Run that command in a terminal, along with git config --global user.name "Your Name", then commit again.
Git could not authenticate. Sign in with the git CLI or GitHub Desktop, then try again.
Push needs credentials Local Test does not have and will not ask for. Set them up once with the git command line or GitHub Desktop and every later push works.
GitHub is rate limiting this machine. Without a token the public API allows 60 requests an hour, and this one resets at 4:15 PM. Add a personal access token in Settings to raise the limit, or try again later.
Account lookups in the repository browser use GitHub's public API, which allows 60 requests an hour from one machine when nobody is signed in, and each lookup spends one request per page of 100 repositories. Wait for the reset time (GitHub's own, shown in your local clock) or add a personal access token, which raises the limit. Nothing else in the app is held up by this: scanning a folder of clones never touches the network, and neither does anything else you were doing.
GitHub rejected the stored token. Remove it in Settings, or create a new one.
GitHub answered with a 401, which means the token has expired, been revoked, or was only ever partly pasted. Tokens have an expiry date chosen when they were made, so this turns up eventually for everyone who sets one. Press Remove beside the field and paste a new one; the lookup works without a token in the meantime, on public repositories only.
GitHub has no account by that name.
The name is a valid GitHub username in shape but no such account exists, so check the spelling. The nearby That is not a GitHub username or profile URL. is a different thing: it means what you typed could not be a username at all, and it is refused before any request is made. A username, a profile URL, or the URL of one of their repositories all work.
That does not look like a GitHub token. Paste the whole thing, with no spaces.
The token is checked for shape before it is stored: 20 to 255 characters, letters, digits, underscores, and hyphens only. This almost always means the copy caught a trailing space or a line break, or stopped short of the end. Copy it again from GitHub in one go. Local Test refuses it here rather than storing something that would fail later against GitHub for reasons that would be harder to read.
Pages in the browser #
404, /about doesn’t exist in this project.
Nothing matched that path in the folder being served. Three things to check: whether the file is really there under that exact name (capitalization counts), whether the folder being served is the one you think (a build folder may have been picked automatically, which the live block tells you) and, for a client-side router, whether SPA fallback is on. Put a 404.html at the root of the served folder and it replaces this page, exactly like GitHub Pages.
403, That path is outside the project folder.
The request resolved to somewhere outside the served folder, either through ../ in a path or through a symbolic link pointing elsewhere on your disk. This is the path guard doing its job; move whatever you are trying to serve inside the project folder.
405, Only GET and HEAD are supported.
Something tried to POST, PUT, or DELETE. The static server only reads files; it has no backend. If your page needs one, run the real thing as a command project instead.
The page did not update after I saved
Live reload is on by default, so a save should refresh the tab. If it did not: check that live reload is still ticked in the project's settings; check that the file you edited is inside the folder actually being served, not beside it; and reload once by hand, the injected reload script only reaches pages loaded after the deploy. On systems without recursive folder watching, reloads never fire at all and a manual refresh is the workaround.
I see a file list instead of my site
There is no index.html at the root of the served folder. Add one, point the Serve folder setting at the folder that has one, or turn directory listings off if you would rather have a 404. Another device on the network never sees the list at all, it gets the 404.
The network address does not load on my phone
Work down this list, most common first. Is the phone on the same Wi-Fi, rather than the guest network or cellular data? Is Allow devices on my network (LAN) actually ticked and saved? Is the project deployed right now? Did you include the port number? Did you allow the firewall prompt? The live block lists every address the machine answers on, best guess first, so try the others: a VPN, Docker, or a VM gives your machine extra addresses and only one of them is your Wi-Fi. If an IPv6 address is the only thing listed, the network is handing out no IPv4 and the phone has to be on that same network to use it. Failing all that, some networks, guest and public Wi-Fi in particular, deliberately stop devices talking to each other at all, and nothing on this machine can change that.
403, That host name is not served here.
The site was reached under a name the server is not reachable under. Use one of the addresses the app printed in the live block, or the machine's own .local name. If you reach the site through a name of your own, a hosts file entry or a tunnel, add it under other hostnames this machine answers to and Save. The same message is what a page on the internet gets when it tries to point a domain of its own at your machine, which is the reason the check exists and why it cannot be turned off.
403, Not shared on the network.
Another device asked for a file that is not one of the types a web page is made of: a database, an archive, a dump, a spreadsheet, a file with no extension. On this machine the same URL still works, and the page gives you the localhost address to use. If the file really is part of the site, either give it a web extension or turn on Serve every file type on the network in that project's settings, which hands other devices the whole folder.
403, Not shared beyond your network.
The request came from an address that is not on your own local network, and it was refused before anything was read. This is normally either someone scanning your machine's public IPv6 address, or you testing from a phone on cellular data rather than on the Wi-Fi. Put the phone on the same network. The per-project switch Answer requests from outside your own network lifts the restriction, and this is what to read first if you are about to use it.
Another device sees a 404 where I see a file list
Working as intended. Folder listings are generated for this machine and never for another device, whatever the project's Directory listings setting says, because a listing is a convenience for you and an inventory of your project for everyone else on the Wi-Fi. Put an index.html in the folder.
Projects, licence keys, and AI mode #
Local Test Free keeps 5 projects, and you have 5. Remove one you have finished with.
You should not be able to see this one. The project limit is not in force while VIP is not on sale, so this sentence has nothing to trigger it and adding a project is never refused for being one too many. It is documented here because older versions did print it, and because a build that starts printing it again would be a bug worth telling me about. If you are looking at it in a current version, the number of projects you have is not the reason.
That key is not the right shape. It looks like LT1-XXXXX-XXXXX-XXXXX-XXXXX, in five groups of five.
The prefix was right but the rest was not. Case, spaces, and missing dashes are all forgiven, and the letters that get misread are corrected for you, so this normally means a group is short or a character was dropped. Copy the key again in one piece. The related Local Test keys start with LT1. Check you copied the whole key. means the copy began part way through.
Add your Anthropic API key in Settings first. AI mode uses your own account, not Local Test's.
There is nothing to send the request with. AI mode is not gated by a tier: what it needs is somewhere to send a question. Either paste a key for the provider you picked, or switch the provider to Ollama, on this machine, which needs no key and no account at all. The sentence names whichever provider is selected, so on OpenAI it asks for an OpenAI API key.
Anthropic rejected that API key. Check it was copied whole, and that it has not been revoked, in your account at console.anthropic.com.
The key stored here is not one the provider will accept: mistyped, partly pasted, or revoked since. Create a fresh one (console.anthropic.com/settings/keys for Anthropic, platform.openai.com/api-keys for OpenAI) and paste it into Settings, replacing what is there. Your prompt is untouched, so you can send it again as soon as the key is fixed.
Your Anthropic account has no credit left. Top it up at console.anthropic.com and try again.
AI mode bills your own provider account, so an empty balance stops it. Adding credit is the whole fix, and nothing else in Local Test is affected in the meantime. See what a request actually costs for the arithmetic, and note that Ollama costs nothing because it never leaves your machine.
Could not reach api.anthropic.com. You are offline, or something is blocking it. Everything else in Local Test works without a connection.
AI mode against a hosted provider is one of only three parts of the app that need a connection, the others being the GitHub lookups and the update check. Your prompt is kept exactly as you wrote it and the error comes with a Try again button, so reconnecting and pressing it again is the whole recovery. Ollama does not need a connection at any point, which is one reason to keep it configured.
Nothing is answering at 127.0.0.1:11434. Ollama does not appear to be running: start it, then send this again. Everything else in Local Test works whether it is running or not.
The provider is set to Ollama and nothing is listening on its port. Start Ollama and send again. If Ollama is running on a different port because you set OLLAMA_HOST, Local Test looks at the default one. The related Ollama, on this machine does not have that model yet. means Ollama answered but has not pulled the model you asked for, and the message names the command that fixes it.
The app itself #
macOS refuses to open the downloaded app
Expected on first launch, and the fix is the right-click → Open step described under Install. It is needed once per copy.
In-place update needs the installed app, opened the release page instead (you are running from source).
You are running from the source archive, so there is no application bundle to swap. The download page opens in your browser instead. Update by downloading a fresh source archive and running npm install in it.
No published releases yet, you’re ahead of the curve.
The update check completed but found no published version to compare against, the usual reason is that none has been posted yet. Nothing is wrong with your copy.
That file is not editable text.
The editor opens plain text only. The file is an image, a binary, or past the size limit for opening, about 1.5 MB for text and 3 MB for images. Open it in a real editor instead; the Open in… button next to the project path is one click away.
No supported editor found on this machine.
None of the sixteen editors Local Test knows about was found. If one is installed, it may have no command-line tool on your path, VS Code and its relatives install theirs from the command palette inside the editor. Detection runs at startup, so restart Local Test afterwards.
The app will not start from source
Delete the node_modules folder inside the Local Test folder, then run npm install again, then npm start. If the window never appears on macOS but the icon is in the Dock, an earlier copy may still be running and holding the single-instance lock; quit it first.
Everything is confused and I want a clean slate
Quit Local Test and move config.json (see where your settings live) somewhere else. The app starts fresh with no projects and default settings, and your folders are untouched. Keep the file if you might want your project list back.