Skip to content
Back to skills

Dev Server Start

ASecurity

Start one workspace member's dev server in the background and wait for ready — package manager and port from project config; reuses a running server.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 20, 2026
ai-agentsrustgoshellbashangularnextjsnodeapifrontendfullstack

Works with

  • terminal
  • api
  • mcp

Security analysis

A100/100

Scanned October 20, 2026

npx -y skills add adnanmokhtar/refract --skill dev-server-start --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Dev Server Start?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Dev Server Start
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/adnanmokhtar-dev-server-start/badge)](https://www.skillsdirectory.com/skills/adnanmokhtar-dev-server-start)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: dev-server-start
description: Start one workspace member's dev server in the background and wait for ready — package manager and port from project config; reuses a running server.
kind: skill
pack: frontend
allowed-tools: [Read, Grep, Glob, Bash]
---

# dev-server-start

## Premise

Existing project conventions are the truth. Read `package.json`, the lockfile, and the framework config before guessing — never invent a start command. Mirror the script the project already ships with (`dev`, `start`, `serve`); if none exists, halt rather than fabricate one. Idempotency is non-negotiable: a re-run in the same session must detect an already-running server, not spawn a duplicate.

Refuse to start a server when `package.json` is absent at the chosen project root — and **choose that root deliberately**, because in a workspace the nearest `package.json` above `$PWD` belongs to the workspace, whose `dev` script starts N servers rather than one.

Boilerplate for "I need the app running locally before I can verify it." Used as a prerequisite by `verify-with-playwright`, `a11y-scan`, `visual-check`, `ssr-audit`, and any task that ends with "open the page and check."

## When to run

- Before invoking the Playwright MCP server's `navigate` tool against `http://localhost:<port>`.
- Before running an e2e test that hits `localhost`.
- Before a manual visual check — agent or human.

## When NOT to run

- The user already has the dev server running in another terminal — `lsof -iTCP:<port> -sTCP:LISTEN` should detect it; this skill is a no-op then.
- Static-only checks (lint, type-check, bundle analysis) — no server needed.
- Production / staging URL targets — point Playwright there directly.

## Inputs

- Working directory of the project (defaults to `$PWD`).
- Optional `app_target` — the workspace member to start (`apps/web`, or its package name). Required, or the run halts, when the workspace holds more than one app member. Ignored in a single-package repo.
- Optional `port_override` (numeric) — when the project's default port is taken or a separate instance is needed (e.g., E2E parallel runs).

## Procedure

### Step 0 — Resolve PROJECT_ROOT (runs before everything)

The hard rule below requires a verified `package.json` at `PROJECT_ROOT`. Nothing *chooses* that root unless this step does, and in a workspace the obvious choice is the wrong one.

1. **Is this a workspace?** Walk up from `$PWD` to the outermost directory holding a `package.json`. It is a workspace root when any of these sits beside it: `pnpm-workspace.yaml`, a `workspaces` key in that `package.json` (npm / yarn / bun), `turbo.json`, `nx.json`, `lerna.json`, `rush.json`.
   **Not a workspace** → `PROJECT_ROOT` is that directory; go to Step 1.

2. **Does the root `dev` script fan out?** Read `scripts.dev` there. `turbo run dev`, `nx run-many`, `pnpm -r dev`, `npm run dev --workspaces`, `lerna run dev`, `concurrently ...` each start **several** servers. That is the whole failure this step exists for: Step 2's single-port precedence picks one default, Step 5 declares ready on whichever app bound it first, and `verify-with-playwright`, `a11y-scan`, `visual-check` and `ssr-audit` then all run against the wrong app with no error raised anywhere. A silently wrong URL is worse than a halt, because the four downstream skills report clean results about a page nobody asked for.

3. **Enumerate the app members.** Resolve the workspace globs — `pnpm-workspace.yaml` `packages:`, the `workspaces` array, or `apps/*` / `packages/*` — then keep only the members that are *apps*: a member with its own `scripts.dev` **and** a framework config beside it (`vite.config`, `next.config`, `nuxt.config`, `angular.json`, `svelte.config`, `astro.config`). A member with a `dev` script but no framework config (a watch-mode library build, an API server in a fullstack repo) is not an app for this skill's purpose — name it in the candidate list, do not silently drop it, and let the caller pick if that is what they meant.

4. **Choose, and never guess:**
   - `app_target` given → that member.
   - Exactly one app member → take it and say so: `root: apps/web (sole app member)`.
   - More than one app member and no `app_target` → **HALT `workspace-target-ambiguous`**, printing one line per candidate: `<dir> — <dev script> — <framework> — <configured port>`. Do not run the fan-out script; do not take the alphabetically-first member.

5. **What resolves where.** `PROJECT_ROOT` is the chosen **member** directory — the framework config, the port, and the ready-probe all come from there. Two exceptions: the **package manager** is read from the lockfile at the *workspace* root (members do not carry one), and `.claude/dev-server.{pid,log}` stay at the workspace root, suffixed with the member so two members can run at once (`dev-server.web.pid`). Launch through the workspace runner so the member's dependencies resolve — `pnpm --filter <member> dev`, `npm run dev -w <member>`, `yarn workspace <member> dev`, `bun run --filter <member> dev`, or `npx --no turbo run dev --filter=<member>` — never a bare `cd <member> && npm run dev`, which bypasses hoisting in some layouts.

### Step 1 — Detect the runtime + start command

Read `scripts.dev` in the **chosen member's** `package.json` (or `scripts.start` for Next/Nuxt). Detect package manager from the lockfile at the workspace root (single-package repo: the same directory):
- `pnpm-lock.yaml` → `pnpm dev`
- `yarn.lock` → `yarn dev`
- `bun.lock` (text lockfile, the default from Bun 1.2) or `bun.lockb` (binary, older Bun) → `bun dev`
- `package-lock.json` → `npm run dev`

If no `dev` script and no recognizable framework config (`vite.config`, `next.config`, `nuxt.config`, `angular.json`, `svelte.config`), halt with `dev-server-not-applicable`.

### Step 1b — Read the framework's own dev lock/manifest FIRST

Some frameworks now record their running dev server themselves — Next.js 16 added a lockfile mechanism
that prevents a second `next dev` on the same project, and prints where the running one is. **A file the
framework wrote is authoritative for URL and PID; the port-probing heuristic below is the fallback for
frameworks that write nothing.** Check for it before probing, and if it exists, trust it over `lsof`.

Confirm the exact path against the installed version's own docs before relying on it — this skill does
not hardcode a path it has not verified for that version, and a stale guess is worse than the probe.

### Step 2 — Detect the port

Order of precedence:
1. `port_override` input.
2. Explicit port in the chosen member's `vite.config.ts` `server.port` / `next.config.js` / `nuxt.config.ts`, or a `--port` flag inside its own `dev` script.
3. Framework default: Vite → 5173, Next → 3000, Nuxt → 3000, Angular → 4200, SvelteKit → 5173, Remix → 3000. **In a workspace, two members can share one default** — `apps/web` and `apps/admin` both on 3000. Whichever booted first owns the port; the second is not this app, so treat that as the collision path in Step 3, not as `already-running`.
4. Fall back to 3000.

### Step 3 — Idempotency check

```bash
lsof -nP -iTCP:<port> -sTCP:LISTEN 2>/dev/null
```

If a process is already listening on that port AND it's a node/bun process (`lsof` output contains `node` or `bun`), assume it's the dev server — but confirm it is **this member's** server, not a sibling workspace member or another repo squatting on the same default port (probe the body for this app's mount point / title / a route only this member serves before adopting it). In a workspace this check is not optional: the sibling app is a node process serving 200s on the expected port, so an unprobed adoption always succeeds and is always wrong. Probe `http://localhost:<port>` with a 2-second timeout — 200 / 304 / any 4xx other than 404 means it's serving. Return `already-running` with the URL.

