Buh. Friendly local hostnames for app repos.
Localghost is a tiny Node.js CLI for friendly local domains in app repos. It gives each project one small contract for .localhost hostnames, Caddy reverse proxies, Vite allowedHosts, and the system hosts file, so developers can open http://app.localhost/ instead of remembering which localhost port belongs to which process.
- Creates and reads
.localghostin your app repo. - Lets repos choose explicit config files or filename patterns when
.localghostis not enough. - Updates only a managed Localghost block in
/etc/hostsduring explicit setup. - Generates
ops/local/Caddyfilefor local reverse proxying. HTTP is the default; HTTPS is explicit with--httpsor--ssl. - Checks whether Caddy is installed, but does not run Homebrew for you.
- Provides a Vite plugin that sets explicit
server.allowedHostsentries. - Defaults Vite dev to the configured Localghost domain and no-ops during production/build.
- Prints parsed config and project-local state as JSON for scripts, Codex, agents, and future MCP tools.
- Checks npm for newer Localghost releases at most once per day, with an explicit opt-out.
- CI runs typecheck, build, site build, and npm package dry-run on Node 20 and 22.
- GitHub Pages is deployed by Actions from the checked-in
site/andassets/sources. - npm publish is guarded by
prepublishOnlyand the release workflow publishes with npm provenance. - Runtime dependencies are intentionally small:
commanderfor the CLI andexecafor process execution. Vite is an optional peer dependency for the Vite plugin. - No postinstall scripts, hidden Homebrew installs, or broad hosts-file rewrites.
- Update checks are best-effort, cached for 24 hours, and can be disabled with
LOCALGHOST_NO_UPDATE_CHECK=1or--no-update-check.
yarn add -D @hamedb89/localghostCreate the project config and optional package scripts:
yarn localghost init --write-scriptsThis creates .localghost:
# Buh. Friendly names for local services.
# Format: <host> <port>
app.localhost 5173
www.app.localhost 5173
api.app.localhost 8787Check the machine:
yarn localghost doctorIf Caddy is missing, Localghost prints:
Caddy: missing
Run: brew install caddy
Localghost will not install it for you. No surprise spells.First time on a machine:
yarn localghost:setupCheck that the hosts block and Caddyfile are ready:
yarn localghost:readyDaily proxy:
yarn localghost:proxyUse HTTPS only when you explicitly want Caddy local certificates:
yarn localghost:proxy:httpsTrust Caddy's local HTTPS CA when you want browsers to stop showing local certificate warnings:
yarn localghost:trustReset generated setup without deleting .localghost:
yarn localghost:reset
yarn localghost:setupPrefer .localhost names. .local is supported, but Localghost warns because .local can collide with mDNS/Bonjour.
By default, Localghost reads .localghost from the project root. Repos that need another name can be explicit:
localghost print --config .localghost.preview
localghost setup --config .localghost.previewYou can pass --config more than once. Localghost uses the first file that exists:
localghost print --config .localghost.private --config .localghostYou can also search project-root filenames with a regular expression:
localghost print --config-pattern '^\.localghost\.(private|preview)$'The Vite plugin accepts the same shape through fileName, configFiles, or configPattern. If you run localghost init --config .localghost.preview --write-scripts, generated package scripts include the matching --config flag.
localghost init --write-scripts adds these scripts when they are missing:
{
"scripts": {
"localghost:setup": "localghost setup",
"localghost:proxy": "localghost dev",
"localghost:proxy:https": "localghost dev --https",
"localghost:run": "localghost run --",
"localghost:ready": "localghost status --ready",
"localghost:trust": "localghost trust",
"localghost:ps": "localghost ps",
"localghost:print": "localghost print",
"localghost:routes": "localghost routes",
"localghost:status": "localghost status",
"localghost:reset": "localghost reset",
"localghost:teardown": "localghost teardown",
"localghost:doctor": "localghost doctor",
"localghost:update": "localghost update",
"caddy:setup": "localghost setup",
"caddy:dev": "localghost dev"
}
}A full app might compose them with its own servers:
{
"scripts": {
"dev": "localghost run -- yarn dev:raw",
"dev:dynamic": "localghost run --dynamic-port -- vite",
"dev:raw": "vite"
}
}In Turborepo, let Localghost wrap the dev runner and keep dev uncached:
{
"scripts": {
"dev": "localghost run --dynamic-port -- yarn dev:raw",
"dev:raw": "turbo dev"
}
}Then keep persistent dev tasks uncached:
{
"tasks": {
"dev": { "cache": false, "persistent": true }
}
}localghost run resolves one shared Localghost context, starts Caddy, handles the optional HTTPS trust prompt, then starts the child command. That keeps Localghost setup/proxy output before Vite's ready log. It passes the selected port to the child command through LOCALGHOST_PORT and VITE_PORT, and stops Caddy when the child exits. With --dynamic-port, Localghost starts at the configured port, checks 127.0.0.1:<port>, and walks upward until it finds a free port.
When settings need to be shared by the CLI wrapper and the Vite plugin, put them in localghost.config.mjs:
import { defineLocalghostConfig } from "@hamedb89/localghost";
export default defineLocalghostConfig({
project: "app",
port: 5173,
https: true,
dynamicPort: true
});Then the daily script can stay small:
{
"scripts": {
"dev": "localghost run -- yarn dev:raw",
"dev:raw": "vite"
}
}www. aliases are enabled by default. A .localghost entry like app.localhost 5173 also sets up www.app.localhost unless wwwAlias: false is set in localghost.config.mjs.
localghost dev and localghost run also register their active sessions in a user-local activity file. Use localghost ps to see the Localghost apps currently running on the machine:
localghost ps
app run: vite
cwd: /Users/you/Projects/app
pid: 12345, caddy: 12346, child: 12347
started: 2026-07-05T12:00:00.000Z
app.localhost -> 127.0.0.1:5173 (listening)Pass --json when another helper, such as a menu bar app, needs to poll the same state.
Localghost includes a tiny native macOS menu-bar widget in apps/macos-widget. It shows LG n in the top bar, where n is the number of active Localghost-managed apps, and its menu lists each project, route, target port, and listening state.
Build it from source:
npm run build
npm run macos:widget:buildThe app bundle is written to dist/LocalghostWidget.app. See docs/macos-widget.md for local development notes.
import { defineConfig } from "vite";
import { localGhostPlugin } from "@hamedb89/localghost/vite";
export default defineConfig({
plugins: [
localGhostPlugin({
port: 5173,
configFiles: [".localghost.private", ".localghost"]
})
]
});The plugin binds Vite to 127.0.0.1 by default, prints the selected Localghost domain, generates an explicit server.allowedHosts list from the selected config file, and does not set allowedHosts: true. It runs only during local vite serve; production/build mode no-ops. When dynamicPort is enabled in plugin options or localghost.config.mjs, the plugin uses the configured port when available and otherwise moves to the next free port before Vite starts.
If .localghost is missing and Vite is running in an interactive terminal, the plugin asks whether to create one, prompts for the primary domain and optional extra domains, and then asks whether to run setup. Before touching /etc/hosts, it explains why macOS may ask for your password and confirms that only Localghost's managed block is changed.
When Vite starts, Localghost prints the browser-facing URLs:
localghost
local: http://app.localhost/
also: http://www.app.localhost/
target: http://127.0.0.1:5173/https: true means the browser-facing URL is expected to go through Caddy on HTTPS, while Vite still runs behind it on 127.0.0.1:<port>. The plugin uses that to set Vite websocket/HMR proxy settings and to print https://... local host URLs. Localghost never opens browser tabs by default.
Set log: false if you want to keep Vite's default terminal output only.
localghost init
localghost init --write-scripts
localghost doctor
localghost setup
localghost setup --project app
localghost setup --config .localghost.preview
localghost setup --https
localghost trust
localghost status
localghost status --ready
localghost ps
localghost ps --json
localghost reset
localghost teardown
localghost teardown --remove-caddyfile
localghost update
localghost --no-update-check doctor
localghost run -- vite
localghost run --trust -- vite
localghost run --dynamic-port -- turbo dev
localghost dev --config-pattern '^\.localghost\.'
localghost dev --https
localghost printLocalghost checks npm for newer releases after successful commands. The check has a short timeout, is cached for 24 hours, and never fails the command. Disable it with LOCALGHOST_NO_UPDATE_CHECK=1 or --no-update-check. Run localghost update when you want an explicit update check.
setup, dev, and teardown refuse to run in production-like environments such as NODE_ENV=production, VERCEL_ENV=production, or LOCALGHOST_ENV=production.
When HTTPS is enabled, localghost dev and localghost run ask once whether to trust Caddy's local HTTPS CA. If you accept, macOS may ask for your password so Caddy can add its local CA to Keychain. Localghost records the result in ops/local/localghost-state.json; use localghost trust or localghost run --trust -- ... when you want to rerun the trust step intentionally.
setup writes only a managed block in the system hosts file:
# localghost:start app
127.0.0.1 app.localhost
# localghost:end appLocalghost does not rewrite the whole hosts file. It replaces only its own managed block for the selected project.
setup writes a project-local state file at ops/local/localghost-state.json. It records the last Localghost action, selected config path, generated Caddyfile path, hosts file path, proxy mode, and the host entries that were applied. This is durable enough for project tooling and avoids relying on OS temp folders for tracking. Most apps should treat it as generated local state and ignore it in git.
localghost status
localghost status --ready
localghost status --jsonlocalghost dev requires setup to be ready before it starts Caddy. If setup is missing or stale, it prints the exact localghost setup command instead of silently running sudo. Use localghost dev --setup only when you explicitly want the dev command to perform setup first.
When a project no longer needs Localghost, teardown removes only the managed hosts block for the selected project:
localghost teardownWhen you want to retest setup without deleting .localghost, use reset:
localghost reset
localghost setupThe generated Caddyfile is left in place by default. Remove it explicitly when you want a fuller cleanup:
localghost teardown --remove-caddyfileLocalghost still uses a short-lived OS temp file while copying /etc/hosts with sudo, but that temp file is not the source of truth.
import {
getConfigFileCandidates,
initLocalghost,
readDevHosts,
readLocalghostState,
removeSystemHosts,
renderCaddyfile,
renderHostsBlock,
runDoctor,
updateSystemHosts
} from "@hamedb89/localghost";
readDevHosts({ configFiles: [".localghost.private", ".localghost"] });
readDevHosts({ configPattern: /^\.localghost\.(private|preview)$/ });Vite helper:
import { localGhostPlugin } from "@hamedb89/localghost/vite";localHostsPlugin is also exported as a compatibility alias.
Localghost copy can be mysterious, goofy, magical, funny, and a little absurd. The product behavior should stay boring in the best way: explicit commands, exact paths, clear errors, and no hidden installs.
MIT


