See every dev server on your machine, and who started it.
PortPilot runs your local dev servers and shows every port, every crash and who started each one. Connect an AI assistant (Claude Code, Cursor, Windsurf, Cline or any MCP client) and it sees the same apps you do, and reuses a running server instead of starting a second copy. It works on its own; the assistant link is optional.
Local-first: no account, no telemetry, no cloud. Windows 10/11 and Linux.
Four surfaces, one config file and one run record. An app you add in one shows up in the others, and a start or crash in one is visible in all of them.
Desktop app. The full view: apps, ports, conflicts, crashes and the History tab. Pick it when you want everything on one screen, or a tray icon that keeps your servers alive after you close the window.
Web portal. The same Apps view in your browser, from npm run agent or from VS Code. It binds 127.0.0.1 only and needs a per-session token. Pick it on a machine where you cannot install the desktop app. The History and Browsers tabs, row previews and crash toasts are not in the portal yet.
VS Code extension. The sidebar tree, a status bar item and a crash toast, without leaving the editor. Running apps come first, stopped ones fold away, and a ✦ marks anything Claude started. Pick it if you live in VS Code. A Browsers view lists your named browser profiles with Start and Stop, and expands a running one to its open tabs.
Claude Code. Install the plugin and Claude sees what you see: a status line, a guard that points a second start at the running server, a crash band with Fix it, and the 26 MCP tools such as find_run. Pick it when Claude is the one starting your servers.
More Claude Code views
Status line, worst state first, ✦ on apps Claude started:
Crash band with Fix it, Restart, Logs and Dismiss:
find_run bringing back a past version of a page, with the literal steps:
The Claude Code images are terminal frames rendered around the plugin's real output on the demo data, not captures of the Claude Code app.
Refresh every image here with docs/demo/tools/ (desktop-shots.mjs, web-shot.mjs, claude-shots.mjs, history-shot.mjs); the demo data is fictional.
/plugin marketplace add m4cd4r4/PortPilot
/plugin install portpilot@portpilot
The MCP server is bundled, so there is no npm install. One install gives Claude:
- Status line. Every session shows what is up, worst state first. A ✦ marks apps Claude started.
⚓ 1 crashed · 7 up · :3000 examworthy-web - Dev-server guard. A start on a busy port is turned away with the URL to reuse. A clean start goes through PortPilot. If the guard can't tell what a command does, it lets it run.
- Crash band with Fix it. When an app Claude started crashes, the session gets a band with Restart, Logs and Fix it. Fix it hands Claude the crash and the stderr tail.
- The PortPilot tools: list, start, stop, scan, kill and group, as the 26 MCP tools.
- A browser per purpose. In the Browsers tab you say what each profile is for (
Google Search Console, Analytics) and which sites it logs into (search.google.com). Ask Claude to "access Google Search Console" and PortPilot hands it the right profile and port (start_browserwithfor); it refuses to guess when two profiles fit, and says so when none does. A headed profile can also keep a saved window position and size, and stay in front, for the times Claude has to click pixels on the screen. - Browsers that do not collide. Several sessions can each have a named browser profile with its own debug port. A profile one session started is leased to it while that session is alive: a second session is told
IN_USEand is steered to a free profile (start_browserwithname: "any"picks one), unless it passestakeover:trueafter telling you.
The plugin does most of this on its own: the dev-server guard routes starts through PortPilot, a skill tells Claude never to start a server with bare Bash, and the start of every conversation lists your browser profiles. The skill is the part Claude decides to load, so for a firm default add this to ~/.claude/CLAUDE.md (all projects) or a project's CLAUDE.md:
## Local servers and browsers: use PortPilot
- Start, stop and check dev servers with the PortPilot MCP tools (list_running, check_port, start_app, add_app), never a bare `npm run dev &` or `kill`. Reuse a running server.
- For any browser automation, call PortPilot `start_browser` with `for` set to the site or URL, and use the `cdpUrl` it returns. Never launch a browser with a debug port or choose a port yourself.
- On NO_MATCH or AMBIGUOUS, ask me; do not fall back to `name: "any"` for a task tied to a site.
- Never kill a port or delete an app unless I ask.
Then label each browser profile in the Browsers tab (what it is for, which sites it logs into), because Claude can only match what you wrote. Other MCP assistants have no plugin, so put the same text in their instructions file.
A debug port is how a program drives a Chromium browser. The browser opens with --remote-debugging-port=<n>, and anything that can reach 127.0.0.1:<n> can then read its tabs and click in them. A browser profile in PortPilot is a named browser with its own sign-ins and its own debug port, so a google profile stays signed in to Search Console while a github profile stays signed in to GitHub. Nothing picks a port by chance: you give each profile one, and PortPilot hands that port to Claude.
-
Start and Stop. In the Browsers tab, or in the VS Code Browsers view, Start opens the profile's own Brave or Chrome on its port and Stop closes it and frees the port.
-
Claude asks by purpose. Label each profile with what it is for and the sites it logs into. Claude calls
start_browserwithforset to the site, gets the matching profile and itscdpUrl, and a session that started a profile holds it until the session ends. -
A blocked port is reported, not worked around. If another program holds a profile's port, the row says which one.
The Claude Code clips on this page are terminal frames rendered around the plugin's real output, not captures of the Claude Code app.
Each row now tells you what is happening and who did it.
Row state, and who started it. Every app shows its state, how long it has been in it and who started it: you, or a Claude Code session. A crashed app reads ✕ Crashed · exit 1. The VS Code tree uses the same states.
Conflict strip. When something else holds an app's port, the row says what holds it and offers Use free port, Kill & start or Show process. Kill asks for a second click.
Crash toast with Ask Claude. A crash raises a sticky toast with the stderr tail. Ask Claude appears when the session that started the app is still open, and sends it the crash. Repeat crashes group into one toast.
History tab. Every run is recorded with the git state it ran from, including uncommitted changes. Search by app, branch, file or SHA, open the page a run served while it is still up, copy its SHA, or pin it so it is never pruned. Re-run this version puts that exact commit and its uncommitted files in a new sibling worktree, installs from the lockfile and starts it on a free port. Claude finds the same runs with find_run.
Browser history. In the Browsers tab, History under a profile lists its past sessions: who started and stopped each, when, and the pages that were open, with secrets in addresses hidden. Open again starts the profile and opens those pages (http and https only, at most 20) in it. A dev-server run a profile had a tab on reads tested in <profile> on its History row.
The History tab also records dev servers PortPilot did not start: any localhost server on this machine (from a terminal, Claude or an editor) gets a run marked detected, with a preview, once it answers HTTP on two scans in a row, and its project folder and start command are kept so Start again can bring it back later. Switch it off under Settings > Run history > Track every localhost server.
Running apps on the Apps tab show a small preview of the page they serve, beside the state. It is the thumbnail the History tab already holds, so no extra capture runs. Hover it, or tab to it, for a larger preview; switch it off under Settings.
The PortPilot MCP server works with Claude Code, Claude Desktop, Cursor, Windsurf, Cline and any other MCP client. Ask in plain language:
"What's running on :3000?"
"Start statewise"
"Kill whatever is on port 8000"
"Start all my favourites"
Setup outside the plugin: mcp-server/README.md.
MCP tools (26)
| Tool | Description |
|---|---|
list_apps |
List all registered apps with running status inline |
get_app |
Get details of a specific app |
get_status |
Get overall PortPilot status summary |
start_app |
Start an app by name or ID |
stop_app |
Stop a running app |
bulk_start |
Start multiple apps at once |
bulk_stop |
Stop multiple apps at once |
add_app |
Register a new app |
add_worktree |
Register a git worktree/branch nested under its parent project (auto-detects branch + parent from git) |
update_app |
Update app configuration |
delete_app |
Remove an app |
list_running |
Show currently running apps |
scan_ports |
Scan for active ports |
check_port |
Check what is running on a specific port |
kill_port |
Kill process on a port |
toggle_favorite |
Star/unstar an app |
delete_all_apps |
Remove all apps (requires confirmation) |
list_groups |
List all app groups |
move_to_group |
Move an app to a different group |
find_run |
Find a past run by text, app, branch or time; returns its git state and literal re-run steps |
list_browser_profiles |
List the named browser profiles: port, cdpUrl, state, purpose, sites, saved window, who is using each, open tabs, free and leasedTo. for (a site, URL or task) returns only the profiles that fit, best first, each with a match.reason |
list_browsers |
List the Chromium-family browsers installed here |
start_browser |
Start the profile for a site or task and get its port and cdpUrl; the caller never picks a port. for (free text or a URL) resolves by the profile's sites, name and purpose: NO_MATCH when none fits, AMBIGUOUS when several do (neither starts anything). name starts an exact profile. name: "any" is a throwaway browser with no logins, never for a task tied to a site. A profile another live session is using is refused (IN_USE); takeover:true overrides after telling the user |
stop_browser |
Stop a profile PortPilot started; never touches a process the profile did not start, and refuses one another live session is using unless takeover:true |
set_browser_mode |
Set a profile's mode for its next start: headed, offscreen or headless |
browser_history |
Past browser sessions, newest first: who started and ended each, when, and the pages (URL and title) that were open. Filters profile, since, limit (default 10, at most 50) |
Browser tools without MCP. The web agent (npm run agent) serves the same six tools as POST /api/browser/<tool> with the arguments as a JSON body and the X-PortPilot-Token header (the token is in agent.json beside the config). The answer is the same JSON the MCP tool returns: { success: true, name, port, cdpUrl, state, ... }, or { success: false, code, error, action }. Who started a profile is shown as a claim. A caller that names its session (sessionId) is held to the lease described below; a caller with no sessionId is never refused.
Purpose, sites and routing. A profile may carry purpose (a short phrase, at most 200 characters) and sites (hostnames such as search.google.com, at most 20, lowercase, no scheme or path). for is scored against them: a site match (the host, or a subdomain of a listed site) is the strongest signal, then the words of the purpose, the profile name, the note and the start URL's host. Only the profile's own words count: PortPilot knows nothing about any site, so write gsc into the purpose if you want to say "gsc". If the best two scores are within 25% of each other the answer is AMBIGUOUS with both names. At the start of every Claude Code conversation the plugin adds a short note listing the profiles (name, port, mode, purpose, sites; at most 25 profiles and 2 KB, never a folder path, a URL query or a long note), so Claude already knows to call start_browser with for. Nothing is added when you have no profiles.
Window placement and keep in front. Edit a profile and open Window: left, top, width, height and Keep in front. Capture current position reads the running browser's real window and fills the fields, so you place it by hand once. When a headed profile starts, PortPilot sets those bounds (Chrome DevTools Protocol Browser.setWindowBounds), reads them back, and reports window: { requested, actual, applied } in the start result. Keep in front also raises the page (Page.bringToFront) after the start and on every start_browser that reuses the running browser (which also puts the window back where it was saved), and, on Windows only and as best effort, makes the window always-on-top (SetWindowPos with HWND_TOPMOST; a failure is logged and reported as topmost: false, never an error). Other systems get the position, size and bring-to-front only. Offscreen and headless profiles ignore the window. Duplicate does not copy purpose, sites or window, so two profiles never both claim one site.
Browser sessions and leases. A profile started by Claude session A is leased to A while A's heartbeat (sessions/<id>.json, written every 15 s by the plugin) is under 45 s old. Session B then gets { success: false, code: "IN_USE", error, action } naming the owner (short id) and when it started; list_browser_profiles shows leasedTo and free on every row, and start_browser with name: "any" (or no name and free: true) starts a free profile, or returns the one the session already holds. takeover:true overrides, for start_browser and stop_browser, and should follow telling the user. A lease whose owner has gone quiet is free again. Every start and stop is also kept in history/browser-runs.json (500 entries by default) with the tabs that were open, sampled about every 30 s and again at stop. Addresses are stored with secrets hidden: userinfo is dropped and the value of any query or fragment parameter that looks like a secret (token, key, code, auth, session, password) becomes ***.
Manual setup for Claude Code without the plugin:
cd mcp-server && npm install && cd ..
claude mcp add portpilot -- node "/path/to/PortPilot/mcp-server/index.js"
claude mcp list # portpilot: ... - ✓ ConnectedRun and organise
- Start and stop apps with port detection and fallback ranges
- Groups with colours, favourites, search and sort
- Branches and worktrees nest under their project, colour-matched to VS Code (Peacock)
- Detail drawer: command, folder, PID, uptime
- Port reservation, health checks and Start all / Stop all per group
Ports
- Scan every TCP port with process, PID, memory and uptime
- Kill a stuck port in one click
- Grouped into Dev, Other and System
- Bind type and IPv4/IPv6 on each port
Auto-detect
- Point it at a folder to find Node.js, Python, Go, .NET, Rust, Ruby and Docker projects (how detection works)
- Add a repo's worktrees in one go; stale ones are flagged
- Docker status badges, and Docker Desktop in one click
Everywhere
- VS Code extension: sidebar tree and status bar
- Web portal in your browser, loopback-only (see Web agent)
- Tray menu with a Stop per running app
- 6 themes plus Auto: Light, TokyoNight, Nord, Dracula, Glass
- Share an app's LAN URL to your phone by QR code
| Platform | Download |
|---|---|
| Windows installer | PortPilot-4.0.4-x64.exe (113 MB) |
| Windows portable | PortPilot-4.0.4-portable.exe (113 MB) |
| Linux AppImage | PortPilot-4.0.4-x86_64.AppImage (123 MB) |
| Debian / Ubuntu | PortPilot-4.0.4-amd64.deb (86 MB) |
macOS: build from source; it is supported but not officially tested. Install it, click Scan, then add your projects. Older builds are on Releases.
git clone https://github.com/m4cd4r4/PortPilot.git
cd PortPilot
npm install
npm startAdding apps and handling conflicts
Auto-detect (recommended). Click Add App, then Browse & Auto-detect Project, and pick the project folder. PortPilot fills in the name from package.json, the command with the right package manager (pnpm run dev, yarn dev, npm run dev), the working directory and the preferred port from config files. Review, then Save.
Manual entry. Click Add App and fill in Name, Command (for example npm run dev), Working Directory, Preferred Port, and an optional Fallback Range (for example 3001-3010).
Port conflicts. When another process holds an app's port, the row shows a conflict strip with Use free port, Kill & start and Show process. See New in the desktop app.
Badges
| Badge | Meaning | Detected when |
|---|---|---|
| 🐳 | Docker app | Command includes docker or compose |
| 📦 | Node.js app | Command includes npm, npx, pnpm, yarn, or bun |
| 🐍 | Python app | Command includes python, uvicorn, flask, or django |
| 🗄️ | Database | Command includes postgres, mysql, redis, or mongo |
| ⚡ | Auto-start | App configured to start on launch |
| 🌐 | Remote | App runs on a remote server/VPS |
A pulsing yellow 🐳 means Docker Desktop is not running (click to start it); green means it is ready. Running apps show v4 or v6 for the IP protocol they are bound to, so the browser button opens the right URL.
Keyboard shortcuts
| Shortcut | Action |
|---|---|
Ctrl+R |
Refresh/scan ports |
Ctrl+N |
Add new app |
Ctrl+F |
Focus global search |
Ctrl+G |
New group |
Escape |
Close modal / Settings panel |
Config file and example
- Windows:
%APPDATA%/portpilot/portpilot-config.json - macOS:
~/Library/Application Support/portpilot/portpilot-config.json - Linux:
~/.config/portpilot/portpilot-config.json
{
"apps": [
{
"id": "app_harbor_web",
"name": "harbor-web",
"command": "npm run dev",
"cwd": "/home/me/dev/harbor-web",
"preferredPort": 3000,
"fallbackRange": [3001, 3010],
"color": "#84CC16",
"autoStart": false
}
],
"settings": {
"autoScan": true,
"scanInterval": 5000,
"openDevTools": false
}
}Web agent (preview)
PortPilot can run as a local web app: the same UI in your browser, backed by a hardened loopback agent.
npm run agent
# http://127.0.0.1:7317/Because the backend can start and kill processes, it is locked down:
- Binds to
127.0.0.1only - A per-session token on every API call
- Host-header validation (defeats DNS rebinding) and Origin/CORS lockdown
- A custom header forces a CORS preflight, blocking cross-site requests
- Strict CSP; token file written
chmod 600
Full threat model: SECURITY.md. Run either the desktop app or the agent, not both at once: they track started processes separately.
Development and testing
npm install
npm start # run the app
npm run dev # dev mode (DevTools if enabled)
npm test # Playwright E2E smoke suite (Electron + test servers on 3000/3001/8080)
xvfb-run -a npm test # headless Linux
npm run screenshots # UI screenshots
npm run build # Windows (NSIS installer)
npm run build:linux # Linux (AppImage + .deb)
npm run build:all-platforms # bothThe smoke suite covers window launch, port scanning, test-server detection, port-card rendering, global search, copy/kill controls, the Settings panel and the Add App modal. The older v1.3/v1.7/groups specs target the pre-2.0 tab UI and are kept for reference only.
Launching from VS Code or Claude Code: launch.js clears ELECTRON_RUN_AS_NODE for you.
Stack: Electron, Node.js, vanilla JS, CSS variables for themes, Playwright for tests, netstat (Windows) / lsof (macOS, Linux) for port scanning.
What changed in each version: CHANGELOG.md and GitHub Releases.
Pull requests are welcome. Bugs and ideas: open an issue.
MIT © Macdara
















