# Persistent Localhost

> Persistent localhost servers (macOS launchd)

- Skill: `davidondrej/persistent-localhost` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add davidondrej/persistent-localhost`
- Raw SKILL.md: https://api.skillmd.com/api/skills/davidondrej/persistent-localhost/raw
- Safety review: pending (external: skill-scanner PASS, skillspector CAUTION)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: David Ondrej (https://skillmd.com/u/davidondrej)
- Updated: 2026-09-08
- Page: https://skillmd.com/skills/davidondrej/persistent-localhost

---


# Persistent localhost servers (macOS launchd)

One script does everything. Resolve `scripts/persistent-localhost.sh` relative to this `SKILL.md`, never from the current directory.

## Workflow

1. Check what already exists:

```bash
scripts/persistent-localhost.sh list
```

2. Start the server. `--name` is the handle. `--port` enables readiness checks and duplicate handling. `--dir` defaults to the current directory.

```bash
scripts/persistent-localhost.sh start --name myproj --port 5111 --cmd "./bin/serve"
```

The command returns only after the port is listening, or fails with the last 20 log lines. Report the printed `URL=` to the user.

3. Inspect, restart, read logs, stop:

```bash
scripts/persistent-localhost.sh status myproj
scripts/persistent-localhost.sh restart myproj   # after editing code that has no reloader
scripts/persistent-localhost.sh logs myproj 100
scripts/persistent-localhost.sh stop myproj
```

## Hot reload

Prefer the framework's own reloader in `--cmd` (`flask run --debug`, `vite`, `uvicorn --reload`, `node --watch`). If the server has none, add `--watch`. It wraps the command in `watchexec -r` on the working directory. Never use both. Requires `brew install watchexec`.

## Rules the script enforces

- Runs as `com.persistent-localhost.<name>` in the user `gui` domain via `launchctl bootstrap`. Restart uses `kickstart -kp`, stop uses `bootout`. No deprecated `load`, `start`, `stop`, no broad `pkill`.
- `KeepAlive` only on crash. A clean exit stays down. Throttle 5s so a syntax error cannot spin.
- Port already taken by the same app (our own label, or a stray process with the same working directory) is replaced. A different app is left alone and the next free port is used. Read the printed `PORT=`.
- The caller's `PATH` and a `PORT` variable are passed into the job. Add more with `--env KEY=VAL`.
- Logs: `~/Library/Logs/persistent-localhost/<name>.log`. State: `~/Library/Application Support/persistent-localhost/<name>.state`. Plist: `~/Library/LaunchAgents/`.

## Fallback

If `launchctl bootstrap` fails, say so and stop. Do not fall back to `nohup` or `&`.

