Theme Tooling and Workflow
|
This content was generated with the assistance of AI and should be verified against the official documentation of each platform before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
This page was written against Shopify CLI 5.0.0 (published 2026-10-27, Node.js 22.12 or later) with the Theme Check engine
@shopify/theme-check-node 3.31.0 that the CLI embeds, and was checked on 2026-10-27. It covers the day-to-day tooling
of a Liquid theme project: the shopify theme commands that create, preview, upload, download, publish, lint, share and
package a theme, how Theme Check is configured and run, how the Shopify GitHub integration keeps a branch and a store
theme in step, and how to run the same commands unattended in a CI pipeline with a Theme Access password. The tooling
documentation consulted does not tie any of these tools to a particular Shopify plan, so everything below applies to any
store that can host a theme, including development stores.
The running example is the Pocket Coffee storefront: a Spanish and English theme in the repository
pocket-coffee-theme, a development store pocket-coffee-dev and a production store pocket-coffee. The theme adds a
small snippet, the roast date badge, to the Skeleton starting point. The store domains are placeholders, and no
credentials appear anywhere on this page.
Commands that need no store were run for this page with Shopify CLI 5.0.0: shopify theme init, shopify theme check
(text and JSON output, configuration files, presets, fail levels, disable comments, environments), shopify theme
package and every --help. The commands that talk to a store (dev, push, pull, publish, share, list,
open, console, profile, preview, duplicate, rename, delete) were not executed, because that would need a
Shopify account, a store and a login. They are documented from the CLI help of the same version and from the official
documentation. The GitHub Actions workflows were parsed as YAML and their shell steps were exercised locally where they
need no store, but they were not run on GitHub.
The pieces and how they fit
Four tools cooperate in a theme workflow, and the rest of the page takes them in turn.
| Tool | What it does | Where it runs |
|---|---|---|
Shopify CLI (the |
Scaffolds a theme, serves a local preview backed by store data, uploads and downloads theme files, publishes a theme, packages a zip and runs Theme Check. |
Developer machine and CI runner. Needs Node.js 22.12 or later, and Git for |
Theme Check |
A linter for the Liquid and JSON files of a theme and of theme app extensions. It is built into the CLI
( |
Same as the CLI. It needs no login, only a cached copy of the Liquid reference data. |
Shopify GitHub integration |
Connects a GitHub branch to a store theme and keeps the two in step in both directions. |
On Shopify and GitHub servers. Configured once in the Shopify admin. |
Theme Access app |
Generates passwords that give a developer or a CI job the |
On the store. The password is passed to the CLI as |
A typical Pocket Coffee change travels through them like this. A developer scaffolds or pulls the theme, runs a local preview against the development store, lints, and pushes a branch. CI lints again and uploads the branch to a throwaway development theme so that a reviewer gets a preview link. After the merge, CI uploads the release as a new unpublished theme and publishes it. Merchant edits made later in the theme editor come back to the repository as commits made by Shopify.
Install, upgrade and prerequisites
Shopify CLI is one npm package for apps, themes and Hydrogen storefronts. The documented requirements are Node.js 22.12 or later, a package manager (npm, Yarn 1.x or pnpm) and Git 2.28.0 or later. The 5.0.0 changelog additionally requires Git 2.38.0 or later for the operations that use Git, which concerns the app commands more than the theme commands.
npm install -g @shopify/cli@latest
shopify version
Yarn (yarn global add @shopify/cli@latest), pnpm (pnpm install -g @shopify/cli@latest) and Homebrew (brew tap
shopify/shopify then brew install shopify-cli) are the other documented routes. A theme repository can also depend
on @shopify/cli as a development dependency so that every developer and the CI runner use the same version.
Three behaviours of recent versions matter for a team.
-
Since CLI 4.0.0 (2026-05-21) the CLI follows SemVer and upgrades itself with the package manager that installed it. It does not auto-upgrade in CI, in project-local installations or when the next release is a new major version.
shopify config autoupgrade offturns it off,shopify config autoupgrade onturns it on andshopify config autoupgrade statusshows the state.shopify upgradeupgrades by hand. -
CLI 4.0.0 removed
shopify theme serve;shopify theme devreplaces it. -
Usage data is collected by default (commands, flags, timings, errors and identifiers such as the store domain). Set
SHOPIFY_CLI_NO_ANALYTICS=1orOPT_OUT_INSTRUMENTATION=trueto opt out. The examples on this page were run with the first variable set.
Source: Shopify CLI for themes
All theme commands share a set of flags that are worth knowing once, because each of them also has an environment variable, which is how CI configures the CLI without long command lines.
| Flag | Environment variable | Meaning |
|---|---|---|
|
|
Store prefix ( |
|
|
A Theme Access password or a custom app Admin API token. Without it the CLI uses your Shopify account login. |
|
|
Name of an environment defined in |
|
|
Directory to run in; defaults to the current directory. |
|
|
Disables prompts and browser authentication, so a missing value is an error instead of a question. |
|
|
Prints the result as JSON and disables colour. |
|
|
Plain output; more output, which may include sensitive data. |
|
|
Account alias to authenticate with. |
Authentication and store targeting
The CLI accepts three kinds of credentials for a store.
-
A Shopify account login: the store owner, a staff account with the Themes permission, or a collaborator account with the Themes permission. Any command that needs the store opens a browser to log in.
shopify auth logoutlogs out and, as a side effect, deletes your development theme. -
A Theme Access password, passed with
--passwordor theSHOPIFY_CLI_THEME_TOKENenvironment variable. This is the right choice for CI and for handing a freelancer access to one store. -
A custom app Admin API token passed the same way. The app needs the
read_themesandwrite_themesscopes. For hot reload it also needsunauthenticated_read_contentfor the Storefront API, and the tokens must then be supplied as environment variables instead of the--passwordflag.
The first command against a store names it with --store, for example shopify theme dev --store pocket-coffee-dev,
and later commands reuse it. shopify theme info prints the store, the theme and the CLI environment currently in use.
Storefronts that are protected by the storefront password additionally need --store-password (environment variable
SHOPIFY_FLAG_STORE_PASSWORD); since 2026-09-23 the theme CLI needs version 3.84.0 or later for password-protected
shops, which every 5.x release satisfies.
Theme Access passwords
Theme Access is a free app from the Shopify App Store. The store owner, or a staff or collaborator account with the
Themes permission under Online Store, installs it and creates a password for a developer. The password is write-scoped
for themes only (write_themes), so it cannot read orders or customers. The developer receives an email with a link;
the link expires after 7 days or after the password is viewed once, so it has to be saved immediately. A lost password is
replaced by resending the email (while the link is valid) or by deleting the password and creating another. Deleting a
password revokes that developer’s access. The app used to be called Theme Kit Access, and passwords from either name
work interchangeably, including with the deprecated Theme Kit.
For Pocket Coffee the developer password of the production store is created by the owner, stored as the GitHub secret
SHOPIFY_CLI_THEME_TOKEN, and never written to shopify.theme.toml in the repository. The store domain is not secret and
goes into a repository variable.
Source: Manage theme access
Project layout, .shopifyignore and development themes
Most theme commands only run in a directory that matches the default theme structure, which describes a buildless theme or the output of a build step.
pocket-coffee-theme
├── assets
├── blocks
├── config
├── layout
├── locales
├── sections
├── snippets
└── templates
└── customers
shopify theme init produced exactly these eight folders on 2026-10-27 together with some repository files (README,
LICENSE, CODE_OF_CONDUCT, CONTRIBUTING, .gitignore, .gitattributes and .shopifyignore). If a project uses a build
pipeline, run the CLI from the directory that holds the generated theme, or point --path at it.
Files that live in the repository but must never be uploaded or overwritten go into .shopifyignore at the theme root.
It accepts exact file names, wildcards and regular expressions between slashes, and the --ignore flag of push and
pull adds to it rather than replacing it. The Skeleton copy of the file ships with commented examples of the three
forms.
# Keep the merchant's customizations out of CLI operations
config/settings_data.json
templates/*.json
/\.(txt|gif|bat)$/
Excluding config/settings_data.json and the JSON templates is the usual way to stop a deployment from overwriting what a
merchant configured in the theme editor. The trade-off is that new settings or templates that the release needs must then
be created by other means, so use the exclusion only when the merchant owns those files.
Theme roles and development themes
A store holds themes in three roles, which the JSON output of the commands reports as role: live (the published
theme), unpublished (in the library, not visible to shoppers) and development. A development theme is a temporary,
hidden theme that the CLI creates for you in the store you develop against. It lets you test with the store’s real data,
open it in the theme editor and share a password-protected preview link. Development themes do not count toward the
store’s theme limit, are deleted after seven days of inactivity and are deleted at once by shopify auth logout. If a
preview must outlive a logout, push to an unpublished theme or use shopify theme share.
Environments with shopify.theme.toml
Flags such as --store, --theme and --ignore add up quickly. A shopify.theme.toml at the project root names reusable
sets of flags, each under [environments.<name>]. Any flag of the command can be used except environment, path and
verbose, which the documentation says are ignored (see below for how path behaves with several environments). Boolean
flags are set to true and repeatable flags are arrays.
[environments.default]
store = "pocket-coffee-dev.myshopify.com"
ignore = ["config/settings_data.json"]
[environments.staging]
store = "pocket-coffee-dev.myshopify.com"
theme = "Staging"
path = "."
[environments.production]
store = "pocket-coffee.myshopify.com"
path = "."
ignore = ["config/settings_data.json", "templates/*.json"]
Use an environment with shopify theme push --environment staging (or -e staging). The CLI prints the flags it took
from the environment. An environment called default is applied to every theme command without -e. Values are applied
in this order of precedence, from strongest to weakest: flags typed on the command line, environment variables such as
SHOPIFY_CLI_THEME_TOKEN, then the TOML environment. A flag in an environment that the running command does not accept is
ignored, a wrongly typed value or mutually exclusive flags are errors, and a few shared flags change meaning between
commands (force on delete deletes without asking), so keep destructive flags out of environments.
Several environments in one run are supported by check, delete, info, list, publish, pull, push, rename
and share: repeat the flag, for example shopify theme push -e staging -e production. With several environments the CLI
does not prompt, so every required flag must be in the command or the file, and an environment that lacks one is skipped
with a warning. In this run of 5.0.0 shopify theme check -e staging -e production reported "Missing required flags in
environment configuration" and named path until each environment contained path = "."; after that, check ran once
per environment and prefixed each summary with the environment name. With a default environment present the output of
every command also carries a [default] prefix.
Because the file can hold passwords, the docs show a password key in an environment. For a repository that is shared,
leave the key out and supply the password through SHOPIFY_CLI_THEME_TOKEN, as the CI workflows below do.
Creating a theme: shopify theme init
shopify theme init [name] clones a Git repository into a new folder named name (a prompt asks for it when omitted).
Without options it clones Shopify’s Skeleton theme from https://github.com/Shopify/skeleton-theme.git, then removes the
.git folder so that the copy starts as plain files.
| Flag | Meaning |
|---|---|
|
Git URL to clone. Default |
|
Download the latest release of the clone URL instead of its default branch. |
|
Common flags described above. |
export SHOPIFY_CLI_NO_ANALYTICS=1
shopify theme init pocket-coffee-theme
cd pocket-coffee-theme
git init -b main
git add -A
git commit -m "Start from Skeleton"
Source: shopify theme init
On 2026-10-27 the first run cloned Skeleton, removed .git and left 39 inspectable files that pass Theme Check without an
offense. A later run from the same machine failed because the command asks the GitHub releases API about the repository
and the anonymous rate limit (HTTP 403, "API rate limit exceeded") had been used up. This is a property of running the
command from a shared address, not of the theme. Retrying later, or cloning with plain git clone and deleting .git,
gives the same files.
The CLI help carries a caution for Theme Store authors: the example theme may be used as a starting point, but the submitted theme has to be substantively different from existing themes. The Theme Store requirements add that Skeleton is the only approved codebase and that themes built on or derived from Dawn or Horizon are not eligible. Dawn 16.0.0 and Horizon 4.3.0 are valid starting points for a merchant’s own storefront, which is the Pocket Coffee case, but the choice between the three is a design decision covered with the reference themes, not a tooling one.
Source: Theme Store requirements
Previewing: shopify theme dev
shopify theme dev uploads the local theme to the store as a development theme and keeps it in step while you edit.
It prints three things: a local URL, by default http://127.0.0.1:9292, that serves the store’s data through your local
files; a link to the theme editor for the development theme in the admin; and a preview link that other developers can
open. If a development theme already exists for the current environment, the command replaces it with your local files,
unless you use --theme-editor-sync. Checkout customizations cannot be previewed on the local URL.
shopify theme dev --store pocket-coffee-dev --open
shopify theme dev -e staging --live-reload full-page --ignore "config/settings_data.json"
Source: shopify theme dev
| Flag (environment variable) | Meaning |
|---|---|
|
Which store, which credential, the storefront password if the shop is password protected, and which TOML environment. |
|
Develop against an existing remote theme, by ID or name, instead of the development theme. |
|
Allows development directly on the live theme. Use it deliberately: each save becomes visible to shoppers. |
|
|
|
|
|
Hot reload only, or skip, files matching a pattern. Repeatable. |
|
Do not delete remote files when a local file is deleted, including files already missing locally when the command starts. |
|
Writes changes made in the theme editor back into the local theme files. |
|
With |
|
Network interface (default |
|
Open the preview in the default browser; or name a file to update when the process is idle, or a URL that receives a webhook about file changes. |
|
For multi-preset Theme Store themes, applies the files from |
|
Injects the standard events inspector into storefront HTML so that analytics events can be inspected. |
Source: shopify theme dev
Two pitfalls follow from the behaviour above. The server runs only in a directory with the default theme structure, so a
mistyped --path ends in an error. And a dev server that finishes or crashes leaves the development theme in the store; it
disappears after seven days of inactivity or on logout.
Uploading: shopify theme push
shopify theme push uploads local files to a remote theme and, unless told otherwise, deletes remote files that do not
exist locally. Without a target it asks which theme to overwrite. In non-interactive environments it needs exactly one of
--development, --live, --theme or --unpublished, and --allow-live when the target is the live theme.
| Flag (environment variable) | Meaning |
|---|---|
|
Push to your development theme. |
|
A unique label, such as a pull request number or a branch name. Reuses the development theme that has this context or creates one. This is what lets each pull request own one preview theme. |
|
Push to the live theme; |
|
ID or name of an existing remote theme. With |
|
Create a new unpublished theme and push to it. |
|
Publish the theme as live after the upload. |
|
Keep remote files that do not exist locally. |
|
Upload only, or skip, matching files; quote wildcards. Repeatable. |
|
Run Theme Check first and refuse to push if it reports errors. Warnings are allowed. |
|
Machine-readable result, described below. |
|
Apply files from |
# Interactive: pick the theme from a list
shopify theme push
# A throwaway preview owned by pull request 42, safe to repeat
shopify theme push --development --development-context pr-42 --nodelete --json
# A named, unpublished release candidate, refused if Theme Check has errors
shopify theme push --unpublished --theme "release-2026-10-27" --strict --json
# Replace the live theme directly (non-interactive runs must say so)
shopify theme push --live --allow-live --strict
Source: shopify theme push
With --json the result conforms to the ThemePushJsonResult schema, which --json-schema prints. For one environment it
is an object with status (success or partial), changed, a theme object (id as a decimal string, not a GraphQL
ID, plus name, role, storeDomain, previewUrl and editorUrl) and issues (an array of filePath and message
for files that failed to upload). With several environments the result is { "environments": [...] }, where each entry
holds either a result (including status values skipped for an unsafe directory and cancelled) or an error of
type abort, bug or external.
{
"status": "success",
"changed": true,
"issues": [],
"theme": {
"id": "108267175958",
"name": "MyTheme",
"role": "unpublished",
"storeDomain": "mystore.myshopify.com",
"editorUrl": "https://mystore.myshopify.com/admin/themes/108267175958/editor",
"previewUrl": "https://mystore.myshopify.com/?preview_theme_id=108267175958"
}
}
Source: shopify theme push: JSON output
Extracting the theme ID in a shell step works with jq -r '.theme.id'; the command printed 108267175958 for this sample.
A partial status or a non-empty issues array means some files did not upload, so a deployment script should fail on
anything other than success with empty issues.
Downloading: shopify theme pull
shopify theme pull downloads the files of a remote theme into the current directory. It is the way to bring a
merchant’s edits (theme editor settings, section order, JSON templates) into the repository when the GitHub integration is
not in use, and the way to start a project from a store that has no repository yet.
| Flag (environment variable) | Meaning |
|---|---|
|
Which remote theme to read. Non-interactive runs need one of the three. |
|
Keep local files that do not exist remotely. |
|
Download only, or skip, matching files; quote wildcards. |
|
Result conforming to the |
shopify theme pull --live --only "templates/*.json" --only "config/settings_data.json"
git diff --stat
Source: shopify theme pull
Pull overwrites local files that differ, so run it on a clean working tree or a new branch and review the diff before committing. Pulling only JSON templates and settings is a safe routine for capturing merchant changes without touching code.
Publishing and sharing
shopify theme publish makes an unpublished theme the live theme. It does not upload anything: push first to create the
theme, then publish it by ID or name. It asks for confirmation, which --force (SHOPIFY_FLAG_FORCE) skips and which is
required in non-interactive runs, as is --theme (SHOPIFY_FLAG_THEME_ID). --json returns a ThemePublishResult with
the theme (including a processing flag).
shopify theme push --unpublished --theme "release-2026-10-27" --json
shopify theme publish --theme 108267175958 --force
Source: shopify theme publish
shopify theme share uploads the local files as a new unpublished theme with a randomized name and prints a preview link,
which makes it a quick way to show work to a stakeholder without touching named themes. It accepts --store,
--password, --environment, --listing and --json (ThemeShareResult), and it can be repeated, so remove old shared
themes with shopify theme delete or in the admin. Unlike theme dev, the shared theme survives shopify auth logout.
shopify theme share --store pocket-coffee-dev
Source: shopify theme share
Packaging: shopify theme package
shopify theme package zips the local theme so it can be uploaded in the admin or submitted to the Theme Store. Only the
folders of the default structure are included, plus listings when it exists (required for multi-preset themes). The file
is named theme_name-theme_version.zip after theme_name and theme_version in config/settings_schema.json, and --json
prints its absolute path as { "path": "..." }.
shopify theme package
shopify theme package --json
Source: shopify theme package
In the Skeleton copy used for this page the command wrote Skeleton-0.1.0.zip, because settings_schema.json declares
"theme_name": "Skeleton" and "theme_version": "0.1.0". The archive contained only assets, blocks, config,
layout, locales, sections, snippets and templates, and the roast date snippet added to the project, and
contained no README, licence or Theme Check configuration. Rename the theme and bump the version in the schema file before
packaging Pocket Coffee, or every release will be called Skeleton-0.1.0. The generated .gitignore already excludes *.zip.
The remaining theme commands
The other commands work against a store and are listed here so that the whole shopify theme topic of CLI 5.0.0 is
covered.
| Command | Purpose and notable flags |
|---|---|
|
Lists themes with ID and role. |
|
Shows the store, the theme and CLI environment in use, for a development theme ( |
|
Prints and optionally opens the preview links of a remote theme: |
|
A Liquid REPL that evaluates expressions against real store data. |
|
Profiles the Liquid rendering time of one page ( |
|
Applies a JSON overrides file ( |
|
Copies a remote theme ( |
|
Renames a theme ( |
|
Deletes remote themes: |
|
Downloads the metafield definitions of the shop into a local file. |
|
Starts the Liquid language server on stdio. It needs no login and is what editors connect to for completion, hover documentation, formatting, Theme Check diagnostics and navigation. |
|
Runs Theme Check; covered in the next section. |
Theme Check
Theme Check is the linter for Liquid and JSON in themes and in theme app extensions. It finds syntax errors, missing
templates and assets, unused variables and snippets, unknown and deprecated tags and filters, translation problems and
performance problems, and it links each finding to documentation for the failing check. It runs through the CLI on demand
or in CI, through the Liquid language server in editors, and in the Shopify Liquid VS Code extension. Checks are written in
TypeScript, so a team can add its own (require in the configuration).
The engine needs the Shopify Liquid reference data (filters, tags, objects, schemas and system translations). It downloads
it the first time it runs and caches it on the machine (on macOS under ~/Library/Caches/theme-liquid-docs-nodejs/).
The cached copy used for this page held 155 filters, 33 tags and 142 objects. Later runs worked with the network blocked,
so a CI job needs network access on its first run and benefits from caching that directory afterwards.
Running it
shopify theme check # lint the current directory
shopify theme check --path ./pocket-coffee-theme
shopify theme check -o json # machine-readable output
shopify theme check --fail-level warning # exit code 1 for warnings too
shopify theme check -a # apply automatic corrections
shopify theme check -C theme-check:all # use the strictest preset
shopify theme check --list # enabled checks with severity, description and docs link
shopify theme check --print # the effective configuration
shopify theme check --init # write a starter .theme-check.yml
shopify theme check -v # Theme Check version
Source: shopify theme check
| Flag (environment variable) | Meaning |
|---|---|
|
A configuration file path, overriding |
|
Fixes offenses that have an automatic correction (for example |
|
|
|
Minimum severity that makes the exit code non-zero: |
|
Mutually exclusive helper modes described in the listing above. |
|
One or several TOML environments (each needs |
A bad path is reported cleanly: shopify theme check --path /nonexistent ended with "A path was explicitly provided but
doesn’t exist."
What the output looks like: the Pocket Coffee snippet
To see the checks work, a deliberately flawed first draft of the roast date badge was added to the Skeleton project as
snippets/roast-date-badge.liquid.
{% comment %} Roast date badge for Pocket Coffee {% endcomment %}
{% assign unused_note = 'x' %}
{% include 'icon-coffee' %}
{%- assign roast = product.metafields.custom.roast_date -%}
{%- if roast -%}
<p class="roast-badge">{{ 'products.product.roast_date' | t }}: {{ roast | date: '%Y-%m-%d' }}</p>
<img src="{{ 'badge.png' | asset_url }}" alt="">
{%- endif -%}
Source: Theme Check checks reference
shopify theme check on this project reported 7 offenses in the one file (40 files inspected) and exited with code 1:
| Check | Severity | Message in this run |
|---|---|---|
|
error |
Missing width and height attributes on img tag. |
|
error |
|
|
error |
|
|
error |
|
|
warning |
Use the |
|
warning |
The snippet is not referenced by any other file. |
|
warning |
The variable |
-o json returned {"valid":false,"issues":[...],"errorCount":4,"warningCount":3,"infoCount":0}, where every issue
carries filePath (absolute), check, severity, startRow, startColumn, endRow, endColumn and message. The
automatic correction (-a) changed nothing for this file, since none of these seven offenses has a safe automatic fix.
The corrected version documents its parameter with the doc tag (the UnusedDocParam and ValidDocParamTypes checks
verify it), uses a translation key that exists, drops the fixed-size image and the stale assign, and is rendered from the
product section.
{%- doc -%}
Shows the roast date of a product as a badge.
@param {product} product - The product whose roast date is shown
{%- enddoc -%}
{%- assign roast = product.metafields.custom.roast_date -%}
{%- if roast -%}
<p class="roast-badge">
{{- 'products.product.roast_date' | t }}: {{ roast | date: '%Y-%m-%d' -}}
</p>
{%- endif -%}
Source: Theme Check checks reference
Added to locales/en.default.json ("products": { "product": { "roast_date": "Roast date" } }) and called from
sections/product.liquid with {% render 'roast-date-badge', product: product %}, the project passed again with 40 files
inspected and no offenses. A snippet that is written but never rendered keeps the OrphanedSnippet warning.
The Spanish storefront adds a trap. A locales/es.json that contains only the new key made the MatchingTranslations check
report 32 errors, one for every key of the default locale that the Spanish file lacked. A secondary locale must be complete
(or be generated from the default with all keys), not a partial overlay.
Severity, presets and the checks that exist
Severity is error, warning or info; in configuration files the integers 0, 1 and 2 are equivalent. The default
fail level is error, so a project with only warnings exits with 0. In this run, a file with two warnings gave exit code 0
by default and exit code 1 with --fail-level warning.
In 3.31.0 the theme-check:recommended preset enables 83 checks (45 at error severity and 38 at warning severity), and
theme-check:all enables 85 (47 errors and 38 warnings). The Skeleton copy had no offenses under either preset. The same
tool reported, on the reference themes at the same date, no offenses for Skeleton (39 files), 9 warnings for Dawn 16.0.0
(155 files: OrphanedSnippet 1, VariableName 2, UnusedAssign 2, LiquidComplexity 1 and UndefinedObject 3) and 6
warnings for Horizon 4.3.0 (357 files: ExcessiveSettingsCount 1 and UnusedDocParam 5). A clean Theme Check is therefore not a
property of an old, well-known theme; it has to be kept clean by running it.
The checks fall into groups, and the official reference documents each one with its options and rationale.
| Group | Examples (all enabled by the recommended preset) |
|---|---|
Syntax and validity |
|
References that must resolve |
|
Dead and deprecated code |
|
Performance and platform rules |
|
Sections, blocks and snippets |
|
Theme app extensions |
The |
Source: Theme Check checks reference
Configuration with .theme-check.yml
shopify theme check --init writes a starter file. The file is optional, and all keys in it are optional too.
| Key | Type | Meaning |
|---|---|---|
|
string |
Directory that holds |
|
string or list |
Presets ( |
|
string or list |
Paths or modules of custom checks (CommonJS import paths). |
|
list |
Globs excluded from every check (minimatch). |
A check name |
object |
|
extends: theme-check:recommended
ignore:
- node_modules/**
- assets/vendor/**
UnusedAssign:
enabled: true
severity: error
OrphanedSnippet:
enabled: false
Source: Theme Check configuration
With this file, a snippet containing two unused assigns was reported as 2 errors and the command exited with 1, and the
OrphanedSnippet warning for the same unreferenced file disappeared. Two behaviours are worth knowing. An entry for a
check name that does not exist is accepted silently: the example of the configuration page uses TemplateLength, which is
not among the 83 checks of the 3.31.0 CLI, and a file that configured it produced no message at all, so a typo in a check
name disables nothing and enforces nothing. And the severity shown for a check in the reference table is the default of the
check, not necessarily the one the preset applies: the reference lists DeprecatedTag as an error while the recommended
preset in 3.31.0 reports it as a warning. Use shopify theme check --list or --print to see the effective values.
Disabling checks in Liquid
A comment disables all checks or named checks, for the next line, for a region, or for the whole file when placed on the first line. The comments are read only after the file parses, so they cannot silence a syntax error.
{% # theme-check-disable-next-line UnusedAssign %}
{% assign legacy = 1 %}
{% # theme-check-disable UnusedAssign, UndefinedObject %}
{% assign x = 1 %}
{% echo y %}
{% # theme-check-enable UnusedAssign, UndefinedObject %}
In a scratch snippet with two unused assigns, the theme-check-disable-next-line UnusedAssign comment removed the
warning for the line it preceded and left the second one.
Editors and the language server
shopify theme language-server speaks the Language Server Protocol, and the Shopify Liquid VS Code extension is built on
it. The same diagnostics, completion for Liquid objects, filters and tags with their documentation, formatting, and code
navigation therefore appear in other editors that can start a language server. Running Theme Check in the editor and again
in CI is the intended combination.
Source: Shopify Liquid language server
The Shopify GitHub integration
The Shopify GitHub app connects the store to a GitHub account or organization and keeps a branch and a store theme in step in both directions. A push to the branch updates the theme in the admin; a save in the theme editor, the code editor or a theme app in the Online Store becomes a commit on the branch. The integration can connect several branches, each as a separate theme (unpublished or published), so a campaign can live on its own branch.
How it behaves
-
Commits written by Shopify cannot be switched off. If some code must stay out of Shopify’s reach, keep it in another repository or in a subtree.
-
A commit is created for owners, staff members and collaborators alike when changes are saved. Edits within about ten seconds are batched into one commit. The commit body names the shop and, since 2026-08-26, the person who saved last, so it is not a per-file record of who changed what.
-
There are no conflict alerts in the code editor: the version in the code editor overwrites the version on GitHub. The theme editor warns before overwriting a file that changed elsewhere. If a commit made outside Shopify conflicts, resolve it on GitHub or force-push to overwrite the file in Shopify. In rare cases a commit from Shopify that races with a push is rejected by GitHub as outdated.
-
View logs next to the Last saved time on the theme card shows the last version control events, and Actions > Reset to last commit re-pulls the branch if the theme fell behind.
-
Only branches that match the default theme folder structure can be connected. Other folders in the repository are ignored, so a repository may hold documentation or tooling beside the theme folders.
-
A disconnected branch cannot be reconnected to the same theme; connecting it again adds a new theme. A connected unpublished theme keeps its connection when it is published.
-
Only repositories you can write to can create a theme. GitHub outside collaborators cannot connect branches, only members of the organization with write access, and personal repositories you only collaborate on are not listed. These are GitHub permissions, unrelated to Shopify collaborator accounts. Granting the app access to only some repositories keeps the others invisible to everyone with the Themes permission.
Connecting a repository
-
Install the Shopify GitHub app on the GitHub account or organization.
-
In the admin go to Online Store, then Themes. In the theme library choose Add theme, then Connect from GitHub.
-
Select the account or organization, the repository and the branch. The theme appears in the library with the repository, branch and time of the last commit on its card.
-
Test the connection by changing a setting in the theme editor, saving, and following the branch name on the card to GitHub: the latest commit is authored by the
shopifybot and names the shop and last editor. -
To track the live storefront, publish the theme that is connected to the branch you want to follow, normally
main.
The account used must be the store owner, a staff account or a collaborator account with the Themes permission.
Branch strategy for Pocket Coffee
The version control guidance recommends connecting main to the store and publishing that theme, so the live storefront
always matches the merged code. Campaigns such as a Christmas gift box run on their own branch connected to an unpublished
theme, are published for the event and replaced by the main-branch theme afterwards. Because merchants edit through the
theme editor, treat main as shared with the merchant: pull before you start work, commit small, and do not rely on
rewriting history, since Shopify’s commits arrive at any time.
Source code and compiled code
The integration only accepts the default structure, so a theme whose source needs a build (bundled JavaScript, PostCSS, Tailwind output) cannot connect its source branch. The version control guide lists the options and recommends the third:
-
Separate repositories for source and compiled code: simple to adopt, but backfilling merchant edits across two repositories is harder.
-
A single compiled directory in a mixed tree: minimal compiled files, but compiled files are easy to confuse with source and merchants may edit them.
-
Separate branches, recommended: compile into a directory and publish it to a deploy branch that the store connects to. Production history stays clean, at the price of managing preview branches.
-
Source only, deploying the build with the CLI: compatible with any bundler, but incompatible with the integration, and compiled code is not versioned.
A deploy branch can be produced from a build directory with git subtree. This was run on a scratch repository: after
committing src and dist, git subtree split --prefix=dist -b deploy created a branch whose root held only the
contents of dist (here assets/main.min.js and sections/a.liquid).
npm run build # writes the theme files to dist/
git add dist && git commit -m "Build 2026-10-27"
git subtree split --prefix=dist -b deploy # a branch whose root is the content of dist
git push origin deploy
# later: git subtree push --prefix=dist origin deploy
Shopify minifies CSS and JavaScript files in assets on delivery, and offers just-in-time transformations, so a theme
that only needs minification can stay buildless and compatible with the integration. Merchant edits of compiled files must
be backfilled into the source by hand, because a compile overwrites them.
Source: File transformation for themes
The documentation does not describe how the CLI and the integration interact when both write to the same theme. A safe
convention, offered here as a recommendation and not as a documented rule, is to let one path own each theme: the
integration for themes connected to branches, and theme push only to themes that no branch is connected to (development
and preview themes, and release candidates that are published and then connected, or not).
CI with Theme Access
A pipeline needs a Node.js step, a step that installs the CLI, and a step that runs the command, with these inputs.
| Name | Required | Value |
|---|---|---|
|
Yes |
The Theme Access password of the store, stored as a masked secret. |
|
Yes |
The store to work on; not secret, so a repository variable is enough. |
|
No |
Set to |
The CLI version must be 3.20 or later. Prefer environment variables over --password on the command line, because command
lines can appear in logs and process lists. The official example installs the CLI with npm install -g @shopify/cli after
actions/setup-node@v4 with node-version: 20; the CLI itself now requires Node.js 22.12 or later, so the workflows below
use Node 22 instead. Pin the CLI major version (@shopify/cli@5) so that a breaking major release does not change your
pipeline overnight; in CI the CLI does not upgrade itself.
Pull request checks
The first workflow needs no Shopify credential at all, because Theme Check runs locally. It fails the pull request when the theme has any warning or error.
name: Theme check
on:
pull_request:
branches: [main]
jobs:
check:
name: Theme Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
- name: Install Shopify CLI
run: npm install -g @shopify/cli@5
- name: Run Theme Check
run: shopify theme check --fail-level warning
A preview theme per pull request
The second workflow uploads the branch to a development theme owned by the pull request, using --development-context so
that pushes of the same pull request update one theme and do not create a new one each time. Repository secrets are not
passed to workflows triggered by pull requests from forks, so the job is limited to branches of the same repository.
name: Theme preview
on:
pull_request:
branches: [main]
concurrency:
group: preview-${{ github.event.number }}
cancel-in-progress: true
jobs:
preview:
name: Push preview theme
# Pull requests from forks do not receive repository secrets.
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
env:
SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }}
SHOPIFY_FLAG_STORE: ${{ vars.SHOPIFY_FLAG_STORE }}
SHOPIFY_CLI_NO_ANALYTICS: "1"
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
- name: Install Shopify CLI
run: npm install -g @shopify/cli@5
- name: Check the theme
run: shopify theme check --fail-level error
- name: Push to the development theme of this pull request
id: push
run: |
shopify theme push \
--development \
--development-context "pr-${{ github.event.number }}" \
--nodelete \
--json > push.json
echo "preview=$(jq -r '.theme.previewUrl' push.json)" >> "$GITHUB_OUTPUT"
- name: Show the preview link
run: echo "Preview ${{ steps.push.outputs.preview }}"
Source: shopify theme push
Development themes of a store accumulate with every open pull request and are removed after seven days of inactivity, so no cleanup job is needed for short-lived branches. The preview shows the store’s real data in the development store, not the production catalogue.
Deploy on merge
The third workflow checks, uploads the merged commit as a new unpublished theme and publishes it. Uploading first and
publishing as a separate step lets the pipeline fail before the live theme changes, and leaves the previous live theme in
the library as an instant rollback target: publish it again with shopify theme publish. --strict makes push itself
refuse to upload a theme that has Theme Check errors. The concurrency group serializes deployments.
name: Theme deploy
on:
push:
branches: [main]
concurrency:
group: theme-deploy
cancel-in-progress: false
jobs:
deploy:
name: Check, upload and publish
runs-on: ubuntu-latest
env:
SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_CLI_THEME_TOKEN }}
SHOPIFY_FLAG_STORE: ${{ vars.SHOPIFY_FLAG_STORE }}
SHOPIFY_CLI_NO_ANALYTICS: "1"
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
- name: Install Shopify CLI
run: npm install -g @shopify/cli@5
- name: Check the theme
run: shopify theme check --fail-level error
- name: Upload as a new unpublished theme
id: upload
run: |
shopify theme push \
--unpublished \
--theme "release-${GITHUB_SHA::7}" \
--strict \
--json > push.json
echo "theme_id=$(jq -r '.theme.id' push.json)" >> "$GITHUB_OUTPUT"
- name: Publish the uploaded theme
run: shopify theme publish --theme "${{ steps.upload.outputs.theme_id }}" --force
The simplest variant, shopify theme push --live --allow-live --strict, replaces the live theme in place and offers no
rollback. If the store also has a GitHub-connected main branch, do not add this workflow for the same theme (see the
convention above).
Performance gate with Lighthouse CI
Shopify publishes shopify/lighthouse-ci-action, a GitHub Action based on Google’s Lighthouse CI. For every run it audits
the home page, a product page and a collection page of a development store that runs the code under test. It needs a
dedicated development store populated with Shopify’s test product CSV (and no other products or collections, for stable
scores) and the credentials of a Dev Dashboard app with read_products and write_themes installed on that store.
name: Shopify Lighthouse CI
on: [push]
jobs:
lhci:
name: Lighthouse
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Lighthouse
uses: shopify/lighthouse-ci-action@v1
with:
client_id: ${{ secrets.SHOP_CLIENT_ID }}
client_secret: ${{ secrets.SHOP_CLIENT_SECRET }}
store: ${{ secrets.SHOP_STORE }}
password: ${{ secrets.SHOP_PASSWORD }}
The action takes client_id and client_secret (or, for custom apps created before January 2026, access_token),
store, an optional storefront password, product_handle, collection_handle, theme_root, pull_theme (a theme whose
settings and JSON templates are pulled in), and the thresholds lhci_min_score_performance (default 0.6) and
lhci_min_score_accessibility (default 0.9). A GitHub status check needs a token, either lhci_github_app_token for the
Lighthouse CI GitHub app or lhci_github_token for a personal access token. The performance default of 0.6 matches the
minimum average Lighthouse performance score that the Theme Store requires.
Pitfalls and a checklist
| Situation | What to do |
|---|---|
|
Pass a target ( |
A deploy overwrote the merchant’s settings |
Add |
|
Put |
The preview disappeared |
Development themes are deleted after seven days of inactivity and on |
Theme Check passes locally but fails in CI |
Different CLI version, a missing first-run download of the Liquid reference data, or an extra |
The password is visible in logs |
Pass it only as |
Fork pull requests cannot deploy a preview |
Repository secrets are withheld from forks; run only Theme Check for forks and push previews for same-repository branches. |
A Spanish locale file broke the build |
Secondary locale files must contain every key of the default locale, otherwise |
|
The command queries the GitHub API. Retry later, or |
The GitHub integration reverted a code editor change |
There are no conflict alerts in the code editor and its version overwrites GitHub’s. Edit through Git, and use Reset to last commit if the theme is out of date. |