If something else is listening (Postgres on 5432, etc.), choose `port_override = port + 1`, retry.

### Step 4 — Launch in background

```bash
mkdir -p "$WS_ROOT/.claude"
LOG="$WS_ROOT/.claude/dev-server${MEMBER:+.$MEMBER}.log"; PIDF="$WS_ROOT/.claude/dev-server${MEMBER:+.$MEMBER}.pid"
( cd "$PROJECT_ROOT" && exec nohup <pkg-manager> dev ) > "$LOG" 2>&1 &
echo $! > "$PIDF"
[ -s "$PIDF" ] && kill -0 "$(cat "$PIDF")" 2>/dev/null || { echo "dev-server-start: no live PID recorded"; exit 1; }
```

`$!` is set only in the shell that ran the `&`. The earlier form, `( cd … && <pm> dev … & )` followed by `echo $! > pid`, backgrounded inside a subshell, so the outer `$!` was empty and every later cleanup killed nothing. Here the subshell itself is backgrounded and `exec` replaces it with the package manager, so `$!` is that process. It is the package manager's PID, not the bundler's: for cleanup, kill it and then whatever still listens on the port (`lsof -t -nP -iTCP:<port> -sTCP:LISTEN`). `kill -0` checks liveness without `ps`, which some sandboxes refuse. `WS_ROOT` is the workspace root (Step 0, item 5) and `MEMBER` the member name, empty in a single-package repo.

