Repository navigation
v2.0.0
What's new in v2.0.0
The quokka release: a new look, a Workbench window, port leases, a setup
wizard for AI tools, and a guard that knows whose server it is. Everything
since 1.16, reviewed twice.
Added
- Settings > Agents. The AI tools this Mac has and how each is
recognised, the guard's one switch ("Refuse agents a server nobody
claims", followed by the CLI and the MCP server too), a project setup
with a click per step (whatportkilla setupdoes: rule files, the MCP
registration for Claude Code), and leases: the default length
portkilla reserveuses and a Release button for each active lease. - Notifications per event. A watched port freeing up, something taking
one, a guard acting, and an agent being refused each have a switch under
the master. - What the list shows. Settings > Display can hide UDP sockets and
ephemeral ports (49152 and up). - Updates. The daily check is a switch, and "Include beta releases"
offers prereleases, which rank below the release they precede. portkilla setupwalks through setting up the AI tools on this Mac:
checksportkillaon PATH, and for each tool found offers to register
the MCP server with Claude Code (claude mcp add), write its rule file
into the project, and shows the rest (Cursor's mcp.json, Codex's
config.toml, shell completions). Asks before every change;--yes
applies all; without a terminal it prints the plan.- Rule files for every tool.
portkilla agent-docs --cursorwrites
.cursor/rules/portkilla.mdc(with frontmatter),--windsurfwrites
.windsurf/rules/portkilla.md,--codexwrites AGENTS.md,--claude
CLAUDE.md; folders are created, existing files keep their content. - A Claude Code plugin in
plugins/portkilla: the MCP server, the lsof
hook, aportkillaskill, and/portkilla:portsand/portkilla:free
commands. Install withclaude plugin marketplace add mukes555/PortKilla
thenclaude plugin install portkilla@portkilla. - Leases on free ports.
portkilla reserve <port> [--for 10m] [--reason ...]takes a lease thatfree-portandexecskip for
everyone else and that makeskillrefuse other agents on the port
(people are warned) until it expires (a day at most) orportkilla release <port>gives it back.portkilla reservationslists them; the
Workbench watchlist shows them with a Release button, and a listener on
a leased port wears a "reserved" chip. MCP:reserve_port,
release_port, and afree_porttool. - Drift. PortKilla reads the port a project meant to use (PORT in
.envfiles,--port/PORT=in package.json scripts,port:in
vite.config) and marks a web server that ended up elsewhere with an
"expected :3000" chip; the inspector andwhoissay who holds the port
it wanted.portkilla driftlists every such server. portkilla exec.portkilla exec --free-port --prefer 3000 -- npm run devpicks a free port nobody has leased, exportsPORT, leases the
port for the run, exportsPORTKILLA_OWNERandPORTKILLA_SESSION
(yours unless given) so the server is attributed even when the tool
leaves no marker, forwards signals, and exits with the command's status.
--port Ninsists on one port and explains who has it when it is busy.- The Workbench. A full-size window (overflow menu, or type
> workbench)
with a sidebar of views: a sortable table of every port; ports grouped by
project with Reveal, Open in editor, and Kill all; ports grouped by agent
session (live, ended, editor terminals, unclaimed) with Stop all and a
one-click clean-up of ended sessions; the watchlist with guard and watch
toggles and a field to add a port; and History. An inspector on the right
shows the selected port's overview, the evidence behind its owner (the
ancestry walked, the markers found), and its history, with Kill, Force,
Tree, Open, Watch, and Guard at hand. - A command palette in the search field. Type
kill 3000,open 5173,
watch 8080,guard 8080, orfree portand Return does it; the strip
under the field says what will happen, including who owns the target and
whether a supervisor is involved.>lists app commands (Refresh, Bulk
Kill, Pin, Workbench, History, Find a Free Port, Settings, Quit). - Rows slide in and fade out as ports come and go.
- Sparklines. The Workbench keeps the last sixty scans of CPU and
memory per process: a trend column in the table, and CPU and memory
charts in the inspector. - Who is connected. The inspector's Connections tab lists the remote
end of every established connection, marked local, from the local
network, or from elsewhere;portkilla whoisprints the same. - Peek at a web server. A Peek button in the inspector sends one GET to
the local server and shows its status, content type, and page title. A
Settings toggle (off by default) does it automatically for web ports. - A welcome tour on first launch: what the app shows, how agents are
kept from killing each other, and how to set the agents up (with the
commands to copy). Later from the menu or> tour. - The quokka. A new app icon, and the mascot in the welcome tour (waving
on the first page, on guard on the second) and in the empty state (asleep
on the tablet).scripts/make_artwork.swiftturns the artwork into the
icon and the cut-outs. - Supervisors understood. PortKilla recognises what would undo a plain
kill: pm2 apps, launchd jobs (Homebrew services and your own
LaunchAgents), Docker containers, and reloaders (nodemon,next dev,
uvicorn --reload,--watchmodes, Flask's and Django's reloaders,
gunicorn, puma, and nginx masters). Rows show an orange chip and the
detail view says why.portkilla killstops a reloader together with its
child and runspm2 stop,brew services stop,launchctl bootout, or
docker stopfor the rest; when the tool is not on PATH it prints the
command and exits 6.--forcekills the listener itself, except for
Docker, where it meansdocker kill. The app asks "Stop via pm2" or
"Kill anyway". Docker's backend is never killed through a port. - Refusals reach you. When the guard refuses an agent, the running app
shows a notification with "Stop it anyway" and "Show in PortKilla", the
History window lists the refusal in orange with who was refused, and
portkilla historyincludes it. portkilla whoisshows the supervisor and its stop command;list --json
carriesmanagedBy.portkilla whois <port>(or--pid): everything PortKilla knows about
a listener, including the evidence behind its owner (the ancestry walked,
the markers found, what was declared) and whatkillwould do for the
caller and why. Also an MCP tool,whois_port.portkilla kill --orphanedstops every server left behind by an
agent session that has ended, for any agent; exit 0 when there is
nothing to clean up.portkilla doctor --agentsprints the agent compatibility matrix
checked against this machine: which tools run or are installed, how each
is recognised, how precisely its sessions are told apart, and where each
fact came from. The same matrix is in docs/AGENTS.md, kept in step by a
test.PORTKILLA_SESSION: tools that export no session id (Codex, Gemini,
custom bots) can export any unique string next toPORTKILLA_OWNER, and
their servers stay tied to that session after reparenting.- A refusal now says when the server runs in the caller's working
directory: a hint that it may be the caller's own unclaimed server, or
the user's. portkilla schema whoisandschema agentsdocument the new outputs.- Connected clients. Each listener shows how many established
connections it has (a chip in the row, a Clients line in the detail
view,connectionsinlist --json), and killing a server with live
clients always confirms first. portkilla free-port [--prefer N] [--range A-B] [--json]prints the
first port that is neither listening nor bound (a real bind probe),
starting from the preferred one. Exit 1 when the range is full.portkilla schema <command>documents every field of that command's
--jsonoutput, so scripts and agents do not have to guess.- Agents no longer kill what nobody claims. An identified agent that
asks to stop a server PortKilla cannot attribute is refused (exit 3,
guardVerdict: "refused", reason "not attributed"); it should start its
own servers withPORTKILLA_OWNERset or ask the person. People at a
plain terminal and the GUI are unaffected;--forcestill overrides.
Commands from an editor terminal count as a person's, except against
another agent's running server, where they need--force. - Scenario tests spawn real servers (
portkilla __serve <port>, debug
builds only) and drive the guard through the CLI as a separate process:
attribution by declaration and by environment after reparenting,
refusals, overrides, real kills,free-port,whoami. portkilla mcpannounces itself on stderr when run from a terminal
(it used to wait in silence), andportkilla mcp --setup [claude|cursor| codex]prints the registration for each agent. There is no background
mode by design: each agent starts its own copy over stdin/stdout when it
needs one and stops it afterwards.
Changed
- The quokka is the brand. The menu bar shows the quokka traced in
black and white from the artwork (lighter while nothing is listening)
or, from Settings > Display, the app icon in colour; the bolt is gone.
The popover header carries the artwork's head with a live summary line,
About shows the app icon, and the Workbench sidebar and empty inspector
wear it too. - A bigger popover. Regular is now 580 by 720 points; Settings >
Display offers Compact (the old 500 by 600) and Large (660 by 840). The
pinned window follows. - Richer rows. Type tiles, section headers with counts and accents,
port and process names a size up, memory with CPU and its trend in
Advanced, and the browser and watch verbs on hover so the list stays
calm. Guard buttons use a shield. - Debug snapshot renders keep their density and watch list in the
throwaway suite instead of the developer's own preferences. - The core is a library.
PortKillaCore(models, scanners, guard, CLI,
MCP; Foundation only) sits under three targets: the menu-bar app, the new
standaloneportkillaexecutable, and the tests. The CLI no longer
links AppKit, so it starts faster and uses less memory; the app binary
still answers the same subcommands. - A declared
PORTKILLA_OWNERnow wins over process ancestry for both the
caller and the target, so a bot started from inside another agent's
session keeps its own name. portkilla kill --dry-runmentions connected clients, andagent-docs
explains the new refusal and points atfree-port.- Row text uses relative text styles (
body,subheadline,caption)
instead of fixed point sizes, and the column widths areScaledMetric,
so the list follows whatever text scaling the system applies rather than
clipping. macOS applies little of it to SwiftUI text today; the snapshot
hook'sPORTKILLA_SNAPSHOT_TEXTSIZE=largeexists for when it does. - Rows no longer observe the whole
PortManager: the section hands each
row plain values (density, protected, watched, terminating), so a
publish re-evaluates only the rows whose inputs changed. - The demo-GIF hook renders against a throwaway preference suite instead
of the real one.
Fixed
- The watchlist no longer disarms guards by looking at them. Rendering
the Workbench's watchlist counted as a guard strike, so a guard stood
down on its next intrusion after a few seconds on screen. - Bulk kills leave supervised servers alone. Kill All and the bulk
sheet used to send a plain kill to pm2, launchd, and reloader servers,
which came straight back; they are skipped with a note, and each row's
own stop verb applies. - Every kill dialog carries the owner, client, and lease warnings,
including the supervised ones, which showed none. - A reloader kill names what goes with it. Stopping nodemon (or another
reloader) takes every server under it; those are now judged by the
guard, listed in the dry run, and recorded in History. - Leases survive two agents at once. The lease store takes an advisory
lock around every read-modify-write, so twoexec --free-portcalls at
the same instant cannot both win a port. - Client counts refresh the row. A change in connected clients
republishes the row and its warnings. - Smaller:
--helpafter--belongs to the commandexecruns;
historyshows kills by default and refusals with--all, so its first
entry always names the killer; the docs installer refuses garbled
markers; the MCP kill tool flags every outcome that leaves the port
busy; the inspector's peek reports a redirect without following it and
stops at 64 KB; debug snapshot renders keep their density and watch
list out of real preferences.
Security
- A name alone claims nothing. A declared
PORTKILLA_OWNERwithout a
session could stop another agent's server, or release its lease, by
matching the name. A known session now has to be shown, with
PORTKILLA_SESSIONor by running from the agent's own shell. - Refusal notifications are believed only when the CLI recorded them.
Any local process could post one, and its "Stop it anyway" killed the
port with no confirmation. Labels are cleaned and the kill goes through
the usual confirmation. - Leases are held by a session, not a name.
- pm2 app names are validated before they become arguments;
pm2 stop allwas reachable from a process name. - Command lines are redacted before they are shown, exported, written to
History, or handed to an agent:--token=...,--api-key ...,
DATABASE_PASSWORD=..., passwords inside URLs, and bearer tokens become
[redacted]. Classification and project detection still see the original.
Distribution
scripts/build.shbundles the standalone CLI as
PortKilla.app/Contents/Helpers/portkilla, and the cask'sbinarystanza
links that instead of the app binary. CI builds before testing so the
scenario tests find the debugportkilla.- The release workflow can pin the Homebrew cask to each release with its
SHA-256 (packaging/homebrew/portkilla.rb.tmpl,livecheck,
brew upgradesupport). It runs only when aHOMEBREW_TAP_TOKENsecret
exists; see RELEASING.md. - README leads with a current screenshot and shows the Workbench; the demo
GIF is regenerated.
📦 Install
Download PortKilla-2.0.0.dmg below, open it, and drag PortKilla to
Applications. Universal binary — runs natively on Apple Silicon and Intel;
requires macOS 13 (Ventura) or newer.
Not notarized (no Apple Developer account). First launch on macOS 15 or
newer: open it once, then System Settings → Privacy & Security → Open
Anyway. On macOS 13 and 14: right-click PortKilla.app → Open → Open.
Either way,xattr -dr com.apple.quarantine /Applications/PortKilla.app
skips the dialog. Homebrew (brew install --cask portkilla) does this for you.
What's Changed
- Leftovers: text styles, row inputs, pinned cask job, fresh assets by @mukes555 in #25
- mcp: announce on a terminal; --setup prints each agent's registration by @mukes555 in #26
- Phase A: core library, standalone CLI, connection counts, redaction, guard rules by @mukes555 in #27
- Phase B1: whois, orphan sweep, agent compatibility matrix, declared sessions by @mukes555 in #28
- Phase B2: managed runtimes with real stop verbs, refusals as notifications by @mukes555 in #29
- UI-1: Workbench window, palette in the search field, row transitions by @mukes555 in #30
- UI-2: sparklines, connections with peers, HTTP peek, welcome tour, mascot by @mukes555 in #31
- C1: leases on free ports, and portkilla exec by @mukes555 in #32
- C2: drift detection by @mukes555 in #33
- E1: setup wizard, rule files for every tool, a Claude Code plugin by @mukes555 in #34
- Assets: screenshots and demo for the new UI by @mukes555 in #35
- Artwork: the quokka icon and mascot by @mukes555 in #36
- Fix the pre-2.0 review findings by @mukes555 in #37
- UI-3: quokka branding, a bigger popover, richer rows by @mukes555 in #38
- The head crop follows the outline; the traced quokka leads the menu bar by @mukes555 in #39
- README: the quokka edition by @mukes555 in #40
- Settings for 2.0: Agents, notifications per event, list filters, updates by @mukes555 in #41
- Review round two: the findings from five angles by @mukes555 in #42
- Release 2.0.0 by @mukes555 in #43
Full Changelog: v1.16.0...v2.0.0