raskrebs/sonar: grouping localhost ports by repository instead of by PID
CLI tool for inspecting and managing services listening on localhost ports
At a glance
- What is it?
- Sonar is a Go CLI that lists what is listening on localhost, assigns each port to a repository-level group and a named service, and can start, tail and stop that whole group as one unit. It is useful if you regularly lose track of which of six dev servers owns port 8000, and less useful if you only ever run one.
- Who is it for?
- Adopt sonar if your working day involves several repositories each running two or more processes, and you want `sonar kill -g <group>` to replace hunting PIDs by hand. Skip it if you run a single server at a time, or if your team cannot commit a .sonar.yaml, because the automatic grouping then depends on everyone's checkout layout matching.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The problem is port ownership, not port discovery
Finding out what listens on a port is a solved problem on every platform. The README's framing is narrower: every port belongs to a group, normally the repository it was started from, and inside that group to a named service. That second layer is the actual product. A raw scan tells you that PID 41233 is bound to 8000; it does not tell you that PID 41233 is the api service of my-app, that my-app also owns 5432 and 5173, and that all three should die together. Sonar's target user is someone with several checkouts open, each running a database, a backend and a frontend, where the failure mode is killing the wrong process or leaving an orphaned watcher behind. The README is explicit that the tool is about ordering rather than detection: "Sonar shows everything listening on localhost and puts it in order."
How grouping is inferred when you configure nothing
The default group resolution is a fallback chain, and it is worth reading closely because it determines how much setup you need. `sonar start` takes `--group` if given; otherwise it reads the `name` field from the nearest `.sonar.yaml`; otherwise it uses the git root's directory name, with the documented exception that a worktree becomes `repo@worktree`; otherwise it falls back to the name of the current directory. Service names follow a similar chain: `--name`, then a `.sonar.yaml` service whose `cmd` matches, then inference from the command itself. The README lists the inference examples directly: `npm run dev` becomes `dev`, `uv run api` becomes `api`, `python -m uvicorn` becomes `uvicorn`, and `./dev.sh` becomes `dev.sh`. The port argument is deliberately not a binding. `--port` is described as a hint, and the run shows as `starting` until the port is genuinely listening, at which point the daemon uses the hint to match process to port. That distinction matters for slow starters: a server that takes eight seconds to bind is not reported as running during those eight seconds.
Process supervision: process groups, inherited streams, exit codes
`sonar start` is a wrapper, and the README is specific about what the child receives. It inherits stdin, stdout, stderr, cwd and the environment, plus three added variables: `SONAR_GROUP`, `SONAR_NAME` and `SONAR_RUN_ID`. It is placed in its own process group, which is the mechanism behind the claim that `sonar kill` takes down the whole tree including watchers and workers. Ctrl+C is forwarded, and sonar exits with the child's exit code, so a script that checks `$?` after `sonar start -- npm run dev` sees the server's status rather than the wrapper's. With `--detach` the command returns immediately and output goes to `~/.config/sonar/logs/<group>/<name>.log`. `sonar start --list` shows what sonar started, and `--json` gives the machine-readable form. The README's own example script backgrounds three `sonar start` invocations and waits, which is the intended shape for a dev.sh.
The .sonar.yaml file is where the ordering becomes declarative
Without a config file sonar still groups by git root, so the file is optional. What it adds is a committed description of the project: a `name`, a list of `services` each with `name`, `cmd`, `port` and optionally `cwd`, `health`, `description`, `icon`, `color` and `depends_on`, plus a top-level `ports` list for ports that belong to the project without a service. The README's example shows a three-service app where `api` declares `health: /healthz` and `depends_on: [db]`, and `frontend` depends on `api`. Two constraints are stated plainly: the group `name` may contain no slashes and no whitespace, and `cwd` is relative to the file and may not escape its directory. The `depends_on` and `health` keys imply an ordering and readiness model for `sonar up`, but the supplied README is truncated mid-sentence inside the `.sonar.yaml` bullet list, so the exact semantics of `sonar up` are not something I can describe from this material. Treat that as unverified until you read the repository docs.
Install routes and the Homebrew trust step
There are four install paths in the README. Homebrew: `brew install raskrebs/sonar/sonar`, with the documented caveat that Homebrew 6 refuses formulae from third-party taps until you run `brew trust raskrebs/sonar` once. A shell script: `curl -sfL https://raw.githubusercontent.com/raskrebs/sonar/main/scripts/install.sh | bash`, which installs to `~/.local/bin` and edits PATH if needed, with `SONAR_INSTALL_DIR` and `SONAR_VERSION` as environment overrides. A PowerShell equivalent for Windows via `irm ... | iex`, also honouring `SONAR_VERSION`. And `go install github.com/raskrebs/sonar@latest` for anyone with a Go toolchain. Shell completions exist for zsh, bash and fish, and the README notes they tab-complete port numbers, which is a small but real usability detail given that port numbers are the tool's primary key. The curl-to-bash pattern is a legitimate criticism of the install script route; the Homebrew and `go install` paths avoid it.
Where the model breaks: hidden listeners, remote scans and stale hints
The grouping model has a blind spot that the README itself surfaces. Desktop apps and system services that happen to listen, including Figma, Discord, Spotify, ControlCenter, macOS `.app` bundles and `/System/Library/` daemons, are hidden unless you pass `-a`. That is a sensible default, but it means `sonar list` is not a complete picture of the machine, and a port conflict caused by a desktop app will not appear in the tree. The `--port` hint is a second weak point: it is used to match a process to a port, so passing the wrong port does not fail loudly, it produces a mismatch that only becomes visible when the run never leaves `starting`. And `--host user@server` scans a remote machine over SSH, which presumably loses the local repository context that makes grouping work, though the README does not describe how groups are resolved remotely. That is a gap in the documentation rather than a confirmed defect, but it is the kind of thing to test before relying on it.
Compared with lsof, ss and docker ps
The obvious alternative is the platform tooling you already have: `lsof -iTCP -sTCP:LISTEN -P -n` on macOS, `ss -ltnp` on Linux, `docker ps` for containers, plus a shell alias to kill by port. Those tools answer the discovery question completely and have no install step, no daemon, no config file and no opinion about repositories. The difference in approach is that they report kernel state, while sonar maintains a model: runs it started, names it assigned, groups it inferred. That model is what lets `sonar kill -g my-app` work, and it is also the cost, because the model can disagree with reality. If you start a server outside sonar and the inference guesses the wrong group, the tree is wrong in a way `lsof` never is. Sonar is a layer on top of the same underlying data, and it earns its place only if you actually use the group operations.
Maintenance cost and licence position
The release cadence visible in the metadata is three releases in the four days before 2026-09-10, which suggests active development but also frequent churn for a tool you would put in a dev.sh. Pinning is cheap: `SONAR_VERSION=vX.Y.Z` works for both the shell and PowerShell installers, and `go install` accepts a version suffix. The `.sonar.yaml` file is intended to be committed, so schema changes to that file are the upgrade risk to watch, since a breaking change there would require edits in every repository that adopted it. The project is MIT licensed, which permits commercial and internal use with the usual requirement to preserve the copyright notice and licence text; this is a summary of the identifier, not legal advice, and the LICENSE file in the repository is the authoritative text. No homepage is listed, so the README is the primary documentation surface.
Editorial conclusion
Adopt sonar if your working day involves several repositories each running two or more processes, and you want `sonar kill -g <group>` to replace hunting PIDs by hand. Skip it if you run a single server at a time, or if your team cannot commit a .sonar.yaml, because the automatic grouping then depends on everyone's checkout layout matching. Before rolling it out, verify two things on your own machine: that `sonar list --tree` attributes your hand-started processes to the right group without a config file, and that `sonar start --detach` writes logs where you expect under ~/.config/sonar/logs/<group>/<name>.log.
Community notes