Buh. Friendly local hostnames for app repos.
Localghost is a tiny Node.js CLI for clean local app domains. Add it as a dev dependency, keep running the command your team already knows, and use http://app.localhost/ instead of remembering which port belongs to which process.
Install it as a dev dependency:
npm install -D @hamedb89/localghost
pnpm add -D @hamedb89/localghost
yarn add -D @hamedb89/localghost
bun add -d @hamedb89/localghostUse the command for your package manager; you only need one of the four lines above.
For Vite apps, add the plugin once:
import { defineConfig } from "vite";
import { localGhostPlugin } from "@hamedb89/localghost/vite";
export default defineConfig({
plugins: [localGhostPlugin()]
});Then keep using the command your repo already expects:
npm exec localghostOn the first interactive run, Localghost can create .localghost, explain the /etc/hosts change, write ops/local/Caddyfile, and print the browser-facing URL:
localghost
local: http://app.localhost/
also: http://www.app.localhost/
target: http://127.0.0.1:5173/For non-Vite apps, wrap your raw dev command:
{
"scripts": {
"dev": "localghost run -- next dev",
"dev:raw": "next dev"
}
}Start the detected development server with Localghost:
npm exec localghostLocalghost detects the package manager, prefers a non-recursive dev:raw script, falls back to dev, repairs stale setup when needed, then starts Caddy and the server. Preview the decision without starting anything:
npm exec localghost -- --dry-runSupported runtime flags such as --https, --clean-caddy, --auto-repair, and --dynamic-port are forwarded by the bare command to the underlying run lifecycle.
Create the repo-local hostname contract:
localghost init --write-scriptsCheck whether the machine is ready:
localghost doctorDoctor also reports occupied configured ports, stale registry leases, and port allocations shared by multiple Localghost instances. Use --json for agent-readable output.
Prepare /etc/hosts and the local Caddyfile:
localghost setupCheck setup readiness:
localghost status --readyRepair stale hosts, Caddy configuration, or setup state:
localghost repairIf an existing project port is occupied or was allocated incorrectly, ask Localghost to choose and remember the next available port:
localghost repair --reallocate-portThis does not edit .localghost; it records a stable runtime allocation in ~/.localghost and regenerates the managed hosts and Caddy state from that allocation. To remove expired or dead leases as part of the repair, add --prune-registry.
Run only the local proxy:
localghost devWrap an app server:
localghost run -- viteSee the domain layer:
localghost routeslocalghost routes
http://app.localhost/ -> http://127.0.0.1:5173
http://api.app.localhost/ -> http://127.0.0.1:8787See active Localghost sessions:
localghost ps
localghost ps --jsonGive coding agents the supported workflow and repository conventions:
localghost guide --agent
localghost guide --agent --jsonLocalghost remembers project and instance port assignments in user-level state under ~/.localghost. The registry is used only for Localghost-managed processes; the operating-system port check remains authoritative for unrelated processes.
Check for updates:
localghost updatePrefer .localhost names. .local is supported, but Localghost warns because .local can collide with mDNS/Bonjour.
Localghost is intentionally small and explicit:
- Reads
.localghostfrom your app repo and turns hostnames into local routes. - Updates only a managed Localghost block in
/etc/hostsduring explicit setup. - Generates
ops/local/Caddyfilefor local reverse proxying. - Records setup state in
ops/local/localghost-state.json. - Keeps HTTP as the default; HTTPS is explicit with
--https,--ssl, or config. - Checks whether Caddy is installed, but never installs Homebrew packages for you.
- Gives Vite explicit
server.allowedHostsentries without usingallowedHosts: true. - Never opens browser tabs by default.
setup, dev, and teardown refuse to run in production-like environments such as NODE_ENV=production, VERCEL_ENV=production, or LOCALGHOST_ENV=production.
Use a .localghost file when you want one stable local domain:
# .localghost
app.localhost 5173Then run:
localghost setup
localghost run -- viteMap each browser-facing host to the upstream port:
# .localghost
app.localhost 5173
www.app.localhost 5173
api.app.localhost 8787
admin.app.localhost 5174localghost routes prints the same domain -> upstream map that setup and dev use.
localghost init --write-scripts adds missing scripts without replacing your existing ones:
{
"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:repair": "localghost repair",
"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"
}
}Wrap the raw app server so teammates keep typing the normal command:
{
"scripts": {
"dev": "localghost run -- vite",
"dev:raw": "vite"
}
}For Turborepo, wrap the dev runner and keep dev uncached:
{
"scripts": {
"dev": "localghost run -- turbo dev",
"dev:raw": "turbo dev"
}
}{
"tasks": {
"dev": { "cache": false, "persistent": true }
}
}localghost run starts Caddy, handles optional HTTPS trust, starts the child command, passes LOCALGHOST_PORT and VITE_PORT, and stops Caddy when the child exits.
Use the plugin when you want Vite to bind to 127.0.0.1, use the selected Localghost port, set strict allowedHosts, and print the browser-facing domain:
import { defineConfig } from "vite";
import { localGhostPlugin } from "@hamedb89/localghost/vite";
export default defineConfig({
plugins: [
localGhostPlugin({
port: 5173,
configFiles: [".localghost.private", ".localghost"]
})
]
});The plugin runs only during local vite serve; production/build mode does not configure Vite dev-server hosting. If ghostTunnel is configured, the build hook can still print the production URL shape for visibility.
By default, Localghost reads .localghost. Repos that need another file 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 idea through fileName, configFiles, or configPattern.
Most repos do not need localghost.config.mjs. Add it when you want shared defaults for CLI and Vite:
import { defineLocalghostConfig } from "@hamedb89/localghost";
export default defineLocalghostConfig({
project: "app",
port: 5173,
dynamicPort: true,
autoRepair: true,
wwwAlias: true
});Localghost derives project from package.json, defaults to port 5173, keeps HTTP as the default, enables dynamic ports and setup repair by default, and adds www. aliases by default. run, dev, and the Vite plugin perform a read-only readiness check first and repair only when the managed hosts block or setup state is stale.
With no subcommand, command takes precedence. Otherwise Localghost detects npm, pnpm, Yarn, or Bun and runs dev:raw or dev. Scripts that invoke Localghost are skipped to prevent recursion.
For a monorepo where one root command already starts every app, keep using command and list the routes in .localghost.
When Localghost should own each process, configure explicit services:
export default defineLocalghostConfig({
services: [
{
name: "web",
cwd: "apps/web",
host: "xyz.localhost",
port: 5173
},
{
name: "api",
cwd: "apps/api",
host: "api.xyz.localhost",
port: 8787
}
]
});Then bare localghost starts one Caddy instance and both services. Each command runs in its own cwd and receives its own LOCALGHOST_PORT, VITE_PORT, and LOCALGHOST_SERVICE. Dynamic-port selection and setup repair apply to every service. When Caddy or any service exits, Localghost stops the remaining processes.
Omit a service command to detect dev:raw or dev from that service directory. Service directories must stay inside the project root, and names and hosts must be unique.
Caddy startup and validation logs are quiet after success; configuration errors remain visible. Set LOCALGHOST_CADDY_VERBOSE=1 when debugging Caddy itself. Once all service ports are listening, Localghost prints the final hostname-to-upstream map beneath the service startup logs.
Try the runnable Node example in examples/multi-service.
Disable automatic repair when you want strict failure behavior:
localghost run --auto-repair=no -- viteOr set autoRepair: false in localghost.config.mjs. HTTPS certificate trust remains explicit.
Dynamic ports are on by default. Localghost starts at the configured port, checks 127.0.0.1:<port>, and walks upward until it finds a free port.
Use strict fixed-port behavior when you want startup to fail instead:
localghost run --dynamic-port=no -- viteOr in config:
export default defineLocalghostConfig({
dynamicPort: false
});HTTP is the default. Use HTTPS only when you explicitly want Caddy local certificates:
localghost setup --https
localghost dev --httpsTrust Caddy's local HTTPS CA when you want browsers to stop showing local certificate warnings:
localghost trust
localghost run --trust -- vitemacOS may ask for your password so Caddy can add its local CA to Keychain. Localghost records the trust result in ops/local/localghost-state.json.
You can also make HTTPS the repo default:
export default defineLocalghostConfig({
https: true
});By default, app.localhost 5173 also creates www.app.localhost.
Disable that when the repo wants only exact hosts:
export default defineLocalghostConfig({
wwwAlias: false
});Warning
Ghost Tunnel is experimental. Its configuration, transport protocol, and public APIs may change between releases. Do not rely on it for production-critical access, and review its authentication and network exposure before sharing a tunnel.
ghostTunnel is an opt-in production URL shape for deployed wildcard endpoints. It does not change local Caddy or /etc/hosts setup.
Use public mode when the deployed app should react to whatever route arrives:
import { defineLocalghostConfig } from "@hamedb89/localghost";
export default defineLocalghostConfig({
ghostTunnel: {
mode: "public",
domains: "copper-comet.example"
}
});Build output stays flexible instead of filling slots from the build machine:
localghost ghost tunnel
mode: public
configured: https://<route>-<project>-<owner>.ghost.copper-comet.example/Production code can parse and validate the incoming wildcard host:
import {
assertSecureGhostTunnelRequest,
readLocalghostProjectConfig
} from "@hamedb89/localghost";
const { config } = await readLocalghostProjectConfig();
const route = assertSecureGhostTunnelRequest({
host: request.headers.get("host") ?? "",
domain: "copper-comet.example",
protocol: "https",
authenticated: Boolean(session),
ghostTunnel: config.ghostTunnel
});By default, secure requests require HTTPS and app-authenticated access.
Use preview only when you want one concrete URL in logs or menus:
export default defineLocalghostConfig({
ghostTunnel: {
mode: "public",
domains: "copper-comet.example",
preview: {
route: "decisionlayer",
project: "decision-layer",
owner: "hamedbahrami"
}
}
});That prints:
localghost ghost tunnel
mode: public
configured: https://decisionlayer-decision-layer-hamedbahrami.ghost.copper-comet.example/Manual mode is the default. It is useful for private or operator-mediated sharing flows:
export default defineLocalghostConfig({
ghostTunnel: {
mode: "manual",
domains: "moonlit-otter.example"
}
});Manual relay helpers are private by default: registration requires a local-agent bearer token plus an exact signed route claim, targets must be explicit local host/port objects, dangerous ports are blocked, sensitive headers/logs are redacted, and offline agents get a safe 503 page.
Use JSON when scripts, menu bar tools, or agents need stable output:
localghost print
localghost status --json
localghost ps --json
localghost update --jsonprint parses the selected config file. status reports the project-local setup state. ps reports active sessions across projects and whether each upstream port is listening.
Reconcile the managed hosts block, regenerate and validate the Caddyfile, and refresh setup state:
localghost repairFor HTTPS certificate trust problems, explicitly re-run Caddy's trust step:
localghost repair --https --trustIf a running Caddy process exits, localghost run exits with it; starting the normal development command again launches a fresh Caddy process.
Retest setup without deleting .localghost:
localghost reset
localghost setupRemove only the managed hosts block for this project:
localghost teardownRemove the generated Caddyfile too:
localghost teardown --remove-caddyfileLocalghost includes a tiny native macOS widget under apps/macos-widget. It reads the shared activity file and shows known setup/running instances.
Build it from source:
npm run build
npm run macos:widget:buildThe app bundle is written to dist/LocalghostWidget.app.
localghost [--cwd path] [--dry-run]
localghost init [--write-scripts] [--config file] [--host host] [--port port]
localghost doctor [--cwd path] [--config file] [--config-pattern regex] [--json]
localghost setup [--project name] [--config file] [--config-pattern regex] [--https|--ssl]
localghost repair [--project name] [--config file] [--config-pattern regex] [--https|--ssl] [--trust] [--reallocate-port] [--prune-registry]
localghost trust [--project name] [--config file] [--config-pattern regex] [--https|--ssl]
localghost reset [--project name]
localghost teardown [--project name] [--remove-caddyfile]
localghost status [--ready] [--json]
localghost ps [--json]
localghost update [--json]
localghost release <patch|minor|major>
localghost dev [--config file] [--config-pattern regex] [--https|--ssl] [--auto-repair yes|no] [--trust]
localghost run [--config file] [--config-pattern regex] [--https|--ssl] [--auto-repair yes|no] [--trust] [--dynamic-port] -- command
localghost routes [--https|--ssl]
localghost print [--config file] [--config-pattern regex]import {
assertSecureGhostTunnelRequest,
constructGhostTunnelUrl,
getConfigFileCandidates,
initLocalghost,
readDevHosts,
readLocalghostState,
readLocalghostProjectConfig,
removeSystemHosts,
renderCaddyfile,
renderHostsBlock,
runDoctor,
updateSystemHosts
} from "@hamedb89/localghost";
import { localGhostPlugin } from "@hamedb89/localghost/vite";localHostsPlugin is also exported as a compatibility alias for the Vite helper.
- 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/,docs/, andassets/sources. - Preview the exact Pages artifact locally with
npm run site:serve, then openhttp://127.0.0.1:4173/. - npm publish is guarded by
prepublishOnlyand the release workflow publishes with npm provenance. - To release the CLI, run
localghost release patch,localghost release minor, orlocalghost release major. The command dispatches the Release workflow frommain; it synchronizes version metadata, verifies the package and runtime matrix, commits and tags the bump, publishes npm, and creates a GitHub Release with generated notes. GitHub CLI must be installed and authenticated. - Runtime dependencies are intentionally small:
commanderandexeca. Vite is an optional peer dependency. - No postinstall scripts, hidden Homebrew installs, surprise browser tabs, 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.
- Website
- Rendered docs
- User flows
- CLI reference
- Ghost Tunnel guide (experimental)
- macOS widget notes
- Brand guidelines
MIT
