diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl
index 56cc925..a3dd8cc 100644
--- a/.beads/issues.jsonl
+++ b/.beads/issues.jsonl
@@ -1,3 +1,16 @@
+{"_type":"issue","id":"risus-cli-q0d","title":"T009: Add post-notarization codesign/spctl verification step","description":"speckit:005-macos-signed-release | T009 | Add post-notarization verification step (if: runner.os == macOS) in .github/workflows/release.yml running codesign --verify and spctl --assess","status":"closed","priority":2,"issue_type":"task","owner":"galadriel@example.com","created_at":"2026-05-03T19:21:13Z","created_by":"galadriel","updated_at":"2026-05-03T19:26:09Z","closed_at":"2026-05-03T19:26:09Z","close_reason":"Closed","dependencies":[{"issue_id":"risus-cli-q0d","depends_on_id":"risus-cli-77n","type":"blocks","created_at":"2026-05-03T21:22:15Z","created_by":"galadriel","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
+{"_type":"issue","id":"risus-cli-sa3","title":"T008: Update macOS artifact upload to use zip in release.yml","description":"speckit:005-macos-signed-release | T008 | Update macOS artifact upload step in .github/workflows/release.yml to upload risus-macos-arm64.zip and .sha256","status":"closed","priority":2,"issue_type":"task","owner":"galadriel@example.com","created_at":"2026-05-03T19:21:12Z","created_by":"galadriel","updated_at":"2026-05-03T19:26:09Z","closed_at":"2026-05-03T19:26:09Z","close_reason":"Closed","dependencies":[{"issue_id":"risus-cli-sa3","depends_on_id":"risus-cli-457","type":"blocks","created_at":"2026-05-03T21:22:20Z","created_by":"galadriel","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
+{"_type":"issue","id":"risus-cli-457","title":"T007: Update checksum step to hash zip on macOS in release.yml","description":"speckit:005-macos-signed-release | T007 | Update Compute checksum step in .github/workflows/release.yml to hash risus-macos-arm64.zip on macOS","status":"closed","priority":2,"issue_type":"task","owner":"galadriel@example.com","created_at":"2026-05-03T19:21:10Z","created_by":"galadriel","updated_at":"2026-05-03T19:26:08Z","closed_at":"2026-05-03T19:26:08Z","close_reason":"Closed","dependencies":[{"issue_id":"risus-cli-457","depends_on_id":"risus-cli-77n","type":"blocks","created_at":"2026-05-03T21:22:14Z","created_by":"galadriel","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
+{"_type":"issue","id":"risus-cli-77n","title":"T006: Add xcrun notarytool submit step to release.yml","description":"speckit:005-macos-signed-release | T006 | Add xcrun notarytool submit --wait step (if: runner.os == macOS, timeout-minutes: 15) in .github/workflows/release.yml","status":"closed","priority":2,"issue_type":"task","owner":"galadriel@example.com","created_at":"2026-05-03T19:21:09Z","created_by":"galadriel","updated_at":"2026-05-03T19:26:08Z","closed_at":"2026-05-03T19:26:08Z","close_reason":"Closed","dependencies":[{"issue_id":"risus-cli-77n","depends_on_id":"risus-cli-7bc","type":"blocks","created_at":"2026-05-03T21:22:13Z","created_by":"galadriel","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0}
+{"_type":"issue","id":"risus-cli-7bc","title":"T005: Add ditto zip step to release.yml after signing","description":"speckit:005-macos-signed-release | T005 | Add ditto -c -k --keepParent step (if: runner.os == macOS) after signing in .github/workflows/release.yml","status":"closed","priority":2,"issue_type":"task","owner":"galadriel@example.com","created_at":"2026-05-03T19:21:08Z","created_by":"galadriel","updated_at":"2026-05-03T19:26:07Z","closed_at":"2026-05-03T19:26:07Z","close_reason":"Closed","dependencies":[{"issue_id":"risus-cli-7bc","depends_on_id":"risus-cli-161","type":"blocks","created_at":"2026-05-03T21:22:08Z","created_by":"galadriel","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
+{"_type":"issue","id":"risus-cli-161","title":"T004: Add codesign step to release.yml after binary rename","description":"speckit:005-macos-signed-release | T004 | Add codesign --force --verbose --timestamp --sign step (if: runner.os == macOS) after binary rename in .github/workflows/release.yml","status":"closed","priority":2,"issue_type":"task","owner":"galadriel@example.com","created_at":"2026-05-03T19:21:06Z","created_by":"galadriel","updated_at":"2026-05-03T19:26:07Z","closed_at":"2026-05-03T19:26:07Z","close_reason":"Closed","dependencies":[{"issue_id":"risus-cli-161","depends_on_id":"risus-cli-p7l","type":"blocks","created_at":"2026-05-03T21:22:07Z","created_by":"galadriel","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
+{"_type":"issue","id":"risus-cli-p7l","title":"T002: Add apple-actions/import-codesign-certs@v7 to release.yml","description":"speckit:005-macos-signed-release | T002 | Add apple-actions/import-codesign-certs@v7 step (if: runner.os == macOS) to macOS matrix job in .github/workflows/release.yml","status":"closed","priority":2,"issue_type":"task","owner":"galadriel@example.com","created_at":"2026-05-03T19:21:05Z","created_by":"galadriel","updated_at":"2026-05-03T19:26:06Z","closed_at":"2026-05-03T19:26:06Z","close_reason":"Closed","dependencies":[{"issue_id":"risus-cli-p7l","depends_on_id":"risus-cli-2my","type":"blocks","created_at":"2026-05-03T21:21:59Z","created_by":"galadriel","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
+{"_type":"issue","id":"risus-cli-6rk","title":"T014: Run manual end-to-end verification per quickstart.md","description":"speckit:005-macos-signed-release | T014 | Run manual end-to-end verification per specs/005-macos-signed-release/quickstart.md against first notarized release artifact","status":"open","priority":2,"issue_type":"task","owner":"galadriel@example.com","created_at":"2026-05-03T19:20:59Z","created_by":"galadriel","updated_at":"2026-05-03T19:20:59Z","dependencies":[{"issue_id":"risus-cli-6rk","depends_on_id":"risus-cli-t58","type":"blocks","created_at":"2026-05-03T21:22:33Z","created_by":"galadriel","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
+{"_type":"issue","id":"risus-cli-t58","title":"T013: Update macOS job name to Build \u0026 Sign in release.yml","description":"speckit:005-macos-signed-release | T013 | Update macOS entry in release workflow job name from Build (macos-latest) to Build \u0026 Sign (macos-latest)","status":"closed","priority":2,"issue_type":"task","owner":"galadriel@example.com","created_at":"2026-05-03T19:20:58Z","created_by":"galadriel","updated_at":"2026-05-03T19:26:11Z","closed_at":"2026-05-03T19:26:11Z","close_reason":"Closed","dependencies":[{"issue_id":"risus-cli-t58","depends_on_id":"risus-cli-0op","type":"blocks","created_at":"2026-05-03T21:22:24Z","created_by":"galadriel","metadata":"{}"},{"issue_id":"risus-cli-t58","depends_on_id":"risus-cli-92w","type":"blocks","created_at":"2026-05-03T21:22:26Z","created_by":"galadriel","metadata":"{}"},{"issue_id":"risus-cli-t58","depends_on_id":"risus-cli-hb2","type":"blocks","created_at":"2026-05-03T21:22:25Z","created_by":"galadriel","metadata":"{}"},{"issue_id":"risus-cli-t58","depends_on_id":"risus-cli-q0d","type":"blocks","created_at":"2026-05-03T21:22:23Z","created_by":"galadriel","metadata":"{}"},{"issue_id":"risus-cli-t58","depends_on_id":"risus-cli-sa3","type":"blocks","created_at":"2026-05-03T21:22:21Z","created_by":"galadriel","metadata":"{}"}],"dependency_count":5,"dependent_count":1,"comment_count":0}
+{"_type":"issue","id":"risus-cli-92w","title":"T012: Add pre-tag secrets check to AGENTS.md Release Checklist","description":"speckit:005-macos-signed-release | T012 | Add pre-tag check to Release Checklist in AGENTS.md: verify all 6 Apple signing secrets configured","status":"closed","priority":2,"issue_type":"task","assignee":"galadriel","owner":"galadriel@example.com","created_at":"2026-05-03T19:20:57Z","created_by":"galadriel","updated_at":"2026-05-03T19:26:11Z","started_at":"2026-05-03T19:24:04Z","closed_at":"2026-05-03T19:26:11Z","close_reason":"Closed","dependencies":[{"issue_id":"risus-cli-92w","depends_on_id":"risus-cli-2my","type":"blocks","created_at":"2026-05-03T21:22:02Z","created_by":"galadriel","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
+{"_type":"issue","id":"risus-cli-hb2","title":"T011: Update build.yml on.push.branches for feature branch CI","description":"speckit:005-macos-signed-release | T011 | Update on.push.branches in .github/workflows/build.yml to add 005-macos-signed-release","status":"closed","priority":2,"issue_type":"task","assignee":"galadriel","owner":"galadriel@example.com","created_at":"2026-05-03T19:20:55Z","created_by":"galadriel","updated_at":"2026-05-03T19:26:10Z","started_at":"2026-05-03T19:24:03Z","closed_at":"2026-05-03T19:26:10Z","close_reason":"Closed","dependencies":[{"issue_id":"risus-cli-hb2","depends_on_id":"risus-cli-2my","type":"blocks","created_at":"2026-05-03T21:22:01Z","created_by":"galadriel","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
+{"_type":"issue","id":"risus-cli-0op","title":"T010: Add Signing Setup subsection to AGENTS.md Release Checklist","description":"speckit:005-macos-signed-release | T010 | Add Signing Setup subsection to Release Checklist in AGENTS.md documenting 6 required GitHub Actions secrets","status":"closed","priority":2,"issue_type":"task","assignee":"galadriel","owner":"galadriel@example.com","created_at":"2026-05-03T19:20:54Z","created_by":"galadriel","updated_at":"2026-05-03T19:26:10Z","started_at":"2026-05-03T19:24:03Z","closed_at":"2026-05-03T19:26:10Z","close_reason":"Closed","dependency_count":0,"dependent_count":1,"comment_count":0}
+{"_type":"issue","id":"risus-cli-2my","title":"T001: Create build/entitlements.plist with 4 Hardened Runtime entitlements","description":"speckit:005-macos-signed-release | T001 | Create build/entitlements.plist with 4 entitlements required for PyInstaller Python runtime under Hardened Runtime","status":"closed","priority":2,"issue_type":"task","assignee":"galadriel","owner":"galadriel@example.com","created_at":"2026-05-03T19:20:52Z","created_by":"galadriel","updated_at":"2026-05-03T19:26:06Z","started_at":"2026-05-03T19:24:01Z","closed_at":"2026-05-03T19:26:06Z","close_reason":"Closed","dependency_count":0,"dependent_count":3,"comment_count":0}
{"_type":"issue","id":"risus-cli-znu","title":"Fix silent connection failure in WSClient.start()","description":"WSClient.start() treats a 'disconnected' frame as a successful connection. When the WebSocket handshake fails (SSL, DNS, network), _async_run swallows the exception and puts a 'disconnected' frame in the inbox. start() reads it, doesn't recognise it as failure, puts it back and returns — showing the user a working menu that is not actually connected to any server.","acceptance_criteria":"1. When connection fails for any reason (SSL, DNS, network), the program exits immediately with a clear error message instead of showing the menu. 2. Error message reads: 'Connection to {server} failed — check address, network, and that the server is running.' 3. WSClient emits a 'connected' sentinel frame after successful WebSocket handshake, before reader starts. 4. start() raises TimeoutError immediately on 'disconnected' frame rather than waiting the full timeout. 5. Existing tests pass. 6. Manual test: wrong server address → immediate exit with correct message.","status":"closed","priority":2,"issue_type":"bug","owner":"galadriel@example.com","created_at":"2026-05-03T17:23:42Z","created_by":"galadriel","updated_at":"2026-05-03T17:26:45Z","closed_at":"2026-05-03T17:26:45Z","close_reason":"connected sentinel + immediate TimeoutError on disconnected + updated error message; 77 unit tests pass","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"risus-cli-cn0","title":"T026: Validate quickstart.md local dev scenario","description":"speckit:004-secure-session | T026 | Validate quickstart.md scenario with --token dev-token-for-testing uses ws://","status":"closed","priority":2,"issue_type":"task","assignee":"Eudicy","owner":"eudicy@boos.systems","created_at":"2026-05-03T07:33:54Z","created_by":"Eudicy","updated_at":"2026-05-03T08:07:52Z","started_at":"2026-05-03T08:07:10Z","closed_at":"2026-05-03T08:07:52Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"risus-cli-ddm","title":"T025: Verify server logs show reason but not token","description":"speckit:004-secure-session | T025 | Verify server logs show reason=token_absent or reason=token_mismatch but never token value","status":"closed","priority":2,"issue_type":"task","assignee":"Eudicy","owner":"eudicy@boos.systems","created_at":"2026-05-03T07:33:53Z","created_by":"Eudicy","updated_at":"2026-05-03T08:07:52Z","started_at":"2026-05-03T08:07:10Z","closed_at":"2026-05-03T08:07:52Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a5c3842..95c5e19 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -2,9 +2,9 @@ name: Build
on:
push:
- branches: [main, 003-standalone-client]
+ branches: [main, 003-standalone-client, 005-macos-signed-release]
pull_request:
- branches: [main, 003-standalone-client]
+ branches: [main, 003-standalone-client, 005-macos-signed-release]
jobs:
build:
@@ -15,9 +15,9 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- - uses: actions/setup-python@v5
+ - uses: actions/setup-python@v6
with:
python-version: "3.12"
@@ -39,7 +39,7 @@ jobs:
shell: pwsh
- name: Upload artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: risus-${{ matrix.os }}
path: dist/risus*
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 13a9cf4..adb680e 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -10,7 +10,7 @@ permissions:
jobs:
build:
- name: Build (${{ matrix.os }})
+ name: Build & Sign (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
@@ -26,9 +26,9 @@ jobs:
binary: dist/risus.exe
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- - uses: actions/setup-python@v5
+ - uses: actions/setup-python@v6
with:
python-version: "3.12"
@@ -47,8 +47,61 @@ jobs:
run: Rename-Item dist\risus.exe ${{ matrix.artifact }}
shell: pwsh
- - name: Compute checksum (Unix)
- if: runner.os != 'Windows'
+ - name: Import signing certificate
+ if: runner.os == 'macOS'
+ uses: apple-actions/import-codesign-certs@v7
+ with:
+ p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
+ p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
+
+ - name: Sign binary
+ if: runner.os == 'macOS'
+ run: |
+ codesign --force --verbose --timestamp \
+ --sign "Developer ID Application: $APPLE_TEAM_ID" \
+ --options=runtime --no-strict \
+ --entitlements build/entitlements.plist \
+ dist/risus-macos-arm64
+ env:
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+
+ - name: Package signed binary
+ if: runner.os == 'macOS'
+ run: ditto -c -k --keepParent dist/risus-macos-arm64 dist/risus-macos-arm64.zip
+
+ - name: Notarize
+ if: runner.os == 'macOS'
+ timeout-minutes: 15
+ run: |
+ echo "$APPLE_API_KEY_CONTENT" | base64 --decode > /tmp/apple_api_key.p8
+ xcrun notarytool submit dist/risus-macos-arm64.zip \
+ --wait \
+ --key /tmp/apple_api_key.p8 \
+ --key-id "$APPLE_API_KEY_ID" \
+ --issuer "$APPLE_API_ISSUER_ID"
+ env:
+ APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_CONTENT }}
+ APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
+ APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
+
+ - name: Clean up API key
+ if: always() && runner.os == 'macOS'
+ run: rm -f /tmp/apple_api_key.p8
+
+ - name: Verify signature and notarization
+ if: runner.os == 'macOS'
+ run: |
+ codesign --verify --deep --strict --verbose=2 dist/risus-macos-arm64
+ spctl --assess --type execute --verbose dist/risus-macos-arm64
+
+ - name: Compute checksum (macOS)
+ if: runner.os == 'macOS'
+ run: |
+ cd dist
+ sha256sum risus-macos-arm64.zip > risus-macos-arm64.zip.sha256
+
+ - name: Compute checksum (Linux)
+ if: runner.os == 'Linux'
run: |
cd dist
sha256sum ${{ matrix.artifact }} > ${{ matrix.artifact }}.sha256
@@ -60,8 +113,18 @@ jobs:
"$hash ${{ matrix.artifact }}" | Out-File -FilePath dist\${{ matrix.artifact }}.sha256 -Encoding ascii
shell: pwsh
+ - name: Upload artifact (macOS)
+ if: runner.os == 'macOS'
+ uses: actions/upload-artifact@v7
+ with:
+ name: ${{ matrix.artifact }}
+ path: |
+ dist/risus-macos-arm64.zip
+ dist/risus-macos-arm64.zip.sha256
+
- name: Upload artifact
- uses: actions/upload-artifact@v4
+ if: runner.os != 'macOS'
+ uses: actions/upload-artifact@v7
with:
name: ${{ matrix.artifact }}
path: |
@@ -73,16 +136,16 @@ jobs:
needs: build
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Download all artifacts
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v8
with:
path: release-assets
merge-multiple: true
- name: Create GitHub Release
- uses: softprops/action-gh-release@v2
+ uses: softprops/action-gh-release@v3
with:
files: |
release-assets/*
diff --git a/.omc/project-memory.json b/.omc/project-memory.json
index 8052137..deff406 100644
--- a/.omc/project-memory.json
+++ b/.omc/project-memory.json
@@ -30,7 +30,7 @@
},
"build": {
"buildCommand": null,
- "testCommand": ".venv/bin/pytest tests/unit -q 2>&1 && .venv/bin/ruff check risus.py client/ws_client.py 2>&1",
+ "testCommand": ".venv/bin/pytest tests/unit -q 2>&1",
"lintCommand": "ruff check",
"devCommand": null,
"scripts": {}
@@ -169,26 +169,26 @@
},
{
"path": "AGENTS.md",
- "accessCount": 22,
- "lastAccessed": 1777812713591,
+ "accessCount": 26,
+ "lastAccessed": 1777830524089,
"type": "file"
},
{
"path": ".specify/memory/constitution.md",
- "accessCount": 21,
- "lastAccessed": 1777812763030,
+ "accessCount": 23,
+ "lastAccessed": 1777831335578,
"type": "file"
},
{
- "path": "specs/003-standalone-client/spec.md",
- "accessCount": 20,
- "lastAccessed": 1777749597169,
+ "path": "client/ws_client.py",
+ "accessCount": 21,
+ "lastAccessed": 1777829969586,
"type": "file"
},
{
- "path": "client/ws_client.py",
+ "path": "specs/003-standalone-client/spec.md",
"accessCount": 20,
- "lastAccessed": 1777829327554,
+ "lastAccessed": 1777749597169,
"type": "file"
},
{
@@ -221,6 +221,12 @@
"lastAccessed": 1777750552764,
"type": "file"
},
+ {
+ "path": "pyproject.toml",
+ "accessCount": 12,
+ "lastAccessed": 1777830092269,
+ "type": "file"
+ },
{
"path": "specs/004-secure-session/plan.md",
"accessCount": 11,
@@ -239,6 +245,12 @@
"lastAccessed": 1777829168275,
"type": "file"
},
+ {
+ "path": "CLAUDE.md",
+ "accessCount": 11,
+ "lastAccessed": 1777831479114,
+ "type": "file"
+ },
{
"path": "specs/003-standalone-client/tasks.md",
"accessCount": 10,
@@ -263,12 +275,6 @@
"lastAccessed": 1777827349702,
"type": "file"
},
- {
- "path": "pyproject.toml",
- "accessCount": 10,
- "lastAccessed": 1777829267614,
- "type": "file"
- },
{
"path": "specs/003-standalone-client/data-model.md",
"accessCount": 9,
@@ -287,12 +293,6 @@
"lastAccessed": 1777754918429,
"type": "file"
},
- {
- "path": "CLAUDE.md",
- "accessCount": 9,
- "lastAccessed": 1777792045335,
- "type": "file"
- },
{
"path": "specs/004-secure-session/data-model.md",
"accessCount": 9,
@@ -371,12 +371,24 @@
"lastAccessed": 1777829220442,
"type": "file"
},
+ {
+ "path": ".specify/feature.json",
+ "accessCount": 5,
+ "lastAccessed": 1777830776429,
+ "type": "file"
+ },
{
"path": "specs/004-secure-session/checklists/requirements.md",
"accessCount": 4,
"lastAccessed": 1777791778635,
"type": "file"
},
+ {
+ "path": ".specify/templates/tasks-template.md",
+ "accessCount": 4,
+ "lastAccessed": 1777831528588,
+ "type": "file"
+ },
{
"path": "specs/003-standalone-client/checklists/requirements.md",
"accessCount": 3,
@@ -389,18 +401,6 @@
"lastAccessed": 1777791070410,
"type": "file"
},
- {
- "path": ".specify/feature.json",
- "accessCount": 3,
- "lastAccessed": 1777791146829,
- "type": "file"
- },
- {
- "path": ".specify/templates/tasks-template.md",
- "accessCount": 3,
- "lastAccessed": 1777792667551,
- "type": "file"
- },
{
"path": "risus.cfg.example",
"accessCount": 3,
diff --git a/.specify/feature.json b/.specify/feature.json
index bb839a4..0372b90 100644
--- a/.specify/feature.json
+++ b/.specify/feature.json
@@ -1,3 +1,3 @@
{
- "feature_directory": "specs/004-secure-session"
+ "feature_directory": "specs/005-macos-signed-release"
}
diff --git a/AGENTS.md b/AGENTS.md
index 72b9451..7823d61 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -213,11 +213,30 @@ Steps to publish a new release binary:
1. **Bump `pyproject.toml`** — update `version` field to next semver (`1.x.y`)
2. **Commit** the version bump: `build(deps): bump version to 1.x.y`
3. **Push** to `main`
-4. **Tag and push tag**:
+4. **Pre-tag: verify Apple signing secrets** — confirm all 6 secrets below are
+ configured in repository **Settings → Secrets and variables → Actions**
+ before pushing a release tag. Missing secrets cause the macOS signing job to
+ fail after the binary is built, blocking the release.
+5. **Tag and push tag**:
```bash
git tag v1.x.y
git push origin v1.x.y
```
-5. GitHub Actions builds and publishes binaries automatically on tag push.
+6. GitHub Actions builds, signs, notarizes, and publishes binaries automatically on tag push.
**Hard rule:** Never push a release tag without first bumping `pyproject.toml` — the binary reports the version baked at build time.
+
+### Signing Setup
+
+The macOS job requires 6 GitHub Actions secrets. Add them under
+**Settings → Secrets and variables → Actions → New repository secret**.
+See `specs/005-macos-signed-release/quickstart.md` for full setup steps.
+
+| Secret | What it is | How to obtain |
+|--------|-----------|---------------|
+| `APPLE_CERTIFICATE` | Developer ID Application certificate + private key, exported as a `.p12` file and base64-encoded | Keychain Access → export the "Developer ID Application" cert as `.p12`; run `base64 -i cert.p12` |
+| `APPLE_CERTIFICATE_PASSWORD` | Password set when exporting the `.p12` file | The password you chose during the `.p12` export |
+| `APPLE_TEAM_ID` | 10-character Apple Developer Team ID | [developer.apple.com/account](https://developer.apple.com/account) → Membership → Team ID |
+| `APPLE_API_KEY_ID` | App Store Connect API key ID | App Store Connect → Users and Access → Integrations → Keys → Key ID |
+| `APPLE_API_ISSUER_ID` | App Store Connect API issuer ID | Same page as above — Issuer ID shown at the top |
+| `APPLE_API_KEY_CONTENT` | Contents of the `.p8` private key file for the API key | Downloaded once when creating the API key; contents of the `.p8` file |
diff --git a/CLAUDE.md b/CLAUDE.md
index eee18c1..393fb62 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -17,5 +17,5 @@ Do not duplicate rules here. Update the constitution or AGENTS.md instead.
For additional context about technologies to be used, project structure,
shell commands, and other important information, read the current plan at
-`specs/004-secure-session/plan.md`.
+`specs/005-macos-signed-release/plan.md`.
diff --git a/build/entitlements.plist b/build/entitlements.plist
new file mode 100644
index 0000000..9d534f4
--- /dev/null
+++ b/build/entitlements.plist
@@ -0,0 +1,14 @@
+
+
+
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+ com.apple.security.cs.allow-dyld-environment-variables
+
+ com.apple.security.cs.disable-library-validation
+
+
+
diff --git a/specs/005-macos-signed-release/checklists/requirements.md b/specs/005-macos-signed-release/checklists/requirements.md
new file mode 100644
index 0000000..96145b6
--- /dev/null
+++ b/specs/005-macos-signed-release/checklists/requirements.md
@@ -0,0 +1,34 @@
+# Specification Quality Checklist: macOS Signed Release
+
+**Purpose**: Validate specification completeness and quality before proceeding to planning
+**Created**: 2026-05-03
+**Feature**: [spec.md](../spec.md)
+
+## Content Quality
+
+- [x] No implementation details (languages, frameworks, APIs)
+- [x] Focused on user value and business needs
+- [x] Written for non-technical stakeholders
+- [x] All mandatory sections completed
+
+## Requirement Completeness
+
+- [x] No [NEEDS CLARIFICATION] markers remain
+- [x] Requirements are testable and unambiguous
+- [x] Success criteria are measurable
+- [x] Success criteria are technology-agnostic (no implementation details)
+- [x] All acceptance scenarios are defined
+- [x] Edge cases are identified
+- [x] Scope is clearly bounded
+- [x] Dependencies and assumptions identified
+
+## Feature Readiness
+
+- [x] All functional requirements have clear acceptance criteria
+- [x] User scenarios cover primary flows
+- [x] Feature meets measurable outcomes defined in Success Criteria
+- [x] No implementation details leak into specification
+
+## Notes
+
+All items pass. Ready for `/speckit-plan`.
diff --git a/specs/005-macos-signed-release/contracts/release-artifact.md b/specs/005-macos-signed-release/contracts/release-artifact.md
new file mode 100644
index 0000000..e6f5a7f
--- /dev/null
+++ b/specs/005-macos-signed-release/contracts/release-artifact.md
@@ -0,0 +1,54 @@
+# Contract: Release Artifact
+
+**Phase**: 1 | **Date**: 2026-05-03
+
+Defines the contract for the macOS release artifact produced by the GitHub Actions release workflow.
+
+## Artifact Naming
+
+| Platform | Artifact filename | Checksum filename |
+|----------|------------------|------------------|
+| macOS arm64 | `risus-macos-arm64.zip` | `risus-macos-arm64.zip.sha256` |
+
+The zip contains: `risus-macos-arm64` (signed binary).
+
+## Verification Commands
+
+After downloading `risus-macos-arm64.zip` and extracting `risus-macos-arm64`:
+
+```bash
+# Verify code signature
+codesign --verify --deep --strict --verbose=2 risus-macos-arm64
+
+# Verify Gatekeeper acceptance (no privacy exceptions required)
+spctl --assess --type execute --verbose risus-macos-arm64
+
+# Expected output from spctl:
+# risus-macos-arm64: accepted
+# source=Notarized Developer ID
+```
+
+## Signing Identity
+
+```
+Developer ID Application: ()
+```
+
+## Required GitHub Actions Secrets
+
+| Secret name | Description |
+|-------------|-------------|
+| `APPLE_CERTIFICATE` | Base64-encoded `.p12` Developer ID Application certificate |
+| `APPLE_CERTIFICATE_PASSWORD` | Passphrase for the `.p12` file |
+| `APPLE_TEAM_ID` | 10-character Apple Developer team identifier |
+| `APPLE_API_KEY_ID` | App Store Connect API key ID (10 chars) |
+| `APPLE_API_ISSUER_ID` | App Store Connect issuer UUID |
+| `APPLE_API_KEY_CONTENT` | Base64-encoded `.p8` API key file content |
+
+## Failure Modes
+
+| Failure | CI behaviour |
+|---------|-------------|
+| `codesign` exits non-zero | Job fails; no artifact uploaded |
+| `notarytool` returns non-zero (Apple rejects or service error) | Job fails; no artifact uploaded |
+| Certificate expired or revoked | `codesign` fails fast with non-zero exit |
diff --git a/specs/005-macos-signed-release/data-model.md b/specs/005-macos-signed-release/data-model.md
new file mode 100644
index 0000000..d3a5179
--- /dev/null
+++ b/specs/005-macos-signed-release/data-model.md
@@ -0,0 +1,64 @@
+# Data Model: macOS Signed Release
+
+**Phase**: 1 | **Date**: 2026-05-03
+
+This feature has no runtime data model changes. The entities below describe the build-time and distribution artifacts.
+
+## Entities
+
+### Release Artifact
+
+The distributable file produced by the macOS release job.
+
+| Attribute | Value |
+|-----------|-------|
+| Name | `risus-macos-arm64.zip` |
+| Contents | Signed, notarized `risus-macos-arm64` binary |
+| Signing identity | Developer ID Application certificate (Developer ID Application: NAME (TEAM_ID)) |
+| Hardened runtime | Enabled (`--options runtime`) |
+| Entitlements | `com.apple.security.cs.disable-library-validation: true` |
+| Notarization | Submitted and approved by Apple's notarization service |
+| Gatekeeper result | `accepted` via online check on first run |
+
+### Developer ID Certificate
+
+| Attribute | Value |
+|-----------|-------|
+| Type | Developer ID Application (not Distribution, not App Store) |
+| Format | `.p12` PKCS#12 archive |
+| Storage | GitHub Actions secret `APPLE_CERTIFICATE` (base64-encoded) |
+| Passphrase secret | `APPLE_CERTIFICATE_PASSWORD` |
+| Keychain | Temporary per-job keychain, deleted after job |
+
+### App Store Connect API Key
+
+Used exclusively for `xcrun notarytool` authentication.
+
+| Attribute | Value |
+|-----------|-------|
+| Format | `.p8` private key file |
+| Key ID | GitHub Actions secret `APPLE_API_KEY_ID` |
+| Issuer ID | GitHub Actions secret `APPLE_API_ISSUER_ID` |
+| Content | GitHub Actions secret `APPLE_API_KEY_CONTENT` (base64-encoded) |
+
+### Entitlements File
+
+| Attribute | Value |
+|-----------|-------|
+| Path | `build/entitlements.plist` |
+| Format | Apple property list (XML) |
+| Entitlements | `allow-jit`, `allow-unsigned-executable-memory`, `allow-dyld-environment-variables`, `disable-library-validation` (all `true`) |
+| Rationale | Python runtime under Hardened Runtime requires all four; see research.md Decision 2 |
+
+## State Transitions
+
+```
+Binary built (unsigned)
+ → codesign applied (signed, hardened runtime)
+ → zipped
+ → notarytool submitted
+ → Apple scan: pass
+ → notarization approved (ticket in Apple DB)
+ → zip uploaded to GitHub Release
+ → user downloads → Gatekeeper online check → accepted → binary runs
+```
diff --git a/specs/005-macos-signed-release/plan.md b/specs/005-macos-signed-release/plan.md
new file mode 100644
index 0000000..557fec7
--- /dev/null
+++ b/specs/005-macos-signed-release/plan.md
@@ -0,0 +1,65 @@
+# Implementation Plan: macOS Signed Release
+
+**Branch**: `005-macos-signed-release` | **Date**: 2026-05-03 | **Spec**: [spec.md](./spec.md)
+**Input**: Feature specification from `/specs/005-macos-signed-release/spec.md`
+
+## Summary
+
+Add Apple code signing and notarization to the macOS job in the existing GitHub Actions release workflow. The binary is built with PyInstaller, signed with a Developer ID Application certificate (hardened runtime + minimal entitlements), zipped, submitted to Apple's notarization service, and distributed as a notarized zip. Gatekeeper verifies the notarization ticket online on first run — no user privacy exceptions required. No runtime code changes.
+
+## Technical Context
+
+**Language/Version**: Python 3.12
+**Build Tool**: PyInstaller 6 (single-file binary, `build/risus.spec`)
+**CI Platform**: GitHub Actions (`macos-latest` runner = Apple Silicon, arm64)
+**Signing Tool**: `codesign` (Xcode Command Line Tools, pre-installed on macOS runners)
+**Notarization Tool**: `xcrun notarytool` (Xcode 13+, pre-installed on macOS runners)
+**Storage**: N/A
+**Testing**: Manual verification via `spctl --assess` and `codesign --verify` on produced artifact
+**Target Platform**: Latest macOS release, arm64 (`macos-latest` runner)
+**Project Type**: CLI binary distribution
+**Performance Goals**: Notarization completes within 15 minutes per release
+**Constraints**: No hard-coded credentials; certificate and API key via GitHub Actions secrets only
+**Scale/Scope**: One artifact per tagged release; macOS runner only (Linux and Windows jobs unchanged)
+
+## Constitution Check
+
+*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
+
+| Principle | Status | Notes |
+|-----------|--------|-------|
+| I. Server Authority | ✅ PASS | Build/release concern only; no runtime state changes |
+| II. Simplicity | ✅ PASS | Touches only `release.yml` (macOS job) and adds `build/entitlements.plist`; zero UX or menu changes |
+| III. No Duplication | ✅ PASS | Signing logic lives in one place (`release.yml` macOS job) |
+| IV. Testing Discipline | ✅ PASS | No new runtime code; verification via `spctl --assess` on artifact |
+| V. No Local Persistence | ✅ PASS | Build artifact, not runtime state |
+
+No violations. Complexity Tracking table omitted.
+
+## Project Structure
+
+### Documentation (this feature)
+
+```text
+specs/005-macos-signed-release/
+├── plan.md # This file
+├── research.md # Phase 0 output
+├── data-model.md # Phase 1 output
+├── quickstart.md # Phase 1 output
+├── contracts/
+│ └── release-artifact.md # Phase 1 output
+└── tasks.md # Phase 2 output (/speckit-tasks)
+```
+
+### Source Code (repository root)
+
+```text
+build/
+├── risus.spec # Existing PyInstaller spec (unchanged)
+└── entitlements.plist # NEW: minimal entitlements for signed binary
+
+.github/workflows/
+└── release.yml # MODIFIED: macOS job gains signing + notarization steps
+```
+
+**Structure Decision**: Single-project layout. Only build tooling files change; no src/ changes.
diff --git a/specs/005-macos-signed-release/quickstart.md b/specs/005-macos-signed-release/quickstart.md
new file mode 100644
index 0000000..372ffac
--- /dev/null
+++ b/specs/005-macos-signed-release/quickstart.md
@@ -0,0 +1,55 @@
+# Quickstart: macOS Signed Release
+
+**Date**: 2026-05-03
+
+## What changes
+
+- `build/entitlements.plist` — new file; minimal entitlements for the signed binary
+- `.github/workflows/release.yml` — macOS job gains certificate import, codesign, notarization, and zip steps
+
+Linux and Windows jobs are unchanged.
+
+## Prerequisites (one-time developer setup)
+
+1. **Export Developer ID Application certificate** from Keychain Access as `.p12` with a strong passphrase.
+2. **Base64-encode the certificate**:
+ ```bash
+ base64 -i DeveloperIDApplication.p12 | pbcopy
+ ```
+3. **Create an App Store Connect API key** at [appstoreconnect.apple.com](https://appstoreconnect.apple.com) → Users and Access → Integrations → App Store Connect API. Download the `.p8` file (downloadable once only).
+4. **Base64-encode the API key**:
+ ```bash
+ base64 -i AuthKey_KEYID.p8 | pbcopy
+ ```
+5. **Add GitHub Actions secrets** (repository Settings → Secrets and variables → Actions):
+ - `APPLE_CERTIFICATE` — base64 certificate
+ - `APPLE_CERTIFICATE_PASSWORD` — certificate passphrase
+ - `APPLE_TEAM_ID` — your 10-character team ID (visible in developer.apple.com membership)
+ - `APPLE_API_KEY_ID` — key ID from App Store Connect
+ - `APPLE_API_ISSUER_ID` — issuer ID from App Store Connect
+ - `APPLE_API_KEY_CONTENT` — base64 API key
+
+## Triggering a release
+
+```bash
+git tag v1.0.5
+git push origin v1.0.5
+```
+
+The release workflow runs. The macOS job:
+1. Builds the binary with PyInstaller
+2. Imports the certificate into a temporary keychain
+3. Signs the binary with `codesign`
+4. Zips the binary
+5. Submits the zip to Apple notarization (`xcrun notarytool submit --wait`)
+6. Uploads `risus-macos-arm64.zip` to the GitHub Release
+
+## Verifying the artifact locally
+
+```bash
+unzip risus-macos-arm64.zip
+codesign --verify --deep --strict --verbose=2 risus-macos-arm64
+spctl --assess --type execute --verbose risus-macos-arm64
+# Expected: risus-macos-arm64: accepted
+# source=Notarized Developer ID
+```
diff --git a/specs/005-macos-signed-release/research.md b/specs/005-macos-signed-release/research.md
new file mode 100644
index 0000000..4da68e6
--- /dev/null
+++ b/specs/005-macos-signed-release/research.md
@@ -0,0 +1,53 @@
+# Research: macOS Signed Release
+
+**Phase**: 0 | **Date**: 2026-05-03
+
+## Decision 1: PyInstaller Binary Signing
+
+- **Decision**: Sign the PyInstaller single-file binary directly with `codesign --force --verbose --timestamp --sign "Developer ID Application: TEAM_NAME (TEAM_ID)" --options=runtime --no-strict --entitlements build/entitlements.plist`.
+- **Rationale**: PyInstaller produces a single Mach-O executable. `--options runtime` enables Hardened Runtime (prerequisite for notarization). `--timestamp` embeds Apple's secure timestamp so the signature remains valid after certificate expiry. `--no-strict` avoids false validation errors on PyInstaller's internal structure. `--deep` is omitted — it is unnecessary for a single-file binary and can cause signing errors.
+- **Alternatives considered**: Signing a `.app` bundle — unnecessary overhead for a CLI tool with no GUI.
+
+## Decision 2: Entitlements
+
+- **Decision**: Provide `build/entitlements.plist` with 4 entitlements: `com.apple.security.cs.allow-jit`, `com.apple.security.cs.allow-unsigned-executable-memory`, `com.apple.security.cs.allow-dyld-environment-variables`, `com.apple.security.cs.disable-library-validation` (all `true`).
+- **Rationale**: PyInstaller embeds a Python runtime. Under Hardened Runtime, macOS enforces strict memory and library constraints that the Python interpreter requires relaxing: JIT for bytecode execution, unsigned executable memory for Python's allocator, DYLD variables for Python path resolution, and disabled library validation because embedded libraries are not individually Apple-signed. This set is confirmed by the malimo project (same signing approach for a .NET self-contained binary, analogous constraints).
+- **Alternatives considered**: Using only `disable-library-validation` — insufficient; Python runtime also needs the JIT and memory entitlements. Signing all embedded libraries individually — impractical with PyInstaller's bundle structure.
+
+## Decision 3: Notarization Authentication
+
+- **Decision**: Use App Store Connect API key (`.p8` file) with `xcrun notarytool submit --key-id ... --issuer-id ... --key ...`.
+- **Rationale**: API key authentication is stateless, requires no 2FA, and is the recommended approach for CI/CD. Apple ID + app-specific password requires storing an Apple ID email as a secret and can be blocked by Apple's security systems.
+- **Alternatives considered**: Apple ID + app-specific password — works but requires more secrets and is more fragile in automated environments. Both approaches produce identical notarization results.
+- **Secrets required**:
+ - `APPLE_API_KEY_ID` — 10-character key ID from App Store Connect
+ - `APPLE_API_ISSUER_ID` — UUID issuer ID from App Store Connect
+ - `APPLE_API_KEY_CONTENT` — base64-encoded `.p8` key file content
+
+## Decision 4: Notarization Submission Format
+
+- **Decision**: Zip the signed binary (`ditto -c -k --keepParent`) and submit the zip to `xcrun notarytool submit --wait`.
+- **Rationale**: `notarytool` requires a zip, `.dmg`, or `.pkg` — not a bare binary. A zip is the simplest wrapper. `--wait` blocks until Apple completes the scan (typically 1–5 minutes), simplifying the CI step.
+- **Alternatives considered**: `.pkg` wrapper — enables stapling (ticket embedded in artifact for offline Gatekeeper checks), but adds packaging complexity. Since the spec requires no privacy exceptions on download (not offline use), a zip with online Gatekeeper verification satisfies all requirements.
+
+## Decision 5: Stapling
+
+- **Decision**: Do not staple. Distribute the notarized zip as-is.
+- **Rationale**: `xcrun stapler` cannot staple a notarization ticket to a bare binary or a zip archive — only to `.app`, `.dmg`, and `.pkg` files. On first run, macOS Gatekeeper performs an online check against Apple's notarization database and passes the binary. This satisfies FR-003 (`spctl --assess` accepted) without any user privacy exception.
+- **Alternatives considered**: Wrapping in `.dmg` or `.pkg` to enable stapling — provides offline Gatekeeper verification but adds significant packaging steps. Out of scope per Simplicity principle; the spec does not require offline use.
+
+## Decision 6: Certificate Import in CI
+
+- **Decision**: Use `apple-actions/import-codesign-certs@v7` GitHub Action to import the `.p12` certificate.
+- **Rationale**: The action handles temporary keychain creation, certificate import, partition list configuration, and cleanup automatically. Eliminates ~15 lines of fragile bash. Confirmed working in malimo project with identical secrets layout.
+- **Secrets required**:
+ - `APPLE_CERTIFICATE` — base64-encoded `.p12` Developer ID Application certificate
+ - `APPLE_CERTIFICATE_PASSWORD` — passphrase for the `.p12` file
+ - `APPLE_TEAM_ID` — 10-character team identifier (used in the signing identity string)
+
+## Resolved Clarifications
+
+All NEEDS CLARIFICATION items from the spec have been resolved:
+- Notarization service unavailability: `xcrun notarytool submit --wait` exits non-zero on Apple service errors; GitHub Actions will mark the job failed and no artifact is uploaded (satisfies FR-005).
+- Certificate expiry: `codesign` fails fast with a non-zero exit code if the certificate is invalid or expired.
+- Artifact portability: The notarization record is tied to the binary's hash in Apple's database; copying to another machine preserves the signature and Gatekeeper performs the same online check.
diff --git a/specs/005-macos-signed-release/spec.md b/specs/005-macos-signed-release/spec.md
new file mode 100644
index 0000000..2396f3c
--- /dev/null
+++ b/specs/005-macos-signed-release/spec.md
@@ -0,0 +1,99 @@
+# Feature Specification: macOS Signed Release
+
+**Feature Branch**: `005-macos-signed-release`
+**Created**: 2026-05-03
+**Status**: Draft
+**Input**: User description: "The macOS release shall not require the user to configure privacy exceptions. The developer is enrolled in the apple developer program."
+
+## User Scenarios & Testing *(mandatory)*
+
+### User Story 1 - Run CLI Without Security Prompts (Priority: P1)
+
+A macOS user downloads the Risus CLI release artifact and runs it immediately without needing to visit System Settings or approve any security exception.
+
+**Why this priority**: Any security prompt is a blocker — users who do not know how to bypass Gatekeeper cannot use the tool at all. This is the primary acceptance gate.
+
+**Independent Test**: Download the release artifact on a clean macOS machine, double-click or execute it in Terminal, and verify it runs without any "unidentified developer" dialog, Gatekeeper block, or privacy exception prompt. Delivers a fully usable CLI out of the box.
+
+**Acceptance Scenarios**:
+
+1. **Given** a macOS user downloads the release artifact, **When** they run it from Terminal for the first time, **Then** the CLI launches without any Gatekeeper warning or security exception dialog.
+2. **Given** a macOS user is on macOS 13 (Ventura) or later, **When** they execute the artifact, **Then** macOS does not quarantine or block execution.
+3. **Given** the artifact is downloaded via a browser, **When** the user runs it immediately, **Then** no "open anyway" step in System Settings is required.
+
+---
+
+### User Story 2 - Developer Produces a Notarized Release Artifact (Priority: P2)
+
+A developer with an Apple Developer Program account runs the release process and produces an artifact that is code-signed and notarized, ready for distribution.
+
+**Why this priority**: Without a repeatable notarization workflow the P1 user story cannot be fulfilled on each release.
+
+**Independent Test**: Run the release build process in a macOS environment with valid Apple Developer credentials. Verify the produced artifact passes `spctl` assessment and carries a valid code signature. Delivers a distributable binary independent of any end-user steps.
+
+**Acceptance Scenarios**:
+
+1. **Given** a developer has valid Apple Developer Program credentials configured, **When** the release build process runs, **Then** it produces a signed and notarized artifact without manual intervention.
+2. **Given** the produced artifact, **When** `spctl --assess` is run against it, **Then** the assessment passes with `accepted` status.
+3. **Given** the produced artifact, **When** `codesign --verify` is run against it, **Then** the signature is valid and the signing identity is a trusted Apple Developer ID.
+
+---
+
+### User Story 3 - CI/CD Produces Consistent Notarized Artifacts (Priority: P3)
+
+The CI/CD pipeline produces a notarized macOS artifact on each tagged release without developer manual steps beyond providing credentials as secrets.
+
+**Why this priority**: Automates the notarization workflow so releases are repeatable and not gated on a specific developer's machine.
+
+**Independent Test**: Trigger a tagged release in CI. Confirm the produced macOS artifact in the release assets is notarized. Delivers automation independent of local developer environment.
+
+**Acceptance Scenarios**:
+
+1. **Given** a tagged release is pushed, **When** the CI pipeline runs, **Then** the macOS artifact in release assets is signed and notarized.
+2. **Given** Apple Developer credentials are stored as CI secrets, **When** the pipeline runs, **Then** signing and notarization succeed without interactive input.
+
+---
+
+### Edge Cases
+
+- What happens if the Apple notarization service is temporarily unavailable during CI?
+- How does the artifact behave on macOS versions older than the minimum supported version?
+- What if the Developer ID certificate is expired or revoked — does the build fail fast with a clear error?
+- What happens if the user copies the artifact to another macOS machine — does the signature remain valid?
+
+## Requirements *(mandatory)*
+
+### Functional Requirements
+
+- **FR-001**: The macOS release artifact MUST be code-signed with a valid Apple Developer ID Application certificate.
+- **FR-002**: The macOS release artifact MUST be submitted to Apple for notarization and approved; Gatekeeper verifies the notarization record online on first run.
+- **FR-003**: The artifact MUST pass `spctl --assess` on a clean macOS system without any user-configured security exceptions.
+- **FR-004**: The release build process MUST accept Apple Developer credentials (Team ID, certificate, App-specific password or API key) via environment variables or secrets — no hard-coded credentials.
+- **FR-005**: The release process MUST fail with a clear error if code-signing or notarization fails, preventing distribution of an unsigned artifact.
+- **FR-006**: The CI/CD pipeline MUST produce the notarized macOS artifact automatically on each tagged release.
+- **FR-007**: The artifact MUST run on the latest macOS release without additional configuration by the user.
+
+### Key Entities
+
+- **Release Artifact**: The distributable macOS binary or archive of the Risus CLI, packaged for end-user download.
+- **Developer ID Certificate**: The Apple-issued code-signing certificate associated with the enrolled Apple Developer Program account.
+- **Notarization Ticket**: Apple's cryptographic attestation stapled to the artifact confirming it passed security checks.
+
+## Success Criteria *(mandatory)*
+
+### Measurable Outcomes
+
+- **SC-001**: A fresh macOS user can download and run the CLI in under 60 seconds with zero security dialogs or System Settings steps.
+- **SC-002**: 100% of tagged releases produce a notarized macOS artifact via the automated pipeline.
+- **SC-003**: `spctl --assess` returns `accepted` on every distributed artifact.
+- **SC-004**: The release build process completes notarization in under 15 minutes per release.
+- **SC-005**: Zero support requests related to macOS Gatekeeper or "unidentified developer" blocks after the feature ships.
+
+## Assumptions
+
+- Developer is enrolled in the Apple Developer Program and has access to a valid Developer ID Application certificate.
+- Credentials (Team ID, certificate, signing password) will be provided as CI secrets — not stored in the repository.
+- The CLI is distributed as a standalone binary or archive (e.g., via GitHub Releases), not through the Mac App Store, so Developer ID signing applies rather than App Store signing.
+- Only the latest macOS release is supported; older versions are out of scope.
+- The existing CI/CD platform (GitHub Actions or equivalent) supports macOS runners capable of running the signing and notarization tools.
+- No entitlements beyond what is required for a command-line tool are needed (no camera, microphone, or other privacy-sensitive access).
diff --git a/specs/005-macos-signed-release/tasks.md b/specs/005-macos-signed-release/tasks.md
new file mode 100644
index 0000000..ab61f23
--- /dev/null
+++ b/specs/005-macos-signed-release/tasks.md
@@ -0,0 +1,155 @@
+# Tasks: macOS Signed Release
+
+**Input**: Design documents from `/specs/005-macos-signed-release/`
+**Prerequisites**: plan.md ✅, spec.md ✅, research.md ✅, data-model.md ✅, contracts/ ✅
+
+**Organization**: Tasks grouped by user story. No tests requested in spec — no test tasks generated.
+
+## Format: `[ID] [P?] [Story] Description`
+
+- **[P]**: Can run in parallel (different files, no dependencies)
+- **[Story]**: Which user story this task belongs to (US1, US2, US3)
+
+---
+
+## Phase 1: Setup (Shared Infrastructure)
+
+**Purpose**: New build artifact required before any signing step can run.
+
+- [x] T001 Create `build/entitlements.plist` with 4 entitlements required for PyInstaller Python runtime under Hardened Runtime: `com.apple.security.cs.allow-jit`, `com.apple.security.cs.allow-unsigned-executable-memory`, `com.apple.security.cs.allow-dyld-environment-variables`, `com.apple.security.cs.disable-library-validation` (all `true`)
+
+---
+
+## Phase 2: Foundational (Blocking Prerequisites)
+
+**Purpose**: Certificate import into CI keychain — MUST complete before any `codesign` call.
+
+**⚠️ CRITICAL**: T002 must be complete before US1 signing steps can run.
+
+- [x] T002 Add `apple-actions/import-codesign-certs@v7` step (`if: runner.os == 'macOS'`) to macOS matrix job in `.github/workflows/release.yml` with `p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}` and `p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}` — action handles keychain creation, import, and cleanup automatically; T003 no longer needed
+
+**Checkpoint**: Keychain lifecycle in place — US1 signing steps can now be added.
+
+---
+
+## Phase 3: User Story 1 - Run CLI Without Security Prompts (Priority: P1) 🎯 MVP
+
+**Goal**: End user downloads and runs the CLI on macOS with zero Gatekeeper dialogs or System Settings steps.
+
+**Independent Test**: Download `risus-macos-arm64.zip` from GitHub Release, extract, run `spctl --assess --type execute --verbose risus-macos-arm64` — output must show `accepted` and `source=Notarized Developer ID`.
+
+### Implementation for User Story 1
+
+- [x] T004 [US1] Add `codesign --force --verbose --timestamp --sign "Developer ID Application: $APPLE_TEAM_ID" --options=runtime --no-strict --entitlements build/entitlements.plist dist/risus-macos-arm64` step (`if: runner.os == 'macOS'`) after binary rename in `.github/workflows/release.yml`
+- [x] T005 [US1] Add `ditto -c -k --keepParent dist/risus-macos-arm64 dist/risus-macos-arm64.zip` step (`if: runner.os == 'macOS'`) after signing in `.github/workflows/release.yml`
+- [x] T006 [US1] Add `xcrun notarytool submit dist/risus-macos-arm64.zip --wait --key /tmp/apple_api_key.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER_ID` step (`if: runner.os == 'macOS'`, `timeout-minutes: 15`) in `.github/workflows/release.yml` — decode `APPLE_API_KEY_CONTENT` to `/tmp/apple_api_key.p8` before calling notarytool, delete the file after
+- [x] T007 [US1] Update the "Compute checksum (Unix)" step in `.github/workflows/release.yml` to hash `risus-macos-arm64.zip` on macOS (not the bare binary) — use `if: runner.os == 'macOS'` for the new step and `if: runner.os == 'Linux'` for the existing Linux step
+- [x] T008 [US1] Update the macOS artifact upload step in `.github/workflows/release.yml` — change `path` to upload `dist/risus-macos-arm64.zip` and `dist/risus-macos-arm64.zip.sha256` instead of the bare binary
+
+**Checkpoint**: Tag a release and confirm the macOS artifact in GitHub Releases is `risus-macos-arm64.zip`, passes `spctl --assess`, and requires no System Settings exception.
+
+---
+
+## Phase 4: User Story 2 - Developer Produces Notarized Artifact (Priority: P2)
+
+**Goal**: Developer can verify the artifact is correctly signed and notarized using standard macOS tooling.
+
+**Independent Test**: Run `codesign --verify --deep --strict --verbose=2 risus-macos-arm64` and `spctl --assess --type execute --verbose risus-macos-arm64` against the downloaded artifact — both commands exit 0.
+
+### Implementation for User Story 2
+
+- [x] T009 [US2] Add post-notarization verification step (`if: runner.os == 'macOS'`) in `.github/workflows/release.yml` — run `codesign --verify --deep --strict --verbose=2 dist/risus-macos-arm64` and `spctl --assess --type execute --verbose dist/risus-macos-arm64`; job fails if either command exits non-zero
+- [x] T010 [P] [US2] Add "Signing Setup" subsection to the Release Checklist in `AGENTS.md` documenting the 6 required GitHub Actions secrets (`APPLE_CERTIFICATE`, `APPLE_CERTIFICATE_PASSWORD`, `APPLE_TEAM_ID`, `APPLE_API_KEY_ID`, `APPLE_API_ISSUER_ID`, `APPLE_API_KEY_CONTENT`) and how to obtain each (reference `specs/005-macos-signed-release/quickstart.md` for full setup steps)
+
+**Checkpoint**: CI run produces a verifiable artifact; developer can confirm signing with `codesign` and `spctl` without any manual steps.
+
+---
+
+## Phase 5: User Story 3 - CI/CD Consistent Notarized Artifacts (Priority: P3)
+
+**Goal**: Every tagged release automatically produces a notarized macOS artifact via GitHub Actions without developer intervention.
+
+**Independent Test**: Push a tag to a branch with secrets configured — GitHub Actions macOS job completes without manual steps and the release asset is `risus-macos-arm64.zip` with `source=Notarized Developer ID`.
+
+### Implementation for User Story 3
+
+- [x] T011 [US3] Update `on.push.branches` in `.github/workflows/build.yml` to add `005-macos-signed-release` so CI runs on this feature branch during development
+- [x] T012 [P] [US3] Add a pre-tag check to the Release Checklist in `AGENTS.md`: verify all 6 Apple signing secrets are configured in repository Settings → Secrets before pushing a release tag
+
+**Checkpoint**: Tagged release with secrets configured completes notarization automatically; secrets missing causes clear CI failure before any artifact is uploaded.
+
+---
+
+## Phase N: Polish & Cross-Cutting Concerns
+
+**Purpose**: Documentation and release checklist hygiene.
+
+- [x] T013 [P] Update the macOS entry in the release workflow job name in `.github/workflows/release.yml` from `Build (macos-latest)` to `Build & Sign (macos-latest)` for clarity in the GitHub Actions UI
+- [ ] T014 Run manual end-to-end verification per `specs/005-macos-signed-release/quickstart.md` against the first notarized release artifact
+
+---
+
+## Dependencies & Execution Order
+
+### Phase Dependencies
+
+- **Setup (Phase 1)**: No dependencies — start immediately
+- **Foundational (Phase 2)**: Depends on T001 (entitlements must exist before signing) — **blocks all US phases**
+- **US1 (Phase 3)**: Depends on T001, T002, T003 — core delivery
+- **US2 (Phase 4)**: T009 depends on T006 (notarization must exist before verify step); T010 is independent [P]
+- **US3 (Phase 5)**: T011 and T012 are independent [P] of US1/US2 implementation
+- **Polish (Phase N)**: Depends on all US phases complete
+
+### User Story Dependencies
+
+- **US1 (P1)**: Depends on Foundational (T001–T003) — no dependency on US2/US3
+- **US2 (P2)**: T009 depends on US1 workflow steps existing; T010 is independent
+- **US3 (P3)**: Independent of US1/US2 — parallel with either
+
+### Within US1
+
+- T004 (sign) → T005 (zip) → T006 (notarize) → T007 (checksum) → T008 (upload): strictly sequential
+
+---
+
+## Parallel Opportunities
+
+```bash
+# Phase 1+2 can start in parallel with US3 tasks:
+Task: T001 "Create build/entitlements.plist"
+Task: T011 "Update build.yml branches list"
+Task: T012 "Add pre-tag secrets check to AGENTS.md"
+
+# After Foundational complete, T010 runs in parallel with T004-T008:
+Task: T010 "Add signing setup subsection to AGENTS.md"
+Task: T004-T008 "US1 workflow signing steps (sequential)"
+```
+
+---
+
+## Implementation Strategy
+
+### MVP First (User Story 1 Only)
+
+1. Complete T001 (entitlements file)
+2. Complete T002–T003 (keychain lifecycle)
+3. Complete T004–T008 (sign, zip, notarize, checksum, upload)
+4. **STOP and VALIDATE**: push a test tag, confirm `spctl --assess` returns `accepted`
+5. Add T009 (verification step) and T010 (docs)
+
+### Incremental Delivery
+
+1. T001 → T002–T003 → T004–T008 → validate (US1 complete, users unblocked)
+2. T009 → T010 → validate (US2 complete, repeatable workflow documented)
+3. T011 → T012 → validate (US3 complete, CI automation confirmed)
+4. T013–T014 (polish)
+
+---
+
+## Notes
+
+- All signing steps require `if: runner.os == 'macOS'` guards — Linux/Windows matrix jobs are unchanged
+- `APPLE_TEAM_ID` is used in the signing identity string: `"Developer ID Application: $(APPLE_TEAM_ID)"` — confirm the exact identity string from `security find-identity -v -p codesigning` on the developer machine
+- `xcrun notarytool submit --wait` typically takes 1–5 minutes; the 15-minute SC-004 limit is well within GitHub Actions job timeout defaults
+- Keychain cleanup step uses `if: always()` so it runs even if signing or notarization fails
+- The bare-binary artifact (`risus-macos-arm64`) is no longer uploaded; only the zip is distributed