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
- Check what already exists:
scripts/persistent-localhost.sh list
- Start the server.
--name is the handle. --port enables readiness checks and duplicate handling. --dir defaults to the current directory.
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.
- Inspect, restart, read logs, stop:
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 &.
1---
2name: persistent-localhost
3description: Start, stop, restart, and inspect durable localhost dev servers on macOS as user LaunchAgents, so they outlive the agent's shell and auto-restart on crash. Use whenever an agent must run a dev server, API, or any long-lived local process on a port: "start the dev server", "run it on localhost", "keep the server running", "the server keeps dying", "restart the app on port 5111". Never start servers with nohup, &, disown, setsid, or run_in_background; those die with the shell or become unfindable orphans.
4---
5
6# Persistent localhost servers (macOS launchd)
7
8One script does everything. Resolve `scripts/persistent-localhost.sh` relative to this `SKILL.md`, never from the current directory.
9
10## Workflow
11
121. Check what already exists:
13
14```bash
15scripts/persistent-localhost.sh list
16```
17
182. Start the server. `--name` is the handle. `--port` enables readiness checks and duplicate handling. `--dir` defaults to the current directory.
19
20```bash
21scripts/persistent-localhost.sh start --name myproj --port 5111 --cmd "./bin/serve"
22```
23
24The command returns only after the port is listening, or fails with the last 20 log lines. Report the printed `URL=` to the user.
25
263. Inspect, restart, read logs, stop:
27
28```bash
29scripts/persistent-localhost.sh status myproj
30scripts/persistent-localhost.sh restart myproj # after editing code that has no reloader
31scripts/persistent-localhost.sh logs myproj 100
32scripts/persistent-localhost.sh stop myproj
33```
34
35## Hot reload
36
37Prefer 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`.
38
39## Rules the script enforces
40
41- 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`.
42- `KeepAlive` only on crash. A clean exit stays down. Throttle 5s so a syntax error cannot spin.
43- 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=`.
44- The caller's `PATH` and a `PORT` variable are passed into the job. Add more with `--env KEY=VAL`.
45- Logs: `~/Library/Logs/persistent-localhost/<name>.log`. State: `~/Library/Application Support/persistent-localhost/<name>.state`. Plist: `~/Library/LaunchAgents/`.
46
47## Fallback
48
49If `launchctl bootstrap` fails, say so and stop. Do not fall back to `nohup` or `&`.