Serve LLM-generated HTML files from your own machine via wildcard subdomains.
Your LLM runs uptool deploy → gets back a URL → you open it anywhere.
LLM writes dashboard.html
LLM runs: uptool deploy dashboard.html
LLM says: ✓ http://x7k2mq.mydev.com (expires in 72h)
You open it on your phone, tablet, or any browser
No cloud service. No third-party uploads. Your machine, your domain.
npm install -g uptool
# or use without installing:
npx uptool <command>Build and install from the repository — useful for trying unreleased changes or contributing:
# 1. Clone
git clone https://github.com/pyeom/uptool.git
cd uptool
# 2. Install dependencies
npm install
# 3. Build (compiles src/ → dist/cli.js)
npm run build
# 4a. Link it as a global `uptool` command…
npm link
# 4b. …or run directly without linking:
node dist/cli.js <command>Verify it works:
uptool --versionTo remove a linked build later:
npm unlink -g uptoolRequires Node.js ≥ 18.
You need:
- A domain you control (e.g.
mydev.com) - A wildcard DNS record pointing to your machine:
*.mydev.com → A → <your machine's public IP> - Port forwarding on your router: port 3000 (or whichever you configure) → your machine
No static IP? Use Cloudflare Tunnel — free, no port forwarding needed.
uptool initWalks you through configuration and writes ~/.uptool/config.toml:
base_url = "mydev.com"
port = 3000 # public HTTP server
api_port = 3001 # internal API (localhost only)
ttl = "72h" # file expiry — 0 = never
storage_path = "~/.uptool/files"
# Optional
max_file_size = 5242880 # max bytes per deployed file (0 = unlimited, default 5 MB)
max_total_storage = 524288000 # max bytes across all deployments (0 = unlimited, default 500 MB)
rate_limit_rpm = 0 # per-IP requests/min on the public server (0 = off)
trust_proxy = false # read X-Forwarded-For for client IP (only behind a proxy you control)
# cert_file = "/path/fullchain.pem" # enables HTTPS when set with key_file
# key_file = "/path/privkey.pem"uptool serveStarts as a background daemon. Logs go to ~/.uptool/server.log.
uptool deploy dashboard.html
# ✓ http://x7k2mq.mydev.com (expires in 72h)cat output.html | uptool deploy
# or pipe directly from a script
echo "<h1>Hello</h1>" | uptool deployLLM output wrapped in markdown fences is handled automatically:
```html
<h1>Hello</h1>
→ strips fences, deploys the HTML
### Update an existing deployment
Keep the same URL while the LLM iterates:
```bash
uptool deploy v2.html --update x7k2mq
# same URL, new content
Print a scannable QR code for the URL, handy for pulling a deploy up on a phone:
uptool deploy dashboard.html --qrWith multiple files in one invocation, a QR is printed after each URL.
Keep the process running and redeploy in place whenever the source changes:
uptool deploy dashboard.html --watch
# ✓ http://x7k2mq.mydev.com
# Watching dashboard.html for changes... (Ctrl-C to stop)
# ↻ redeployed http://x7k2mq.mydev.com (14:32:07)Works on a single file or a directory bundle, and combines with --qr (printed once, on the first deploy). Changes are debounced 300ms. --watch requires exactly one file/directory argument and can't be used with stdin. Stop with Ctrl-C.
Require a key to view (dashboards with semi-private data, drafts):
uptool deploy report.html --protect # autogenerates a key
# ✓ http://x7k2mq9a.mydev.com
# key: dGhpc2lzYWtleQ (Basic Auth password — any username)
uptool deploy report.html --protect mysecret # or bring your ownThe browser prompts once (leave the username blank, paste the key as the password) and re-sends credentials for every asset in the bundle. Updating with --update keeps the existing key. Use HTTPS — Basic Auth over plain HTTP is readable in transit.
Extend a deployment's TTL without redeploying:
uptool touch x7k2mq9a --ttl 7d # 7 more days from now
uptool touch dashboard --ttl 0 # never expire
uptool touch x7k2mq9a # renew with the configured default ttluptool list
# x7k2mq http://x7k2mq.mydev.com [dashboard.html] expires in 71h 45m 3 hits · last seen 12m ago
# a9f3kd2p http://a9f3kd2p.mydev.com [draft.html] expires in 20h 3m never viewedView counts track HTML page loads only — assets inside a bundle, 404s and
HEAD requests don't inflate the number. They survive daemon restarts and
--update redeploys.
For scripts (or for the LLM driving uptool), --json emits the full record:
uptool list --json
# [{"slug":"x7k2mq","url":"http://x7k2mq.mydev.com","filename":"dashboard.html",
# "created":1754006400000,"expires":1754265600000,"hits":3,
# "last_seen":1754092800000,"protected":false}]Access keys of protected deployments are never included in either output.
uptool rm x7k2mquptool stop # stop the daemon
uptool status # check if running + last 10 log lines
uptool status --json # machine-readable health for monitoring (exit 1 if unhealthy)uptool logs # last 50 lines of ~/.uptool/server.log
uptool logs -n 200 # last 200 lines
uptool logs -f # follow as it grows (Ctrl-C to stop)Reads the log file directly — no daemon or config needed, so it still works when the daemon is down, which is usually when you want it.
Survives reboots and restarts on failure:
uptool install-service
systemctl --user daemon-reload
systemctl --user enable --now uptool
# start on boot without logging in:
loginctl enable-linger $USERPoint Uptime Kuma (or any monitor) at a cron job running uptool status --json — it exits non-zero when the daemon or API is down.
Tell your LLM to deploy files using uptool deploy. Example prompt addition:
When you create an HTML file for me to review, run
uptool deploy <filename>and include the returned URL in your response.
Works with Claude Code, Cursor, Cline, or any tool-enabled LLM that can run shell commands.
uptool serve
├── Public server (port 3000) — routes by subdomain slug → serves HTML
└── Internal API (port 3001) — localhost only, accepts deploy/list/rm
uptool deploy file.html
└── POSTs HTML to internal API → gets slug back → prints URL
Files stored at ~/.uptool/files/<slug>.html. Manifest at ~/.uptool/files/manifest.json. Expired files cleaned on startup and hourly.
| Value | Meaning |
|---|---|
72h |
72 hours |
7d |
7 days |
30m |
30 minutes |
0 |
Never expires |
uptool serves files from your machine on your domain, reachable by anyone on the internet. Understand what that means before you point a domain at it.
- Anything you deploy is public. There is no login wall on served pages. Anyone with the URL can view the content. Don't deploy secrets, credentials, or private data.
- Slugs are unguessable; names are not. Random slugs (
x7k2mq) are 8 chars of crypto-random base36 — not enumerable. But a named deployment (--name dashboard) is trivially guessable (dashboard.yourdomain). Use names only for content you're fine exposing. - You are responsible for what you host. Serving content on your domain makes you the publisher of it. Don't deploy untrusted HTML you wouldn't stand behind.
- The internal API is protected with a bearer token. It binds to
127.0.0.1and checks theAuthorization: Bearer <token>header. The token is stored in~/.uptool/token(mode 0600, readable by your user only) and generated duringuptool init. All CLI commands read this token and pass it to the daemon. - Protected deployments use Basic Auth.
--protectkeys are stored in the local manifest and checked with a constant-time compare. Over plain HTTP the key travels base64-encoded, not encrypted — combine--protectwith HTTPS or treat it as a speed bump, not a lock. - Use HTTPS for anything real. Set
cert_file/key_file(certs for your own domain), or terminate TLS at a proxy such as Cloudflare Tunnel. Plain HTTP sends content — and the live-reload socket — in the clear. - Abuse controls. The public server sets request/header/idle timeouts by default. Deploys are capped by
max_file_size(5 MB) andmax_total_storage(500 MB) so a looping LLM can't fill your disk. For raw internet exposure you can also setrate_limit_rpm. Behind a proxy/tunnel, settrust_proxy = trueso the limit keys off the real visitor IP instead of the proxy.
Found a vulnerability? Open an issue at https://github.com/pyeom/uptool/issues (or mark it security-sensitive).
MIT — see LICENSE.