Give your website
local machine power.
web2local is a tiny daemon that bridges websites to your local environment — safely. Your site can run commands, read files, and talk to hardware that a browser alone can never reach.
How it works
Browsers are sandboxed on purpose — a web page cannot touch your filesystem, run a process, or connect to a serial port without explicit permission. But some tools genuinely need local power: build scripts, hardware dashboards, developer utilities, media processors.
web2local runs a small HTTP server bound to 127.0.0.1 only
(never reachable from the internet). Your website calls it with
fetch(). The daemon checks who is asking, then acts accordingly.
The daemon answers only to requests whose Host header is
127.0.0.1 or localhost. This one check kills the
most dangerous class of localhost attacks — DNS rebinding —
where a malicious site manipulates DNS to make the browser treat
evil.com as a local address.
Security model
Every request carries an Origin header — the site that made it.
The daemon classifies that origin into one of three tiers:
What the graylist dialog shows you
When a graylist site sends a command, a window appears with:
- The exact requesting origin (the website's URL)
- The full command in a scrollable monospace box — no truncation
- A countdown timer (auto-deny at zero)
- Two buttons: Deny and Allow Execution
The command is displayed as an argument list — no shell expansion happens. What you see is exactly what will run. Scroll right if it is long; the dialog will not dismiss until you click a button.
What it does not protect against
The graylist is only as good as your reading. Keep these risks in mind:
- Rubber-stamping — users who click Allow without reading are the weakest link.
- State accumulation — a site approved for 10 harmless commands may use their combined effect maliciously.
- Non-browser callers —
Originis only enforced by browsers. A script usingcurlcan forge it. The daemon should only be used from browser-based websites. - Broad whitelist scope — whitelisting
https://example.comtrusts every page on that domain.
Quick start
Get the daemon
A single Python file, no dependencies beyond the standard library. Requires Python 3.7+.
Run the daemon
python3 daemon.py
The daemon binds to 127.0.0.1:7878 and stays running
in your terminal. On the first launch it creates a config file at
~/.config/web2local/config.json.
Add your site to a list
You can do this from the demo below, from the config file directly, or via the API:
# Whitelist (trust fully)
curl -s -X POST http://127.0.0.1:7878/config/whitelist \
-H 'Content-Type: application/json' \
-d '{"origin":"https://mysite.com"}'
# Graylist (ask before each command)
curl -s -X POST http://127.0.0.1:7878/config/graylist \
-H 'Content-Type: application/json' \
-d '{"origin":"https://other-site.com"}'
Include the client library
<script src="client.js"></script>
Copy client.js from this repo into your project.
Call the API from your page
const w2l = new Web2Local(); // default port 7878
if (await w2l.isRunning()) {
const r = await w2l.run("ls", ["-la", "/tmp"]);
console.log(r.stdout);
console.log("exit:", r.exit_code);
} else {
console.warn("web2local daemon not running");
}
API reference
All endpoints are on http://127.0.0.1:7878.
| Method & Path | Auth | Description |
|---|---|---|
| GET /status | any origin | Returns {"status":"running","version":"1.0.0"}. Use this to detect the daemon. |
| POST /run | whitelist / graylist | Execute a command. Body: {"command":"ls","args":["-la"]}.
Returns {"stdout":"…","stderr":"…","exit_code":0}. |
| GET /config | any (local only) | Return current whitelist and graylist. |
| POST /config/whitelist | any (local only) | Add origin to whitelist. Body: {"origin":"https://…"}. |
| POST /config/graylist | any (local only) | Add origin to graylist. |
| POST /config/remove | any (local only) | Remove origin from all lists. |
| POST /config/reload | any (local only) | Reload config from disk without restarting. |
| GET /log | any (local only) | Return last 200 audit log lines. |
| POST /spawn | whitelist / graylist | Start a long-running process. Returns immediately with {"pid":…, "started_at":"…", "log_path":"…"}. Output is captured to the log file. |
| POST /stop | whitelist / graylist | Stop a spawned process by PID. Body: {"pid":1234}. Sends SIGTERM, then SIGKILL after 3 s. |
| GET /ps | any (local only) | List spawned processes still alive (verified via /proc start-time, so PID reuse won't fool it). |
| GET /logs?pid=N | any (local only) | Return the last 200 lines of a spawned process's captured output. |
| POST /deploy | whitelist / graylist | Verify SHA-256 → write script to ~/.config/web2local/agents/ → spawn it. Always shows the approval dialog, even for whitelist origins — the dialog shows filename, full SHA-256, destination path, and the exact command. Body: {"source":"…","sha256":"…","filename":"…","command":"python3","args":["serve",…]}. Returns {"pid":…,"path":"…","already_running"?:true}. |
Error responses
403 {"error": "origin not in whitelist or graylist"}
403 {"error": "command denied by user"}
403 {"error": "host must be 127.0.0.1 or localhost"}
400 {"error": "command must be a non-empty string"}
408 {"error": "command timed out (30 s limit)"}
Command rules
commandis the executable name — no shell, no&&, no pipes.argsis a flat list of strings — each element is one argument.- Commands time out after 30 seconds.
- The process inherits the daemon's environment and working directory.
Live demo
This page connects to your local daemon in real time.
Run a command
Manage lists
Audit log (last 20 entries)
Process manager
POST /run waits for the command to finish — fine for quick
jobs, useless for daemons. POST /spawn starts a process and
returns immediately with its PID. The daemon tracks
every spawned PID, captures its output to a log file, and lets you stop
it from this page.
Spawn a process
Tip: for a shell pipeline use sh with -c "…".
Output is captured to ~/.config/web2local/processes/.