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 shopify theme commands)

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 init.

Theme Check

A linter for the Liquid and JSON files of a theme and of theme app extensions. It is built into the CLI (shopify theme check), into the Liquid language server and into the Shopify Liquid VS Code extension.

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 write_themes scope on one store without a staff account.

On the store. The password is passed to the CLI as SHOPIFY_CLI_THEME_TOKEN or --password.

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.

flowchart TD subgraph local["Developer machine"] init["shopify theme init"] --> dev["shopify theme dev"] dev --> check["shopify theme check"] check --> commit["git commit and push"] end commit --> pr["Pull request"] subgraph ci["GitHub Actions"] pr --> ccheck["theme check with a fail level"] ccheck --> preview["theme push to a development theme"] end preview --> review["Review with the preview link"] review --> merge["Merge to main"] merge --> release["theme push to a new unpublished theme"] release --> publish["theme publish"] publish --> live(["Live theme"]) live --> editor["Merchant edits in the theme editor"] editor -->|"commit by Shopify when the branch is connected"| main["main branch"] main -->|"git pull"| dev

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 off turns it off, shopify config autoupgrade on turns it on and shopify config autoupgrade status shows the state. shopify upgrade upgrades by hand.

  • CLI 4.0.0 removed shopify theme serve; shopify theme dev replaces it.

  • Usage data is collected by default (commands, flags, timings, errors and identifiers such as the store domain). Set SHOPIFY_CLI_NO_ANALYTICS=1 or OPT_OUT_INSTRUMENTATION=true to opt out. The examples on this page were run with the first variable set.

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

-s, --store

SHOPIFY_FLAG_STORE

Store prefix (pocket-coffee-dev) or full myshopify.com domain. The store is remembered for later commands until another is given.

--password

SHOPIFY_CLI_THEME_TOKEN

A Theme Access password or a custom app Admin API token. Without it the CLI uses your Shopify account login.

-e, --environment

SHOPIFY_FLAG_ENVIRONMENT

Name of an environment defined in shopify.theme.toml.

--path

SHOPIFY_FLAG_PATH

Directory to run in; defaults to the current directory.

--no-input

SHOPIFY_FLAG_NO_INPUT

Disables prompts and browser authentication, so a missing value is an error instead of a question.

-j, --json

SHOPIFY_FLAG_JSON

Prints the result as JSON and disables colour. --json-schema prints the JSON schemas of the result, error and events.

--no-color, --verbose

SHOPIFY_FLAG_NO_COLOR, SHOPIFY_FLAG_VERBOSE

Plain output; more output, which may include sensitive data.

--auth-alias

SHOPIFY_FLAG_AUTH_ALIAS

Account alias to authenticate with.

Authentication and store targeting

The CLI accepts three kinds of credentials for a store.

  1. 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 logout logs out and, as a side effect, deletes your development theme.

  2. A Theme Access password, passed with --password or the SHOPIFY_CLI_THEME_TOKEN environment variable. This is the right choice for CI and for handing a freelancer access to one store.

  3. A custom app Admin API token passed the same way. The app needs the read_themes and write_themes scopes. For hot reload it also needs unauthenticated_read_content for the Storefront API, and the tokens must then be supplied as environment variables instead of the --password flag.

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.

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

-u, --clone-url

Git URL to clone. Default https://github.com/Shopify/skeleton-theme.git. Use https://github.com/Shopify/dawn.git or https://github.com/Shopify/horizon.git to start from those themes.

-l, --latest

Download the latest release of the clone URL instead of its default branch.

-j, --json, --path, --no-input, --auth-alias

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"

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.

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"
Flag (environment variable) Meaning

-s, --store, --password, --store-password, -e, --environment

Which store, which credential, the storefront password if the shop is password protected, and which TOML environment.

-t, --theme (SHOPIFY_FLAG_THEME_ID)

Develop against an existing remote theme, by ID or name, instead of the development theme.

-a, --allow-live (SHOPIFY_FLAG_ALLOW_LIVE)

Allows development directly on the live theme. Use it deliberately: each save becomes visible to shoppers.

--live-reload (SHOPIFY_FLAG_LIVE_RELOAD)

hot-reload (default) refreshes CSS and sections in place, full-page reloads the whole page on every change, off disables reloading.

--error-overlay (SHOPIFY_FLAG_ERROR_OVERLAY)

default shows an overlay when an asset upload fails, silent hides it.

-o, --only, -x, --ignore

Hot reload only, or skip, files matching a pattern. Repeatable.

-n, --nodelete (SHOPIFY_FLAG_NODELETE)

Do not delete remote files when a local file is deleted, including files already missing locally when the command starts.

--theme-editor-sync (SHOPIFY_FLAG_THEME_EDITOR_SYNC)

Writes changes made in the theme editor back into the local theme files.

