Sonar CLI
Core Distinction
Use the two Sonar CLIs for different jobs:
sonar-scanner: whole-project analysis upload. Use for CI, quality gates, coverage import,sonar-project.properties, and final source scans.sonar: developer/operator CLI. Use forsonar auth status, creating/listing projects, listing issues, calling Sonar APIs, scanning one file, and generating one-off tokens.
If the user is confused about the two tools, read references/cli-distinction.md.
Never wire the PROJ-end sonar_cmd (wave-gate-config.json, run once per PROJ by the Quality Gate — no wave gate runs Sonar) to sonar analyze/sonar verify alone: without a language analyzer installed, both can exit 0 having checked every file and found nothing — a silent no-op that looks green. sonar-scanner is the only command that proves a real analysis ran; verify this yourself by requiring a fresh .scannerwork/report-task.txt (which only sonar-scanner writes) after sonar_cmd succeeds.
Project Setup Workflow
- Inspect the repo before writing config:
- package/build system:
package.json, lockfiles, test runner config. - existing Sonar config:
rg --files -g 'sonar-project.properties' -g '.sonar*'. - coverage output paths.
- package/build system:
- Create or update
sonar-project.propertieswith stable project metadata:sonar.host.urlsonar.organizationfor SonarQube Cloudsonar.projectKeysonar.sourcessonar.tests- language coverage path, such as
sonar.javascript.lcov.reportPaths=coverage/lcov.info
- Add
.scannerwork/to.gitignore; coverage output should normally already be ignored. - Add local scripts only when they match the repo:
test:coverageshould generate the report referenced by Sonar.sonarcan callsonar-scanner.
- Run coverage before the scanner.
- Run
sonar-scannerwith an analysis token, normally throughSONAR_TOKEN, never committed. In a skill-chain persistent worktree, set it in.env.local(the control checkout's, whichworktree.shsymlinks into every worktree) rather than only exporting it in a shell — preflight.sh and wave-gate.sh both fall back to reading it from.env.localwhen the ambient env doesn't have it. - Wait for the compute task, then pull issue and measure data through
sonar apiorsonar list issues. - Keep project measured state separate from quality gate status. Clean measures do not mean an enforced gate exists.
Authentication Workflow
First check which CLI is authenticated:
sonar auth status
env | awk -F= '/^SONAR/ { print $1 }'
Important: a successful sonar auth status does not mean sonar-scanner is authenticated. The scanner generally needs SONAR_TOKEN or -Dsonar.token=....
If sonar is authenticated and has permission, generate a short-lived scanner token without printing it:
TOKEN_JSON=$(sonar api post "/api/user_tokens/generate" --data "{\"name\":\"local-scanner-$(date +%Y%m%d%H%M%S)\"}")
SONAR_TOKEN=$(TOKEN_JSON="$TOKEN_JSON" node -e 'const data=JSON.parse(process.env.TOKEN_JSON); if (!data.token) process.exit(2); process.stdout.write(data.token);') sonar-scanner
If the organization from sonar auth status differs from sonar.organization, either log into the correct org/account or change the project config only if that matches the user's intent.
Quality Gate Workflow
Measured project state and quality gate status are separate. A project can have clean measures while /api/qualitygates/project_status returns NONE because no gate is assigned or no new analysis has run since assignment.
Use this workflow when a project needs an enforced gate:
sonar api get "/api/qualitygates/list?organization=<org>"
sonar api post "/api/qualitygates/create" --data "name=<gate-name>&organization=<org>"
# or copy an existing gate:
sonar api post "/api/qualitygates/copy" --data "id=<source-gate-id>&name=<gate-name>&organization=<org>"
sonar api post "/api/qualitygates/create_condition" --data "gateName=<gate-name>&metric=coverage&op=LT&error=80&organization=<org>"
sonar api post "/api/qualitygates/select" --data "organization=<org>&projectKey=<project-key>&gateName=<gate-name>"
sonar-scanner
Gate assignment is not retroactive. After assigning or changing a gate, the previous analysis may still show NONE; run a fresh sonar-scanner analysis before expecting project_status to become OK or ERROR.
Always check both assignment and evaluated status:
sonar api get "/api/qualitygates/get_by_project?organization=<org>&project=<project-key>"
sonar api get "/api/qualitygates/project_status?projectKey=<project-key>"
Do not use security_hotspots directly as a quality-gate metric through create_condition; check hotspots separately:
sonar api get "/api/hotspots/search?projectKey=<project-key>&ps=500"
Triage Workflow
After a successful scan:
sonar api get "/api/ce/task?id=<task-id>"
sonar list issues --project <project-key> --page-size 500
sonar api get "/api/measures/component?component=<project-key>&metricKeys=bugs,vulnerabilities,code_smells,security_hotspots,coverage,duplicated_lines_density,ncloc"
sonar api get "/api/qualitygates/project_status?projectKey=<project-key>"
Prioritize fixes in this order:
- Real bugs and vulnerabilities.
- Security hotspots with actual exploitability.
- Accessibility issues that affect users.
- Cognitive-complexity and readability issues in recently touched files.
- Mechanical style issues only when they are low-risk and broad enough to justify churn.
Use NOSONAR sparingly and only with a precise inline reason when the finding is a confirmed false positive or an intentional local-only prototype artifact.
Coverage Workflow
Sonar's coverage metric combines line and branch/condition coverage. An LCOV line coverage value such as 85% can become Sonar coverage around 79.3% if branch/condition coverage is lower.
Use coverage exclusions sparingly. Exclude boundary wrappers only when justified, such as:
- Next.js
page/layoutwrappers. - Server-action wrappers with no domain logic.
- Generated Supabase types.
- Supabase client factories.
Keep domain logic, repositories, services, validation, and UI components in coverage scope.
For repos with separate unit and DB Jest configs, merge LCOV before scanning:
rm -rf coverage
npm run test:unit -- --coverage --coverageDirectory=coverage/unit
npm run test:db -- --coverage --coverageDirectory=coverage/db
npx lcov-result-merger "coverage/**/lcov.info" "coverage/lcov.info"
sonar-scanner
If the repo uses different scripts or reporters, preserve the same shape: produce one merged coverage/lcov.info, point sonar.javascript.lcov.reportPaths at it, then scan.
Final Verification Bundle
Run this bundle before declaring Sonar clean:
sonar api get "/api/issues/search?componentKeys=<project-key>&resolved=false&ps=500"
sonar api get "/api/hotspots/search?projectKey=<project-key>&ps=500"
sonar api get "/api/measures/component?component=<project-key>&metricKeys=bugs,vulnerabilities,code_smells,security_hotspots,coverage"
sonar api get "/api/qualitygates/project_status?projectKey=<project-key>"
Common Failure Modes
sonar.organizationmissing: SonarQube Cloud scanner config is incomplete.Project not found: wrongsonar.projectKey, wrongsonar.organization, missing project, or token lacks access.- Scanner 403 while
sonar auth statusis connected: the scanner is not using thesonarCLI login token; passSONAR_TOKEN. - Coverage warning about unresolved paths: LCOV references files outside indexed
sonar.sources/sonar.tests, or test helper files are not included insonar.test.inclusions. - Missing blame warnings are expected with dirty/uncommitted files and do not necessarily affect quality gate conditions.
- Quality gate status remains
NONEafter assignment: run a freshsonar-scanner; gate assignment is not retroactive.
Official References
- SonarScanner CLI: https://docs.sonarsource.com/sonarqube-cloud/analyzing-source-code/scanners/sonarscanner-cli
- SonarQube CLI command reference: https://docs.sonarsource.com/sonarqube-developer-tools/sonarqube-cli/using-sonarqube-cli/commands
- Sonar Web API: https://docs.sonarsource.com/sonarqube-server/extension-guide/web-api