diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml
new file mode 100644
--- /dev/null
+++ b/.github/workflows/sonarcloud.yml
@@ -0,0 +1,21 @@
+name: SonarCloud
+
+on:
+  push:
+    branches: [main]
+  pull_request:
+    types: [opened, synchronize, reopened]
+
+permissions:
+  contents: read
+
+concurrency:
+  group: sonarcloud-${{ github.ref }}
+  # A push to main is the run that publishes coverage; superseded runs there would never finish.
+  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
+
+jobs:
+  sonarcloud:
+    # Tracked at @main so org-wide CI improvements propagate without PR churn.
+    uses: storytold/craft-common-actions/.github/workflows/sonarcloud.yml@main
+    secrets: inherit
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
 # Craft AI integrations
 
+<a href="https://sonarcloud.io/project/overview?id=storytold_craft-ai-integrations"><img alt="Scanned by SonarCloud" src="https://img.shields.io/badge/Scanned_by-SonarCloud-4b9fd5?style=flat-square&logo=sonarqubecloud&logoColor=white"></a>
+
 One AI connection for every Craft app. A person sets up a tool and its model once, in the launcher or
 natively in any app, and every app that has that tool uses it with no further setup. The shared crates
 own connections, models, the tool to model binding, the model picker, consent and jobs. Each app keeps
diff --git a/sonar-project.properties b/sonar-project.properties
new file mode 100644
--- /dev/null
+++ b/sonar-project.properties
@@ -0,0 +1,3 @@
+sonar.projectKey=storytold_craft-ai-integrations
+sonar.organization=storytold
+sonar.coverage.exclusions=**/tools/**,**/docs/**
