ci: add GitHub Actions security review workflow
What changed, and why it matters
This commit adds a new GitHub Actions workflow that runs an automated security review on pull requests using Anthropic's Claude Code tool. It is a defensive security measure, not a vulnerability fix. The workflow is intentionally restricted so it only auto-runs for trusted maintainers; external contributors must trigger it manually after review. The change also updates the existing Python review script to read GitHub Actions environment variables instead of Cirrus CI variables.
No action required; this is a defensive CI addition. If reviewing the workflow, verify that the author-association gate and manual-dispatch policy for external PRs are enforced, that CLAUDE_CODE_OAUTH_TOKEN is stored as a repository secret with minimal scope, and that pull_request_target checkout of the PR head is acceptable for the project's threat model.
Security signals we found
New CI workflow for automated security review
pull_request_target trigger with explicit checkout of PR head
Author-association gate to prevent untrusted automatic execution
External contributors require manual workflow_dispatch trigger
Comment notes risk of PR modifying the called Python script and potential API key exfiltration
GITHUB_TOKEN write permission is intentionally disabled (commented out)
Pinned third-party actions to specific commit hashes
Evidence from the diff
The diff introduces .github/workflows/security-review.yml, a pull_request_target and workflow_dispatch triggered job that checks out the PR head, installs the Claude Code CLI, sets up Python 3.14 and project dependencies, builds libsecp256k1, and runs contrib/ci/claude_security_review.py with CLAUDE_CODE_OAUTH_TOKEN. The job is gated by author_association to limit automatic execution to OWNER/MEMBER/COLLABORATOR. The Python script is updated from Cirrus CI env vars (CIRRUS_PR, CIRRUS_BASE_BRANCH, CIRRUS_REPO_FULL_NAME, CIRRUS_TASK_ID) to GitHub Actions equivalents (PR_NUMBER, BASE_BRANCH, GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_SERVER_URL). GITHUB_TOKEN-based PR commenting is left commented out.
Changed components
.github/workflows/security-review.ymlcontrib/ci/claude_security_review.pyInspect captured patch +108 / −12
diff --git a/.github/workflows/security-review.yml b/.github/workflows/security-review.yml
new file mode 100644
index 0000000..021ae10
--- /dev/null
+++ b/.github/workflows/security-review.yml
@@ -0,0 +1,93 @@
+name: security-review
+
+on:
+ pull_request_target:
+ workflow_dispatch:
+ inputs:
+ pr_number:
+ description: 'PR number to review'
+ required: true
+ type: string
+
+permissions:
+ contents: read
+# pull-requests: write # required for the script to post review comments via GITHUB_TOKEN
+
+concurrency:
+ group: security-review-pr-${{ github.event.pull_request.number || inputs.pr_number }}
+ cancel-in-progress: true
+
+jobs:
+ security-review:
+ name: "security review: Claude Code"
+ runs-on: ubuntu-24.04
+
+ # Auto-run only for maintainers (and their forks).
+ # External contributors trigger manually so we can first review if their PR modifies
+ # the Python script this task calls (they could attempt to exfiltrate the api key through the script).
+ if: |
+ github.event_name == 'workflow_dispatch' ||
+ contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association)
+
+ env:
+ LD_LIBRARY_PATH: contrib/_saved_secp256k1_build/
+ ELECTRUM_ECC_DONT_COMPILE: "1"
+ steps:
+ - name: Checkout PR head
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ ref: refs/pull/${{ github.event.pull_request.number || inputs.pr_number }}/head
+ fetch-depth: 0
+
+ - name: Setup Node
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+ with:
+ node-version: '20'
+
+ - name: Install Claude Code CLI
+ run: sudo npm install -g @anthropic-ai/claude-code
+
+ # install Python and dependencies so the llm can quickly access the dependencies source and execute code/tests
+ - name: Setup Python
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
+ with:
+ python-version: '3.14'
+ cache: 'pip'
+ cache-dependency-path: |
+ contrib/requirements/requirements-ci.txt
+ contrib/requirements/requirements.txt
+
+ - name: Cache libsecp256k1
+ id: cache-libsecp
+ uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ with:
+ path: contrib/_saved_secp256k1_build
+ key: libsecp-${{ runner.os }}-${{ hashFiles('contrib/make_libsecp256k1.sh') }}
+
+ - name: Build libsecp256k1
+ if: steps.cache-libsecp.outputs.cache-hit != 'true'
+ run: |
+ sudo apt-get update
+ sudo apt-get -y install automake libtool
+ ./contrib/make_libsecp256k1.sh
+ mkdir -p contrib/_saved_secp256k1_build
+ cp electrum/libsecp256k1.so.* contrib/_saved_secp256k1_build/
+
+ - name: Install Qt/QML runtime deps
+ run: |
+ sudo apt-get update
+ sudo apt-get -y install libgl1 libegl1 libxkbcommon0 libdbus-1-3
+
+ - name: Install Python dependencies
+ run: |
+ pip install -r contrib/requirements/requirements-ci.txt
+ pip install ".[tests,qml_gui]"
+
+ - name: Run Claude Code security review
+ env:
+ PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
+ BASE_BRANCH: ${{ github.event.pull_request.base.ref || 'master' }}
+ # needs to be set in the GitHub repository settings
+ CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
+ # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # can be enabled to make claude comment on PRs
+ run: python3 contrib/ci/claude_security_review.py
diff --git a/contrib/ci/claude_security_review.py b/contrib/ci/claude_security_review.py
index b0d1d8b..b066310 100644
--- a/contrib/ci/claude_security_review.py
+++ b/contrib/ci/claude_security_review.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
-Cirrus CI task: Claude Code security review for Electrum pull requests.
+GitHub Actions job: Claude Code security review for Electrum pull requests.
Runs Claude Code against the PR diff to detect critical security
vulnerabilities. Optionally posts findings as a GitHub PR comment.
@@ -15,11 +15,13 @@ Environment variables:
CLAUDE_CODE_OAUTH_TOKEN -- OAuth token from `claude setup-token` (MAX subscription)
Optional:
GITHUB_TOKEN -- GitHub token for posting PR comments
- Set by Cirrus CI:
- CIRRUS_PR -- PR number (empty if not a PR build)
- CIRRUS_BASE_BRANCH -- target branch of the PR
- CIRRUS_REPO_FULL_NAME -- e.g. "spesmilo/electrum"
- CIRRUS_TASK_ID -- current Cirrus task ID
+ Set by the workflow:
+ PR_NUMBER -- PR number (empty if not a PR build)
+ BASE_BRANCH -- target branch of the PR
+ Set by GitHub Actions runtime:
+ GITHUB_REPOSITORY -- e.g. "spesmilo/electrum"
+ GITHUB_RUN_ID -- current workflow run ID
+ GITHUB_SERVER_URL -- e.g. "https://github.com"
"""
import json
@@ -151,8 +153,9 @@ def post_github_comment(body: str, *, repo: str, pr: str) -> None:
print("GITHUB_TOKEN not set -- skipping PR comment.")
return
- task_id = os.environ.get("CIRRUS_TASK_ID", "")
- log_url = f"https://cirrus-ci.com/task/{task_id}" if task_id else ""
+ run_id = os.environ.get("GITHUB_RUN_ID", "")
+ server_url = os.environ.get("GITHUB_SERVER_URL", "https://github.com")
+ log_url = f"{server_url}/{repo}/actions/runs/{run_id}" if run_id else ""
comment = (
f"## Security Review -- Issues Found\n\n"
@@ -195,17 +198,16 @@ def main() -> int:
print("Claude Code Security Review")
print(separator)
- pr = os.environ.get("CIRRUS_PR", "").strip()
+ pr = os.environ.get("PR_NUMBER", "").strip()
if not pr:
- print("Not a PR build (CIRRUS_PR is empty). Skipping.")
+ print("Not a PR build (PR_NUMBER is empty). Skipping.")
return 0
if not os.environ.get("CLAUDE_CODE_OAUTH_TOKEN", "").strip():
print("ERROR: CLAUDE_CODE_OAUTH_TOKEN is not set.")
return 2
- repo = os.environ.get("CIRRUS_REPO_FULL_NAME", "").strip()
- base_branch = os.environ.get("CIRRUS_BASE_BRANCH", "master").strip()
+ base_branch = os.environ.get("BASE_BRANCH", "master").strip()
print(f"PR #{pr} -> base branch: {base_branch}")
print("\nFetching base branch...")
@@ -259,6 +261,7 @@ def main() -> int:
verdict = parse_verdict(review)
if verdict == VERDICT_FAIL:
+ repo = os.environ.get("GITHUB_REPOSITORY", "").strip()
print("\nVERDICT: FAIL -- Critical or high severity issues found.")
post_github_comment(review, repo=repo, pr=pr)
return 1
Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.