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:
If you're wondering why this matters, read The fake register, our breakdown of how a script tag hijacked Shopify checkouts.
The check has two parts:
.shopify/app-security/<config name>/deterministic-findings.json.agent-checks.json. A coding agent investigates each one, then saves what it found with shopify app security record.shopify app security review combines both parts into one report.
A few things make this work well in CI:
--client-id.--blocking high (or medium, or low) to exit with code 1 when it finds an issue at or above that severity.You'll need:
shopify.app.toml filenpm install -g @shopify/cli@latest)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/
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.
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.
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:
| Flag | What it does |
|---|---|
--blocking high | Fails the job when the scan finds a high-severity issue |
--skip-instructions | Skips the agent instructions, since this job only runs the scan |
--no-color | Keeps the log readable in GitHub |
A few notes:
include-hidden-files: true. The results live in .shopify/, and upload-artifact skips hidden folders unless you tell it not to.main. Now a failing scan blocks the merge.Once it's working, tighten it. Change --blocking high to --blocking medium when you're ready to hold medium issues to the same bar.
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:
security-review labelThat keeps your API costs predictable. Add the label to pull requests that touch auth, webhooks, app proxies, or storefront code.
Two details matter here:
shell: bash turns on pipefail. Without it, tee hides the exit code from review, and a failing review would pass.shopify app security commands and read files. It can write its findings document, but the prompt tells it not to edit your app. Keep the tool list tight. The agent is reading code from a pull request, and that code could try to give it instructions.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.
When a job fails, open the run and check the job summary or the uploaded artifact. Each finding includes:
OPEN_REDIRECTEvery 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.
A passing check is a good sign, not a guarantee. Shopify says this too. A few things to keep in mind:
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.
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.