Developer Tools

Local dev needs an exit ramp

5 min read

A local development loop should tell you how to leave.

Starting the app is only half the contract. The other half shows up later, when the port is stuck, the database is dirty, a worker is still running, a Docker volume is full, or the last agent left a dev server in the background.

That is when local development stops feeling like a setup problem and starts feeling like an operations problem in miniature.

The project needs an exit ramp.

start is the easy command

Most projects eventually get a start command.

pnpm dev

That is fine, but it does not describe the loop. Does it start one process or five? Does it start a database? Does it write a PID file? Does it assume ports are free? Does it reuse an existing container? Does it clean old state? Does it watch files? Does it leave logs somewhere?

The start command should make its shape visible:

starting web: http://localhost:4321
starting worker: pid 18422
using database: localhost:5432
logs: .local/logs/dev.log
stop with: pnpm stop
reset with: pnpm reset

That output is not fluff. It gives the next human or agent the recovery path before anything breaks.

stop should be safe twice

The stop command should be boring and idempotent.

If the server is running, stop it. If it is already stopped, say so and exit cleanly. If the PID file is stale, remove it. If a port is occupied by a process the script started, kill it. If the port is occupied by something else, warn instead of swinging wildly.

A good stop script distinguishes ownership:

web:
  pid file exists
  process matches expected command
  sending SIGTERM

worker:
  pid file exists
  process not running
  removing stale pid file

postgres:
  external service
  leaving it alone

That is the difference between an exit ramp and a demolition button.

Local scripts should avoid cleverness where failure is expensive. Killing every process on a port may be convenient until it kills the wrong thing. Deleting every volume may be easy until it destroys test data someone needed. The script should say what it owns.

reset should name what it destroys

Reset is more dangerous than stop.

It might delete a database, clear generated files, remove caches, reinstall dependencies, prune containers, or wipe uploads. That can be useful. It should never be vague.

I want reset output that reads like a warning label:

This will delete:
  .local/db.sqlite
  .local/uploads/*
  .astro/
  node_modules/.vite

This will keep:
  .env
  src/content/*
  public/uploads/manual-fixtures

For destructive resets, require an explicit flag:

pnpm reset --yes

That protects humans and agents. An agent should not infer that reset is safe because the name sounds routine. The script should make the blast radius obvious.

stale state is expected

Local state goes stale because development is messy.

Seed data changes. Migrations half-run. Environment variables get renamed. Browser sessions persist old auth. Feature flags stick. Background jobs hold locks. Generated files lag behind schemas. A dev server survives the terminal that launched it.

The project should assume this will happen.

Useful recovery checks include:

  • port ownership
  • database migration status
  • generated file freshness
  • required environment variables
  • background worker health
  • dependency install state
  • lock files
  • stale PID files

The point is not to make local dev perfect. It is to make recovery obvious enough that someone can get back to a known state without reading every script in the repo.

agents make this more important

Coding agents make local exit ramps more valuable because they often run commands while the user is not watching every line.

An agent may start a dev server, run a browser check, leave a session alive, then move on. A later agent may inherit the same workspace and find port 3000 occupied. If the project has no stop command, the agent has to inspect processes manually and guess what is safe to kill.

That is how local state becomes a hidden dependency between sessions.

For agent-friendly projects, I like fixed script names:

init.sh   prepare dependencies and local state
serve.sh  start the app
test.sh   run the expected checks
stop.sh   stop owned processes
reset.sh  return to a known local state

Package scripts can wrap those. The important part is that the contract is discoverable.

logs should have a place

Local logs should not require terminal archaeology.

If a dev loop starts multiple processes, write logs to predictable files. Keep the terminal summary readable and let the details live somewhere durable.

.local/logs/web.log
.local/logs/worker.log
.local/logs/db.log

This helps humans and agents in the same way. When a smoke test fails, the next step can be “read .local/logs/web.log” instead of “scroll up and hope the terminal still has the line.”

Logs should rotate or reset with the local state. A six-month log file is a different kind of mess.

the readme should include the exit

README files often explain how to start and forget how to stop.

A useful local-dev section should answer:

start:
  pnpm dev

stop:
  pnpm stop

reset:
  pnpm reset --yes

smoke test:
  pnpm smoke

common failure:
  port 4321 occupied
  run pnpm stop, then pnpm dev

That is enough for most days. The deeper docs can explain internals. The top-level instructions should get a new session unstuck.

the standard

The standard I want is simple: a new session should recover without folklore.

If the project starts processes, it should stop them. If it creates state, it should explain how to reset it. If reset destroys data, it should say what data. If logs matter, they should have a path. If ports matter, they should be named.

The exit ramp is not glamorous. It is how the project lets you come back tomorrow and still know where you are.

Jeremy London

About Jeremy London

Engineering leader and builder in Denver. I write about AI platforms, agents, security, reliability, homelab infrastructure, and the parts of engineering work that have to survive production.