--reconciliation-strategy

With --theme-editor-sync, how JSON conflicts are resolved: keep-local, keep-remote or abort (fail instead of prompting).

--host, --port (SHOPIFY_FLAG_HOST, SHOPIFY_FLAG_PORT)

Network interface (default 127.0.0.1) and local port (1 to 65535) of the preview server.

--open, --notify

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.

--listing (SHOPIFY_FLAG_LISTING)

For multi-preset Theme Store themes, applies the files from listings/<preset-name>.

--standard-events-inspector

Injects the standard events inspector into storefront HTML so that analytics events can be inspected.

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

-d, --development (SHOPIFY_FLAG_DEVELOPMENT)

Push to your development theme.

-c, --development-context (SHOPIFY_FLAG_DEVELOPMENT_CONTEXT)

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.

-l, --live (SHOPIFY_FLAG_LIVE) and -a, --allow-live

Push to the live theme; --allow-live is required when no prompt is possible.

-t, --theme (SHOPIFY_FLAG_THEME_ID)

ID or name of an existing remote theme. With --unpublished and without --development, it is the name of the theme to create.

-u, --unpublished (SHOPIFY_FLAG_UNPUBLISHED)

Create a new unpublished theme and push to it.

-p, --publish (SHOPIFY_FLAG_PUBLISH)

Publish the theme as live after the upload.

-n, --nodelete

Keep remote files that do not exist locally.

-o, --only, -x, --ignore

Upload only, or skip, matching files; quote wildcards. Repeatable. .shopifyignore is also respected.

--strict (SHOPIFY_FLAG_STRICT_PUSH)

Run Theme Check first and refuse to push if it reports errors. Warnings are allowed.

-j, --json

Machine-readable result, described below.

--listing

Apply files from listings/<preset-name> for a multi-preset theme.

# 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

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"
  }
}

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

-d, --development, -l, --live, -t, --theme

Which remote theme to read. Non-interactive runs need one of the three.

-n, --nodelete (SHOPIFY_FLAG_NODELETE)

Keep local files that do not exist remotely.

-o, --only, -x, --ignore

Download only, or skip, matching files; quote wildcards. .shopifyignore is also respected.

-j, --json

Result conforming to the ThemePullResult schema.

shopify theme pull --live --only "templates/*.json" --only "config/settings_data.json"
git diff --stat

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

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

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

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

shopify theme list

Lists themes with ID and role. --role live|unpublished|development, --name (substring) and --id filter the list; --json returns ThemeListResult.

shopify theme info

Shows the store, the theme and CLI environment in use, for a development theme (-d) or a given theme (-t).

shopify theme open

Prints and optionally opens the preview links of a remote theme: -d, -l, -t select the theme and -E, --editor opens the theme editor.

shopify theme console

A Liquid REPL that evaluates expressions against real store data. --url sets the page context (default /) because some objects exist only on certain pages.

shopify theme profile

Profiles the Liquid rendering time of one page (--url) and opens the result in the Speedscope profiler.

shopify theme preview

Applies a JSON overrides file (--overrides) to a theme and returns a preview URL; --preview-id updates an existing preview and --open launches it.

shopify theme duplicate

Copies a remote theme (-t) under a new name (-n). --force skips confirmation. Push first if the source is the local theme.

shopify theme rename

Renames a theme (-n, --name) chosen with -d, -l or -t.

shopify theme delete

Deletes remote themes: -t (repeatable), -d (development theme), -a, --show-all (include other development themes) and -f, --force. This cannot be undone.

shopify theme metafields pull

Downloads the metafield definitions of the shop into a local file.

shopify theme language-server

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.

shopify theme check

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
Flag (environment variable) Meaning

-C, --config (SHOPIFY_FLAG_CONFIG)

A configuration file path, overriding .theme-check.yml, or a preset: theme-check:recommended, theme-check:all, theme-check:theme-app-extension. Combine with --path to set the theme directory.

-a, --auto-correct (SHOPIFY_FLAG_AUTO_CORRECT)

Fixes offenses that have an automatic correction (for example DeprecatedFilter).

-o, --output (SHOPIFY_FLAG_OUTPUT)

text (default) or json. -j, --json is a shortcut with colour disabled.

--fail-level (SHOPIFY_FLAG_FAIL_LEVEL)

Minimum severity that makes the exit code non-zero: crash, error (default), suggestion, style, warning or info.

--init, --list, --print, -v, --version

Mutually exclusive helper modes described in the listing above.

-e, --environment

One or several TOML environments (each needs path when several are used).

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 -%}

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

ImgWidthAndHeight

error

Missing width and height attributes on img tag.

MissingTemplate

error

snippets/icon-coffee.liquid does not exist.

MissingAsset

error

assets/badge.png does not exist.

TranslationKeyExists

error

