Help us shape the future of Go! Take the 2026 Go Developer Survey

sonar

command module
v0.5.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 6, 2026 License: MIT Imports: 1 Imported by: 0

README

███████╗ ██████╗ ███╗   ██╗ █████╗ ██████╗
██╔════╝██╔═══██╗████╗  ██║██╔══██╗██╔══██╗
███████╗██║   ██║██╔██╗ ██║███████║██████╔╝
╚════██║██║   ██║██║╚██╗██║██╔══██║██╔══██╗
███████║╚██████╔╝██║ ╚████║██║  ██║██║  ██║
╚══════╝ ╚═════╝ ╚═╝  ╚═══╝╚═╝  ╚═╝╚═╝  ╚═╝

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

Install

Homebrew (macOS / Linux)

brew install raskrebs/sonar/sonar

Install script

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

Using Go

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

Sixty seconds

Prefix the commands in your dev.sh with sonar start:

#!/usr/bin/env bash
sonar start --name db       --port 5432 -- docker compose up db &
sonar start --name api      --port 8000 -- uv run uvicorn app:app &
sonar start --name frontend --port 5173 -- npm run dev &
wait

The group name comes from the repository, so nothing else needs configuring. In another terminal:

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

And when you are done, stop the whole project — servers, watchers and workers:

sonar kill -g my-app

Examples below marked # check are executed against a fresh build by scripts/readme-check.sh on every CI run.

Commands

sonar list

sonar 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 start

