How to run Shopify's app security check in GitHub Actions

Shopify's new app security check looks for the mistakes that show up most often in Shopify apps. These include unsafe innerHTML, open redirects, unverified app proxy requests, secrets committed to your repo, and more.

Shopify recommends running it on every release. The easiest way to make sure that happens is to stop relying on memory and let CI run it for you.

This guide sets up two jobs:

  1. A fast scan on every pull request. It needs no secrets, takes seconds, and fails the build when it finds a high-severity issue.
  2. An agent review before you ship. Claude Code runs the checks that need judgment, like "does this endpoint check who's calling it?" This one costs API credits, so run it on releases or on demand.

If you're wondering why this matters, read The fake register, our breakdown of how a script tag hijacked Shopify checkouts.

How the check works

The check has two parts:

shopify app security review combines both parts into one report.

A few things make this work well in CI:

Before you start

You'll need:

Step 1: Run the check locally

Run it once on your own machine first, so you know what CI is going to find. From your app directory:

shopify app security check

Fix anything high severity before you turn on blocking. Otherwise, your first pull request will fail for issues it didn't cause.

Then keep the results out of Git. Add this line to your .gitignore:

.shopify/app-security/

Have more than one app config?

If your repo has more than one shopify.app*.toml file, CI can't stop and ask which one to use. Pass --config with the config name, like --config production. Run the job once per config if you need to scan more than one.

Is your backend somewhere else?

The check scans your app directory by default. If your backend or a shared library lives outside it, add it with --include-dir:

shopify app security check --include-dir ../backend

To see exactly which files the check will scan, add --list-files. It prints the list and stops.

Step 2: Block risky pull requests

Create .github/workflows/shopify-app-security.yml:

name: Shopify app security

on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  scan:
    name: Security scan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5

      - uses: actions/setup-node@v5
        with:
          node-version: 22

      - name: Install Shopify CLI
        run: npm install -g @shopify/cli@4.9.4

      - name: Run the security check
        run: shopify app security check --no-color --skip-instructions --blocking high

      - name: Upload the results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: shopify-app-security-scan
          path: .shopify/app-security/
          include-hidden-files: true

Here's what each flag does:

FlagWhat it does
--blocking highFails the job when the scan finds a high-severity issue
--skip-instructionsSkips the agent instructions, since this job only runs the scan
--no-colorKeeps the log readable in GitHub

A few notes:

Once it's working, tighten it. Change --blocking high to --blocking medium when you're ready to hold medium issues to the same bar.

Step 3: Add an agent review before you ship

The scan can't answer every question. Some checks need something that can read your code and reason about it. This job uses the Claude Code GitHub Action to do that.

It doesn't need the Shopify AI Toolkit. shopify app security instructions prints the full workflow for the agent to follow.

First, add your API key as a repository secret named ANTHROPIC_API_KEY in Settings > Secrets and variables > Actions.

Then add this job to the same workflow file, and add the new triggers to the on section:

on:
  pull_request:
    types: [opened, synchronize, reopened, labeled]
  push:
    branches: [main]
  release:
    types: [published]
  workflow_dispatch:

# ...

  agent-review:
    name: Agent security review
    if: >-
      github.event_name == 'release' ||
      github.event_name == 'workflow_dispatch' ||
      contains(github.event.pull_request.labels.*.name, 'security-review')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5

      - uses: actions/setup-node@v5
        with:
          node-version: 22

      - name: Install Shopify CLI
        run: npm install -g @shopify/cli@4.9.4

      - name: Run the agent checks
        uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          prompt: |
            Review this Shopify app for security issues.

            Run `shopify app security instructions` and follow the workflow it
            prints, step by step. When you're done, record your findings with
            `shopify app security record`.

            Only report issues. Don't edit any files in the app.
          claude_args: >-
            --allowedTools "Bash(shopify app security:*),Bash(cat:*),Read,Glob,Grep,Write"

      - name: Show the combined results
        shell: bash
        run: |
          shopify app security review --no-color --blocking high | tee review.txt
        # The review fails the job on high-severity issues, from either the scan or the agent

      - name: Add the results to the job summary
        if: always()
        shell: bash
        run: |
          {
            echo '## Shopify app security review'
            echo '```'
            cat review.txt
            echo '```'
          } >> "$GITHUB_STEP_SUMMARY"

      - name: Upload the results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: shopify-app-security-review
          path: .shopify/app-security/
          include-hidden-files: true

This job runs in three cases:

That keeps your API costs predictable. Add the label to pull requests that touch auth, webhooks, app proxies, or storefront code.

Two details matter here:

Keep secrets away from untrusted code

Use the pull_request trigger, not pull_request_target. With pull_request, pull requests from forks don't get your secrets, so the agent job won't run for them. That's what you want. Don't hand an API key to code you haven't reviewed.

Reading the results

When a job fails, open the run and check the job summary or the uploaded artifact. Each finding includes:

Every check ID has its own page in Shopify's check list, with a full explanation and an example of the fix. Fix critical and high issues first, then push again.

Need to skip generated code or test fixtures? Use --exclude, and quote each glob so your shell doesn't expand it:

shopify app security check --exclude '**/generated' --exclude 'test/fixtures/**'

Exclusions apply to check only, so pass them every time you run it.

What it won't catch

A passing check is a good sign, not a guarantee. Shopify says this too. A few things to keep in mind:

A note on telemetry

The check sends Shopify the number of findings from each run, but not the details. To turn off Shopify CLI usage reporting in CI, set SHOPIFY_CLI_NO_ANALYTICS: 1 as an environment variable on the job.

Wrap up

The fast scan costs nothing and runs in seconds, so put it on every pull request. Save the agent review for releases and risky changes. Between the two, the most common Shopify app mistakes never make it to a merchant's store.