products.product.roast_date has no matching entry in locales/en.default.json.

DeprecatedTag

warning

Use the render tag instead of include.

OrphanedSnippet

warning

The snippet is not referenced by any other file.

UnusedAssign

warning

The variable unused_note is assigned but not used.

-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 -%}

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

LiquidSyntaxError, LiquidHTMLSyntaxError, UnclosedHTMLElement, JSONSyntaxError, ValidJSON, ValidSchema, ValidSchemaName, ValidSettingsKey

References that must resolve

MissingTemplate (render or include of a snippet that does not exist), MissingAsset, UndefinedObject, UnknownFilter, UnsupportedFilterArguments, TranslationKeyExists, MatchingTranslations

Dead and deprecated code

UnusedAssign, OrphanedSnippet, DeprecatedTag (such as include), DeprecatedFilter, DeprecateLazysizes, DeprecateBgsizes, the DeprecatedFontsOnSectionsAndBlocks, DeprecatedFontsOnSettingsData and DeprecatedFontsOnSettingsSchema checks

Performance and platform rules

ImgWidthAndHeight, ParserBlockingScript, RemoteAsset, AssetPreload, CdnPreconnect, MaxFileSize, LiquidComplexity, LiquidNestingDepth, ContentForHeaderModification, HardcodedRoutes, PaginationSize

Sections, blocks and snippets

SchemaPresetsBlockOrder, SchemaPresetsStaticBlocks, ValidBlockTarget, ValidLocalBlocks, UniqueSettingId, UniqueStaticBlockId, ValidVisibleIf, MissingRenderSnippetArguments, UnrecognizedRenderSnippetArguments, UnusedDocParam, UniqueDocParamNames, ValidDocParamTypes

Theme app extensions

The theme-check:theme-app-extension preset, for example AppBlockValidTags, AssetSizeAppBlockCSS and AssetSizeAppBlockJavascript

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

root

string

Directory that holds templates, sections and snippets when the theme is generated, for example dist.

extends

string or list

Presets (theme-check:recommended, theme-check:all, theme-check:theme-app-extension) or other config files. When several are listed, objects are deep merged, arrays concatenated and later entries win.

require

string or list

Paths or modules of custom checks (CommonJS import paths).

ignore

list

Globs excluded from every check (minimatch).

A check name

object

enabled, severity, a per-check ignore list and the options of that check.

extends: theme-check:recommended
ignore:
  - node_modules/**
  - assets/vendor/**
UnusedAssign:
  enabled: true
  severity: error
OrphanedSnippet:
  enabled: false

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.

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

  1. Install the Shopify GitHub app on the GitHub account or organization.

  2. In the admin go to Online Store, then Themes. In the theme library choose Add theme, then Connect from GitHub.

  3. 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.

  4. 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 shopify bot and names the shop and last editor.

  5. 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.

sequenceDiagram participant Dev as Developer participant GH as GitHub branch main participant Shop as Connected theme participant Mer as Merchant Dev->>GH: merge a pull request GH->>Shop: theme files updated Mer->>Shop: save in the theme editor Shop->>GH: commit by Shopify after about 10 seconds Dev->>GH: git pull

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:

  1. Separate repositories for source and compiled code: simple to adopt, but backfilling merchant edits across two repositories is harder.

  2. 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.

  3. 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.

  4. 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.

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

SHOPIFY_CLI_THEME_TOKEN

Yes

The Theme Access password of the store, stored as a masked secret.

SHOPIFY_FLAG_STORE

Yes

The store to work on; not secret, so a repository variable is enough.

SHOPIFY_FLAG_FORCE

No

Set to 1 to turn off interactive prompts, for example if the step times out waiting for input.

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 }}"

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

theme push in CI hangs or asks a question

Pass a target (--development, --live, --theme or --unpublished), --allow-live for live, --force for publish, --no-input, or set SHOPIFY_FLAG_FORCE=1.

A deploy overwrote the merchant’s settings

Add config/settings_data.json and the JSON templates to .shopifyignore or --ignore, or pull them before deploying.

theme check -e a -e b skips environments

Put path (and all other required flags) in each environment: with several environments the CLI never prompts.

The preview disappeared

Development themes are deleted after seven days of inactivity and on shopify auth logout. Use theme share or an unpublished theme for previews that must last.

Theme Check passes locally but fails in CI

Different CLI version, a missing first-run download of the Liquid reference data, or an extra .theme-check.yml. Pin the CLI major version and print the version with shopify theme check -v.

The password is visible in logs

Pass it only as SHOPIFY_CLI_THEME_TOKEN from a secret, never inline, and remember that --verbose output may include sensitive data.

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 MatchingTranslations reports one error per missing key.

theme init returns an API rate limit error

The command queries the GitHub API. Retry later, or git clone the template and delete its .git directory.

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.