Run a 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 — --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 — --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 — --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 (--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

A project names itself and its services in a .sonar.yaml at the repository root. It is optional — sonar groups by git root without it — and it is meant to be committed:

name: my-app
services:
  - name: db
    cmd: docker compose up db
    port: 5432
    health: /
    description: Postgres 17
    icon: database
    color: "#4f8cc9"
  - name: api
    cmd: uv run uvicorn app:app --port 8000
    cwd: backend
    port: 8000
    health: /healthz
    depends_on: [db]
  - name: frontend
    cmd: npm run dev
    port: 5173
    depends_on: [api]
ports: [9229]        # ports that belong to this project without a service
  • name — the group name. No slashes, no whitespace.
  • cmd, cwd, port — how sonar up starts the service. cwd is relative to the file and may not escape its directory.
  • health — an HTTP path the daemon polls while the service is up, so a service can be running but not yet healthy. It reports ok, fail or unknown, with the reason for a failure.
  • description, icon, color — free-form metadata for the desktop app; sonar never infers them.
  • depends_on — start order. Naming a service that is not in the file, or a cycle, is an error; an invalid file is reported once and never stops a scan.

.sonar.yml is read if that is how you spell it; sonar init always writes .sonar.yaml. The daemon watches the projects it knows about and picks up edits to the file without a restart. When the desktop app edits a service the file is re-rendered from its own syntax tree, so comments, key order and layout survive — except that extra spaces lining a trailing comment up (cmd: x # note) collapse to one, because the YAML library keeps the comment but not its column.

sonar up

sonar up                       # the .sonar.yaml at or above this directory
sonar up my-app                # a group by name
sonar up --only api,frontend
sonar up --json

Starts every service the group's .sonar.yaml declares, in depends_on order: a service waits for the ports its dependencies declare before it is started, and one that is already listening is skipped. Each runs detached in its own process group, with its output in ~/.config/sonar/logs/<group>/<service>.log.

  ✓ db        pid 41022  ~/.config/sonar/logs/my-app/db.log
  - api       already running
  ✓ frontend  pid 41108  ~/.config/sonar/logs/my-app/frontend.log

2 started, 1 already running

A service that fails to start is reported on its own line and makes the command exit non-zero, whatever else came up. Stop them all again with sonar kill -g my-app. sonar up needs the daemon and starts it if it is not already running.

sonar groups and sonar init

sonar init --dry-run
sonar groups
sonar groups --json
# check

sonar groups lists every group sonar can see and where each name came from: manual (you pinned it with sonar assign), start (a sonar start run), file (a .sonar.yaml) or auto (the git root or the Compose project). sonar groups <name> shows one group's ports and services, and the services that are declared but not running.

sonar init writes a .sonar.yaml at the git root from what is listening right now — desktop apps and ports below 1024 left out. It refuses to overwrite without --force, and --dry-run prints the file instead of writing it.

sonar kill

sonar kill 3000                            # SIGTERM, then SIGKILL after 5s
sonar kill 3000 5432 -f                    # SIGKILL both straight away
sonar kill 3000 --tree                     # the listener and everything below it
sonar kill --pid 12345 --tree              # by process id
sonar kill -g my-app                       # a whole group, confirms unless -y
sonar kill --all --filter docker -y        # every container publishing a port
sonar kill --all --project my-app          # one Compose project
sonar kill 3000 --ip 127.0.0.1             # one bind address of several
sonar kill --all --dry-run --json          # the plan for the whole machine

--dry-run takes any selector and changes nothing: it prints the actions the kill would take, children first, and leaves everything running. End to end, against a listener of your own:

sonar start --detach --name plan --port 8231 -- sonar map 3000 8231
sonar wait 8231
sonar kill 8231 --dry-run --json           # the plan; the mapping keeps running
sonar kill 8231 -y                         # and now for real
# check

A positional argument is read as a port, and as a pid only when nothing is listening on that number. -g matches the resolved group, a legacy run tag or id, and the Compose project, case-insensitively.

A process that ignores SIGTERM is sent SIGKILL once the port is still listening after --grace (5s); --no-escalate turns that off. Children are signalled before parents, so a tree comes down in order. Docker containers are stopped with docker stop and never signalled. A listener started by sonar start is always stopped together with its process group.

--json prints one row per process: {port, bind_address, pid, name, method, ok, error}, where method is sigterm, sigkill, docker_stop, map_stop or none. An empty sweep exits 0; an unknown group exits 1.

sonar map

sonar map 6873 3002        # also serve the service on 6873 from port 3002

Runs a TCP proxy in the foreground until you stop it. sonar kill reports a mapping it stopped as map_stop.

sonar rename, sonar assign, sonar history

sonar rename 3000 storefront     # a name of your own, survives restarts
sonar rename 3000 --clear
sonar assign 3000 my-app         # pin a port to a group by hand
sonar assign 3000 --clear
sonar history                    # everything that came up, went down, restarted
sonar history 3000 --since 24h --limit 20
sonar history --since 1h
sonar history --json
# check

Names and pins are stored in sonar's database, keyed by the most specific thing known about the port: the run (run:<group>/<name>), the container (docker:<project>/<service>), the working directory, and the port number last. A renamed dev server keeps its name across restarts; a name pinned to port 3000 alone applies to whatever answers there. These three commands need the daemon and start it if it is not running.

Reading a port

sonar info 3000                            # command, user, bind, stats, health
sonar logs 3000                            # tail; docker logs for containers
sonar wait 5432 3000 --timeout 60s         # block until ready
sonar wait 5432 --http=/health             # wait for HTTP 200-399, not just TCP
sonar next 3000                            # first free port from 3000
sonar next 3000-3100 -n 3                  # three consecutive free ports
sonar graph                                # who is connected to whom
sonar graph --dot                          # Graphviz
sonar open 3000                            # open in the browser
sonar attach 3000                          # shell into the container, or TCP
sonar watch                                # live view
sonar watch --stats --notify
sonar next 3000
sonar next 3000-3100 -n 3 --json
sonar graph --json
sonar info --help
# check

sonar wait exits 0 (ready), 1 (timeout) or 2 (interrupted), which makes it the thing to put between starting something and testing it:

docker compose up -d
sonar wait 5432 3000 --timeout 60s && npm run migrate && npm run test

Daemon or direct scan. Every read command asks the daemon if one is running, because it already has the answer and does not have to fork lsof. If none is running they scan directly and print one note on stderr saying so. sonar kill follows the same rule: a reachable daemon does the killing, so it rescans immediately and its next answer — and the port history — already knows the port is gone. Neither reads nor kills start a daemon behind your back. --no-daemon forces the direct scan silently and works on any command:

sonar list --no-daemon --json
# check

The daemon

One background process scans ports, resolves groups, polls health, keeps the database and streams changes to whoever is subscribed — the CLI, the desktop app, and editors.

sonar serve                  # in the foreground
sonar serve --detach         # in the background
sonar daemon status          # pid, uptime, subscribers, scans, capabilities
sonar daemon path            # the socket it listens on
sonar daemon log -n 50 -f    # what it is doing
sonar daemon restart
sonar daemon stop
sonar daemon path
sonar daemon status --json
sonar daemon log -n 5
# check
What Where
Socket $XDG_RUNTIME_DIR/sonar/daemon.sock, else ~/.config/sonar/daemon.sock; \\.\pipe\sonar on Windows
Database ~/.config/sonar/sonar.db (SONAR_DB overrides)
Daemon log ~/.config/sonar/daemon.log, rotated at 5 MiB, three kept
Run logs ~/.config/sonar/logs/<group>/<service>.log
Config ~/.config/sonar/config.yaml

SONAR_SOCKET overrides the socket path everywhere, for both the daemon and its clients — useful for a second isolated instance. The socket is created 0600 in a 0700 directory, so only you can talk to it. Only one daemon runs at a time; a socket left behind by a crash is cleaned up on the next start.

The daemon stops on its own after 30 minutes with no clients and no subscribers. Set daemon.idle_timeout in the config file to change that, or 0 to keep it running.

A subscriber that asks for include: ["health"] makes the daemon probe every listening port on a slower cadence, not only the services that declare a health: path — those are polled on every tick and reach every subscriber whether or not health was asked for.

Configuration

~/.config/sonar/config.yaml is optional; flags always win.

sonar config path
sonar config init
# check
sonar config edit       # open it in $EDITOR
list:
  columns: [port, process, group, container, image, containerport, url]
  sort: port            # port | pid | name | type
  filter: ""            # docker | user | system | "" (all)
  all: false            # include desktop apps by default
daemon:
  idle_timeout: 30m     # 0 keeps the daemon running
  log_level: info       # debug | info | warn | error
color: true
services:               # label custom/unknown ports
  9000: php-fpm
  5050: my-dashboard

Invalid values are ignored with a warning and sonar carries on with defaults. Environment overrides that have no config key: SONAR_DB, SONAR_SOCKET, and SONAR_NO_HINTS=1 to silence the migration notices below.

Agents: MCP, skills and hooks

sonar install mcp --claude-code               # merge into <git root>/.mcp.json
sonar install mcp --cursor --scope user       # ~/.cursor/mcp.json
sonar install mcp --codex                     # codex mcp add
sonar install skills --claude-code            # the bundled sonar skill
sonar install hooks --claude-code             # optional, see below
sonar install mcp --generic --print
sonar install skills --print
sonar install hooks --print
# check

install mcp registers {"command": "sonar", "args": ["mcp"]} and leaves every other server and key in the file alone; running it twice changes nothing, and --uninstall removes exactly what sonar wrote. The sonar mcp server itself lands in the next milestone — the wiring is here first so a client is already configured when it does.

install skills writes the bundled skill, which teaches an agent to start servers with sonar start --, to sonar wait instead of sleeping, and to clean up what it started. install hooks adds two Claude Code hooks: one exports SONAR_SESSION so everything a session starts is attributed to it, the other suggests sonar start -- when a bare dev server is about to run (it advises, it never blocks). Both take --scope project|user, --print and --uninstall.

The desktop app

The Sonar app is the same picture in a window and in the menu bar or system tray: groups down the side, ports in a grid with live stats and health, logs, and the buttons for everything above. It talks to the same daemon, so the CLI and the app never disagree. sonar tray launches it if it is installed and otherwise tells you where to get it: https://github.com/raskrebs/sonar/releases.

Until the app ships, macOS release tarballs still carry the old sonar-tray menu bar binary, and sonar tray falls back to it when the app is not installed.

Moving from the old commands

The pre-group commands still work and print a single line on stderr saying what replaced them. They go away one minor release from now. SONAR_NO_HINTS=1 silences the notices, and --json output never carries them.

Old New
sonar run --tag X -- cmd sonar start --group X -- cmd
sonar runs sonar start --list
sonar list --tag X sonar list --group X
sonar kill-all --filter docker sonar kill --all --filter docker
sonar down X sonar kill -g X
sonar profile create X sonar init
sonar profile show X sonar groups X
sonar up X (checked a profile) sonar up X now starts the group
sonar tray (Swift menu bar app) sonar tray launches the desktop app

Profiles were a per-machine snapshot of ports; .sonar.yaml is committed with the project. Convert one and read it before you keep it — nothing is written for you:

sonar profile list
# check
sonar profile export my-app > .sonar.yaml

A profile never recorded how a service starts, so the proposal has ports, names and health paths, and you fill in cmd.

Troubleshooting

Something is wrong with the daemon. sonar daemon log -f while you reproduce it, and sonar daemon status for pid, uptime and scan count. Stop it with sonar daemon stop; every read command keeps working without it.

"daemon unavailable, using direct scan". Nothing is listening on the socket. That is normal — reads do not start a daemon. Run sonar serve -d if you want one.

A socket left over from a crash. sonar daemon path shows it; starting a daemon removes a stale one by itself. If a second daemon refuses to start while the first is gone, sonar daemon restart clears the lock.

Ports are missing from the list. Processes owned by another user are invisible without privileges; sonar says so under the table. Re-run with sudo sonar list to see them. On Linux, ss must be installed (iproute2); on Windows, netstat is used.

A kill did nothing. Docker containers are stopped through the Docker daemon: check docker ps. A process that ignores SIGTERM needs -f, and one supervised by something else (systemd, Compose restart: always) comes back by design — stop the supervisor.

Reporting a bug. Include these two, plus the last lines of sonar daemon log:

sonar version
sonar daemon status
# check

Supported platforms

  • macOS (uses lsof)
  • Linux (uses ss)
  • Windows (uses netstat)

Contributors

Thanks to everyone who has contributed to sonar!

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
cmd
gen-schema command
Command gen-schema writes the daemon protocol JSON Schema, generated from the Go types in internal/state and internal/daemon/rpc, to disk.
Command gen-schema writes the daemon protocol JSON Schema, generated from the Go types in internal/state and internal/daemon/rpc, to disk.
internal
cmd
daemon
Package daemon implements `sonar serve`: the socket listener, the JSON-RPC dispatcher, the subscription fan-out and the process lifecycle.
Package daemon implements `sonar serve`: the socket listener, the JSON-RPC dispatcher, the subscription fan-out and the process lifecycle.
daemon/client
Package client is the Go client for the sonar daemon.
Package client is the Go client for the sonar daemon.
daemon/groupstart
Package groupstart serves `groups.start`: it walks a `.sonar.yaml`'s services in dependency order and spawns each one detached, streaming a chunk per service as it goes (contract §1).
Package groupstart serves `groups.start`: it walks a `.sonar.yaml`'s services in dependency order and spawns each one detached, streaming a chunk per service as it goes (contract §1).
daemon/rpc
Package rpc holds the JSON-RPC 2.0 wire types, the newline-delimited codec, the error registry and the method-description registry the protocol schema is generated from.
Package rpc holds the JSON-RPC 2.0 wire types, the newline-delimited codec, the error registry and the method-description registry the protocol schema is generated from.
daemon/runsreg
Package runsreg is the daemon's registry of processes started through `sonar start` (and `runs.spawn`).
Package runsreg is the daemon's registry of processes started through `sonar start` (and `runs.spawn`).
groups
Package groups resolves which project a listening port belongs to.
Package groups resolves which project a listening port belongs to.
hooks
Package hooks recognises long-lived dev-server commands inside a shell command line, so the Claude Code PreToolUse hook can suggest running them through `sonar start` instead.
Package hooks recognises long-lived dev-server commands inside a shell command line, so the Claude Code PreToolUse hook can suggest running them through `sonar start` instead.
install
Package install writes sonar's integration files into coding-agent configuration directories.
Package install writes sonar's integration files into coding-agent configuration directories.
killer
Package killer is sonar's single implementation of "stop what is listening here".
Package killer is sonar's single implementation of "stop what is listening here".
runs
Package runs implements the tagged-runs registry: a small on-disk JSON file recording processes spawned via `sonar run`, keyed by PID.
Package runs implements the tagged-runs registry: a small on-disk JSON file recording processes spawned via `sonar run`, keyed by PID.
scanner
Package scanner owns the daemon's single scan goroutine.
Package scanner owns the daemon's single scan goroutine.
spawn
Package spawn starts a child process on behalf of `sonar start`, `sonar up` and the daemon's `runs.spawn`, in a way that makes the whole tree killable and attributable: the child gets its own process group (a Job Object on Windows), the run's identity in its environment, and — when detached — a rotated log file under ~/.config/sonar/logs.
Package spawn starts a child process on behalf of `sonar start`, `sonar up` and the daemon's `runs.spawn`, in a way that makes the whole tree killable and attributable: the child gets its own process group (a Job Object on Windows), the run's identity in its environment, and — when detached — a rotated log file under ~/.config/sonar/logs.
state
Package state holds the daemon's published data model.
Package state holds the daemon's published data model.
store
Package store is the daemon's SQLite database: per-machine renames, group pins, the port-event history ring and the set of known .sonar.yaml roots.
Package store is the daemon's SQLite database: per-machine renames, group pins, the port-event history ring and the set of known .sonar.yaml roots.
tray
Package tray implements `sonar tray`.
Package tray implements `sonar tray`.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL