diff --git a/.github/workflows/safety.yml b/.github/workflows/safety.yml
new file mode 100644
index 0000000..768229d
--- /dev/null
+++ b/.github/workflows/safety.yml
@@ -0,0 +1,39 @@
+# SPDX-License-Identifier: MIT
+
+name: Safety
+
+on:
+  push:
+    branches: [master,master-next]
+  schedule:
+    - cron: '0 6 * * 1'
+  workflow_dispatch:
+
+permissions:
+  contents: read
+
+jobs:
+  scan:
+    runs-on: ubuntu-latest
+    timeout-minutes: 15
+    env:
+      SAFETY_API_KEY: ${{ secrets.SAFETY_API_KEY }}
+    # Skipped where the secret isn't set, e.g. forks.
+    steps:
+      - uses: actions/checkout@v5
+
+      - name: Build image
+        if: env.SAFETY_API_KEY != ''
+        run: docker build -t error-report-web-test .
+
+      # Scan what's installed, not requirements.txt ranges.
+      - name: Safety scan
+        if: env.SAFETY_API_KEY != ''
+        run: |
+          mkdir safety-scan
+          cp .safety-project.ini safety-scan/
+          docker run --rm error-report-web-test pip3 freeze > safety-scan/requirements.txt
+          # typer pinned: newer releases break safety 3.7.0 ("no active click context")
+          python3 -m venv /tmp/safety
+          /tmp/safety/bin/pip install -q safety==3.7.0 typer==0.23.0
+          /tmp/safety/bin/safety scan --target safety-scan --detailed-output
diff --git a/.safety-project.ini b/.safety-project.ini
new file mode 100644
index 0000000..29b7a3b
--- /dev/null
+++ b/.safety-project.ini
@@ -0,0 +1,5 @@
+[project]
+id = error-report-web
+url = /codebases/error-report-web/findings
+name = error-report-web
+