### Step 5 — Wait for ready

Poll `http://localhost:<port>` every 500ms. Ready conditions:
- HTTP 200 / 304 / 304-equivalent.
- For SPAs: response body contains the root mount-point (`<div id="app">`, `<div id="__next">`, etc.) — guards against the framework still bundling.

Timeout: 60 seconds. On timeout, print last 30 lines of `.claude/dev-server.log` and halt with `dev-server-timeout`.

### Step 6 — Hand off

Return:
- `url` — `http://localhost:<port>` (or override).
- `pid` — for the caller to kill on cleanup if it owns lifecycle.
- `mode` — `started-fresh` or `already-running` (caller decides whether to kill on exit; never kill an already-running server).

## Output format

```
dev-server-start
  root: apps/web        (workspace: pnpm + turbo; 2 app members, app_target given)
  pkg-mgr: pnpm
  cmd: pnpm dev
  port: 5173
  mode: started-fresh
  pid: 84321
  url: http://localhost:5173
  log: .claude/dev-server.log
  ready-after: 4.2s
```

## Failure modes

- **Port collided with non-dev process** — detected (Postgres, system service); skill auto-bumps and retries on `port + 1`. If still colliding after 3 bumps, halt.
- **Bundler error** — dev server exits in <2s with non-zero code. Skill detects via `kill -0` on the recorded PID + log inspection. Surfaces last 30 log lines + halts.
- **Watcher exhausted** (Linux ENOSPC) — fs.inotify limit hit. Surface the precise error + suggest `sudo sysctl fs.inotify.max_user_watches=524288`.
- **Wrong project root** — `package.json` not found at `$PROJECT_ROOT`. Halt; do not start a server in the wrong dir.
- **Workspace fan-out mistaken for one server** — the root `dev` delegates to `turbo` / `nx` / `pnpm -r`, N servers boot, and the ready probe adopts whichever app won the default port. Prevented by Step 0; if the fan-out already started, kill it and re-run against a single member rather than probing your way out.
- **Sibling member adopted as `already-running`** — two members share a framework default port and the idempotency probe skipped the body check. The run then verifies the wrong app and reports PASS.

## Related

- `verify-with-playwright` — typical caller; consumes the returned URL.
- `a11y-scan`, `visual-check`, `ssr-audit` — also depend on a running server.
- To stop the server: kill the PID recorded in `.claude/dev-server.pid` (if present), then any process still listening on the port (the package manager's child), and clear the file.

## Hard rules

- **Idempotent.** Re-running this skill in a session must never spawn a duplicate server. The port-detection step is non-skippable.
- **Background only.** Never block the agent's main loop on the server's stdout. Tail the log on demand.
- **Never start a server in production / CI mode** — refuse if `NODE_ENV=production` or `CI=true` is set.
- **Project-root verified, and chosen rather than assumed.** Step 0 selects `PROJECT_ROOT`; `package.json` must exist there. No fallback to `cwd`, and no fallback to the workspace root when the target is a member.
- **One member per run.** This skill starts a single dev server. Starting a workspace's whole app set is out of scope — the caller runs it once per member with an explicit `app_target`.

## Halt conditions

- Halt if `package.json` has no `dev` / `start` script and no recognised framework config — do not fabricate a start command.
- Halt if the lockfile + package manager don't match (e.g., `pnpm-lock.yaml` present but `npm` requested). Mirror what the project actually ships with.
- Halt if the port collides with a non-dev process after 3 bumps — stop guessing, surface the conflict.
- Halt if the server exits within 2s of launch — capture the last 30 log lines and refuse to declare success.
- Halt if the PID file is empty or names a dead process after launch — a server nobody can stop is not `started-fresh`.
- Halt `workspace-target-ambiguous` if the repo is a workspace with more than one app member and no `app_target` was given. List the candidates with their dev script, framework and configured port; picking one is the caller's decision, not the run's.
- Halt if the resolved start command is a fan-out runner (`turbo run dev`, `nx run-many`, `pnpm -r dev`, `npm run dev --workspaces`, `concurrently`) — that starts N servers and this skill returns one `url`. Re-resolve to a member first.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.