diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f0a5de4..ef207a6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -125,6 +125,10 @@ jobs:
run: |
npm ci
npm run build
+ - name: Verify macOS entitlement config
+ run: |
+ test "$(plutil -extract 'com\.apple\.security\.device\.audio-input' raw app/src-tauri/Entitlements.plist)" = "true"
+ test "$(plutil -extract 'com\.apple\.security\.cs\.disable-library-validation' raw app/src-tauri/Entitlements.plist)" = "true"
# Build sherpa in RELEASE so its prebuilt dylibs land in target/release — the exact path
# tauri.conf.json's `bundle.macOS.frameworks` points at, which tauri_build validates at compile
# time. The app itself still builds in debug (fast); only the dylib *location* needs release.
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 9b1b175..63c009d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -109,6 +109,12 @@ jobs:
working-directory: app
run: npm ci
+ - name: Verify macOS entitlement config
+ if: runner.os == 'macOS'
+ run: |
+ test "$(plutil -extract 'com\.apple\.security\.device\.audio-input' raw app/src-tauri/Entitlements.plist)" = "true"
+ test "$(plutil -extract 'com\.apple\.security\.cs\.disable-library-validation' raw app/src-tauri/Entitlements.plist)" = "true"
+
# Stamp the version from the tag into the manifests so installer FILENAMES and the embedded
# version (macOS About box, Windows MSI ProductVersion) match the release. The repo's checked-in
# 0.1.0 stays as the working version; the tag is the source of truth at release time.
@@ -145,3 +151,7 @@ jobs:
releaseDraft: true
prerelease: false
args: ${{ matrix.args }}
+
+ - name: Verify bundled macOS entitlements
+ if: runner.os == 'macOS'
+ run: ./scripts/verify-macos-entitlements.sh app/src-tauri/target/release/bundle/dmg/Wisp_*.dmg
diff --git a/app/src-tauri/Entitlements.plist b/app/src-tauri/Entitlements.plist
index bdcd6ae..c090c67 100644
--- a/app/src-tauri/Entitlements.plist
+++ b/app/src-tauri/Entitlements.plist
@@ -15,5 +15,8 @@
-->
com.apple.security.cs.disable-library-validation
+
+ com.apple.security.device.audio-input
+
diff --git a/scripts/verify-macos-entitlements.sh b/scripts/verify-macos-entitlements.sh
new file mode 100755
index 0000000..5bbb028
--- /dev/null
+++ b/scripts/verify-macos-entitlements.sh
@@ -0,0 +1,50 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+if [[ $# -ne 1 ]]; then
+ echo "Usage: $0 /path/to/Wisp.app-or.dmg" >&2
+ exit 2
+fi
+
+artifact_path="$1"
+mount_point=""
+
+cleanup() {
+ if [[ -n "$mount_point" ]]; then
+ hdiutil detach "$mount_point" >/dev/null
+ rmdir "$mount_point"
+ fi
+}
+
+trap cleanup EXIT
+
+if [[ -d "$artifact_path" ]]; then
+ app_path="$artifact_path"
+elif [[ -f "$artifact_path" && "$artifact_path" == *.dmg ]]; then
+ mount_point="$(mktemp -d "${TMPDIR:-/tmp}/wisp-dmg.XXXXXX")"
+ hdiutil attach -readonly -nobrowse -noautoopen -mountpoint "$mount_point" "$artifact_path" >/dev/null
+ app_path="$mount_point/Wisp.app"
+else
+ echo "App bundle or DMG not found: $artifact_path" >&2
+ exit 2
+fi
+
+if [[ ! -d "$app_path" ]]; then
+ echo "Wisp.app not found in artifact: $artifact_path" >&2
+ exit 2
+fi
+
+entitlements="$(codesign -d --entitlements :- "$app_path" 2>/dev/null)"
+
+if ! microphone_access="$(printf '%s' "$entitlements" | plutil -extract 'com\.apple\.security\.device\.audio-input' raw - 2>/dev/null)" || [[ "$microphone_access" != "true" ]]; then
+ echo "Missing com.apple.security.device.audio-input entitlement: $app_path" >&2
+ exit 1
+fi
+
+if ! library_validation="$(printf '%s' "$entitlements" | plutil -extract 'com\.apple\.security\.cs\.disable-library-validation' raw - 2>/dev/null)" || [[ "$library_validation" != "true" ]]; then
+ echo "Missing com.apple.security.cs.disable-library-validation entitlement: $app_path" >&2
+ exit 1
+fi
+
+echo "Verified macOS audio-input and library-validation entitlements: $artifact_path"