
CLI tool for inspecting and managing services listening on localhost ports
██████ ███ ██████ ██ ██████
██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ ██ ██ ████
██ ██ ██ ██ ██ ██ ██ ██
██████ ███ ██ ██ ██ ██ ██ ██
Know what's running on your machine.
Sonar shows everything listening on localhost and puts it in order: every port
belongs to a group — normally the repository it was started from — and
inside that group to a named service. Start your dev servers with
sonar start and the whole project becomes one thing you can list as a tree,
wait for, tail, and stop with a single command. Docker containers, Compose
projects and processes you started by hand are picked up too, without any
configuration.
$ sonar list --tree
my-app (3 ports, running) ~/code/my-app
├─ 5432 db postgres:17 http://localhost:5432
├─ 5173 frontend vite (v5.4) http://localhost:5173
└─ 8000 api uvicorn app:app http://localhost:8000
ungrouped (1 port)
└─ 3000 next-server (v16.1.6) http://localhost:3000
brew install raskrebs/sonar/sonar
Homebrew 6 refuses formulae from third-party taps until you trust the tap once
(Error: Refusing to load formula raskrebs/sonar/sonar from untrusted tap):
brew trust raskrebs/sonar
curl -sfL https://raw.githubusercontent.com/raskrebs/sonar/main/scripts/install.sh | bash
Downloads the latest binary to ~/.local/bin and adds it to your PATH if needed. Restart your terminal or source ~/.zshrc.
On Windows (PowerShell):
irm https://raw.githubusercontent.com/raskrebs/sonar/main/scripts/install.ps1 | iex
Custom install location:
curl -sfL https://raw.githubusercontent.com/raskrebs/sonar/main/scripts/install.sh | SONAR_INSTALL_DIR=/usr/local/bin bash
Install a specific version:
curl -sfL https://raw.githubusercontent.com/raskrebs/sonar/main/scripts/install.sh | SONAR_VERSION=vX.Y.Z bash
$env:SONAR_VERSION="vX.Y.Z"; irm https://raw.githubusercontent.com/raskrebs/sonar/main/scripts/install.ps1 | iex
go install github.com/raskrebs/sonar@latest
Shell completions (tab-complete port numbers):
sonar completion zsh > "${fpath[1]}/_sonar" # zsh
sonar completion bash > /etc/bash_completion.d/sonar # bash
sonar completion fish | source # fish
Describe how your project runs in a sonar.yaml at the repository root, in
place of a dev.sh:
name: my-app
services:
- name: db
cmd: docker compose up db
port: 5432
- name: api
cmd: uv run uvicorn app:app --port ${port}
port: auto
depends_on: [db]
- name: frontend
cmd: npm run dev -- --port ${port} --strictPort
port: auto
depends_on: [api]
env:
VITE_API_URL: ${api.url}
Then start it:
sonar start
✓ db http://localhost:5432 pid 41022 ~/.config/sonar/logs/my-app/db.log
✓ api http://localhost:21408 pid 41040 ~/.config/sonar/logs/my-app/api.log
✓ frontend http://localhost:21409 pid 41077 ~/.config/sonar/logs/my-app/frontend.log
3 started
following the logs; Ctrl+C stops the 3 services started here
api | INFO: Uvicorn running on http://127.0.0.1:21408
frontend | VITE v5.4 ready in 312 ms
Sonar picks a free port for every port: auto service and tells each service
where the others are, so a second worktree runs next to the first without a
single port clashing. sonar start -d does the same in the background, and
sonar down stops it and gives the ports back. In another terminal:
sonar list --tree
my-app (3 ports, running) ~/code/my-app
├─ 5432 db postgres:17 http://localhost:5432
├─ 21408 api uvicorn app:app http://localhost:21408
└─ 21409 frontend vite (v5.4) http://localhost:21409
No sonar.yaml? sonar start -- npm run dev runs one command as a named
service, and sonar list and sonar kill work on anything that is listening.
Examples below marked # check are executed against a fresh build by
scripts/readme-check.sh on every CI run.
sonar listsonar list
sonar list --tree
sonar list --group my-app
sonar list --json
# check
sonar list --stats # CPU, memory, threads, uptime, state
sonar list --health # HTTP health checks
sonar list --filter docker # only Docker ports
sonar list --sort name # port | pid | name | type
sonar list -a # include desktop apps
sonar list -c port,process,group,cpu,mem
sonar list --host user@server # scan a remote machine over SSH
Default columns are port, process, group, container, image,
containerport, url, where process shows the name you gave the port
(sonar rename), then the service name, then what was detected.
Available columns: port, process, pid, type, url, group, cpu,
mem, threads, uptime, state, connections, health, latency,
container, image, containerport, compose, project, user, bind,
ip.
Desktop apps and system services that happen to listen — Figma, Discord,
Spotify, ControlCenter, macOS .app bundles, /System/Library/ daemons — are
hidden unless you pass -a.
sonar startStart a project from its sonar.yaml:
sonar start # every service in the nearest sonar.yaml
sonar start api frontend # only these (they still wait for their dependencies)
sonar start ../other-project # the sonar.yaml in or above another directory
sonar start -d # in the background, like `sonar up`
In the foreground, sonar starts the services through the daemon, follows their logs with the service name in front of every line, and when you press Ctrl+C stops the services it started. A service that was already running is left alone, and sonar returns by itself once every service it started has exited.
Arguments before -- name a directory or services; everything after -- is a
command. Or run one command as a named service in a group:
sonar start -- npm run dev
sonar start --group my-app --name frontend -- npm run dev
sonar start --port 5173 -- npm run dev # expected port, before it binds
sonar start --detach --name api -- uv run uvicorn app:app
sonar start --list
Nothing has to be passed:
--group, else the name in the nearest sonar.yaml, else the
git root's directory name (a worktree becomes repo@worktree), else the name
of the current directory.--name, else the sonar.yaml service whose cmd matches, else
inferred from the command (npm run dev → dev, uv run api → api,
python -m uvicorn → uvicorn, ./dev.sh → dev.sh).--port is a hint, not a binding: the run shows as starting
until the port is actually listening, and the daemon uses it to match the
process to the port.The child inherits stdin, stdout, stderr, cwd and environment, plus
SONAR_GROUP, SONAR_NAME and SONAR_RUN_ID. It gets its own process group,
so sonar kill takes down the whole tree — a dev server with its watchers and
workers. Ctrl+C is forwarded, and sonar exits with the child's exit code.
--detach returns immediately and writes the output to
~/.config/sonar/logs/<group>/<name>.log. --list shows what sonar started,
and under it what has since ended — with the exit code, and whether it crashed
or was stopped (--json for the machine-readable form):
sonar start --list
sonar start --detach --name demo --port 8123 -- sleep 5
sonar start --list --json
# check
sonar.yaml