Know which coding agent owns a local dev server — and stop the wrong one from being killed.
English | 简体中文
PortMarshal is an agent-aware ownership and safety layer for local development services on macOS and Linux. It maps attributable TCP listeners to their PID, project directory, and launching agent; coordinates sticky port claims; detects port drift; and blocks one agent from stopping another agent's active service by default.
npm install -g portmarshal
portmarshal list
portmarshal whois 3000
The last command inspects the process, project, and agent behind a dev server already listening on port 3000.
Parallel coding agents create three recurring problems:
Silent drift— an agent expects port 3000, but the framework silently starts on 3001.** Detached services**— a session exits while its dev server keeps listening.** Friendly fire**— one agent frees a port by stopping another agent's service.
PortMarshal scans first and coordinates second. Existing listeners do not need to be launched through PortMarshal to be discovered. Cooperative agents gain stable claims and stronger ownership signals, while uncooperative services still appear when the operating system exposes their process metadata.
npm install -g portmarshal
portmarshal --help
Requires Node.js 18.17 or newer. The runtime has no npm dependencies.
npm uninstall -g @worsher/portscout @worsher/portmarshal
npm install -g portmarshal
On first use, PortMarshal copies an existing ~/.portscout/registry.json
into ~/.portmarshal/registry.json
, preserving sticky claims without deleting the old data.
| Command | What it does |
|---|---|
portmarshal list [--json] [--all] [--project .] |
|
| List listeners with project, source, and state: active, reserved, unregistered, or drift | |
portmarshal whois <port> [--json] |
|
| Inspect one port: PID, project directory, full command, agent or service source | |
portmarshal claim <name> [--prefer N] [--range A-B] |
|
| Allocate a cooperative sticky port claim; stdout contains only the port number | |
portmarshal release <name> |
|
| Release a claim without stopping its process | |
| `portmarshal stop <port | name> [--force |
| Stop a service behind the ownership guard | |
portmarshal gc [--kill-detached] |
|
| Reap stale claims and review or stop detached service candidates | |
portmarshal watch |
|
Refreshing terminal dashboard; press q to quit |
|
portmarshal menubar [--install] |
|
| SwiftBar menu with guarded click-to-stop actions |
Typical agent startup:
PORT=$(portmarshal claim web --prefer 3000)
npm run dev -- --port "$PORT"
A claim is a cooperative lease, not an operating-system socket reservation. PortMarshal revalidates a previous claim before reusing it: a port must still be free or be attributable to the same project. There is still an unavoidable handoff window between returning a free port and the application binding it.
PortMarshal follows the process parent chain to identify claude-code
, cursor
, antigravity
, vscode/electron
, terminal
, docker
, and pm2
. PM2-managed listeners are enriched from pm2 jlist
, displayed as pm2:<app-name>
, and attributed to the application's configured cwd; the full PM2 environment is never retained. For published Docker ports, PortMarshal inspects running-container metadata: shared Docker Desktop listeners are split by container, the source is shown as docker:<compose-project>/<service>
, and the host project directory is recovered from Compose, Dev Container, or bind-mount metadata. If managed-runtime metadata is unavailable, attribution safely falls back without inventing ownership. PortMarshal also recognizes launchd:<label>
on macOS and systemd:<unit>
on Linux. A process reparented to PID 1 without a recognized manager is labeled detached
— this is a review signal, not proof that the process is abandoned.
| Target | Default stop behavior |
|---|---|
| PM2 application owned by the caller's project/claim | Run pm2 stop <id> ; never signal a managed child that PM2 would restart |
| Docker container owned by the caller's project/claim | Run docker stop for that container; never signal the shared Docker backend |
| Detached service or a service owned by the caller's project/claim | Stop with SIGTERM, then SIGKILL after 3 seconds if needed |
| Another active service | Block, print attribution, and exit with code 3 |
--force
overrides the guard after review. On macOS, --gui
asks through a native confirmation dialog.
PortMarshal can only attribute listeners whose process metadata is visible to the current user. For example, Linux ss
output without PID information is not invented or guessed; those rows are omitted.
Add this policy to AGENTS.md
, CLAUDE.md
, or your editor's agent rules:
- Before starting a dev server, get a port with `PORT=$(portmarshal claim <service> --prefer <default>)`.
- Diagnose conflicts with `portmarshal list --project . --json` and `portmarshal whois <port> --json`.
- Stop services with `portmarshal stop <port>`; exit code 3 means another active service owns it, so show the attribution and ask before using --force.
A ready-to-copy Claude Code skill lives in integrations/claude-code/skills/portmarshal.
andlsof
ss
expose sockets and processes; PortMarshal adds project/agent attribution, claims, drift detection, and stop policy.Sonaris a broad localhost and Docker management CLI; PortMarshal focuses on cross-agent ownership and guarded actions.Portlesslaunches apps behind stable named local URLs; PortMarshal can inspect services whether or not it launched them. The tools can be used together.
pnpm test
pnpm smoke
pnpm build
GitHub Actions runs build, unit tests, and a real listener smoke test on macOS and Linux. Tagged releases publish to npm with provenance.
Design: docs/specs/2026-07-16-portmarshal-design.md ·
macOS and Linux · Node.js ≥ 18.17 · zero runtime dependencies · MIT