Skip to main content

Code Editor content policy

What the Code Editor checks when you save, what each error means, and the rules for URLs, JavaScript, CSS and Liquid.

📖 When you save a file in the Code Editor, Boost checks the code against a content policy before the save goes through. This article explains what the policy allows, what each error means, and how to write customizations that pass.

⚠️ The Code Editor is available on Boost AI Search & Discovery TURBO with template version 1.6.1 and above. To open it, follow Manage your template: Code Editor.


The rule in one sentence

Code saved in the Code Editor may only make the shopper's browser open, load, or send data to a URL on your own store or on a reviewed list of allowed hosts.

Two rules follow from that:

  • If the checker cannot tell where a URL goes, the URL is not allowed. For example, fetch(url) where url is a variable.

  • If the checker does not recognize something, it is not allowed. For example, an unknown Liquid tag, an unknown Liquid variable, or a browser API that is not on the reviewed list.

The checker only reads your code. It never runs it and never calls the network.


When the check runs

The check runs when you save a file in the Code Editor.

File

What happens

customization.js

Checked with the JavaScript rules. A violation rejects the save, and the editor lists the issues.

customization.css

Checked with the CSS rules. The save still goes through, and the issues are recorded.

Any widget .liquid file

Checked with the Liquid and HTML rules, plus the JavaScript and CSS rules for code inside the template. A violation rejects the save.

Each save is checked in two steps.

  1. Syntax first. JavaScript, CSS and Liquid are checked for syntax errors. If one of these fails, you see that error and the content policy does not run yet.

  2. Then the content policy. It runs only after step 1 passes.

How a Liquid file is checked

The checker does not look at the file you edit on its own. It builds every widget that Boost publishes and that includes your edit, the same way publishing does, and checks each result. Because of this:

  • {% settings %} and {% render './x.liquid' %} work in the editor, because publishing resolves them before the check.

  • Variables that a parent widget sets, such as loop variables, are known to the checker.

  • Each issue names the published widget it was found in, for example Collection header. Liquid issues have no line number, because the line would point into the built widget, which the editor does not show.

  • One problem can appear once for each published widget that contains it.

  • A violation in another customized widget inside the same published widget also rejects your save.


Reading an error in the Code Editor

Each issue has a message and a rule ID. JavaScript and CSS issues also have a line and column. Liquid issues name the widget instead.

The start of the message tells you where the URL is, for example fetch, img[src], CSS url(), href write, or CSS @import.

Example message

What it tells you

fetch sends a request to evil.example, which is not on the allowed hosts list.

The rule ID is code-policy/host-not-allowed, and the issue is on a specific line and column.

a[href] links to instagram.com. Links can only point to pages on your store.

The rule ID is code-policy/cross-origin-navigation, and the issue is in the Product item widget.


The seven rule IDs

Rule ID

What it means, and how to fix it

code-policy/host-not-allowed

The URL goes to a host that is not your store and not on the allowed hosts list for that kind of use. Typical causes: fetch('https://api.example.com'), <img src="https://other-cdn.com/a.png">, <script src="...">, a Google Fonts @import. Fix: use a path on your store, or upload the file to Shopify Files (cdn.shopify.com). If you genuinely need another host, contact our support team.

code-policy/cross-origin-navigation

A link, form, or redirect sends the shopper to another site. Typical causes: <a href="https://instagram.com/brand">, location.href = 'https://...'. Fix: links in code may only point to pages on your store.

code-policy/forbidden-scheme

The URL uses a scheme other than https: (or wss: and data: where those are allowed). Typical causes: http://, mailto:, tel:, javascript:, data:text/html. Fix: use https:// or a path on your store.

code-policy/unresolvable

The checker cannot work out where the URL goes. Typical causes: fetch(url), el.innerHTML = '<div>' + name + '</div>', href="{{ product.title }}", .click(). Fix: start the URL with fixed text, such as '/products/' + handle.

code-policy/forbidden-construct

The code uses something the checker does not allow or does not recognize. Typical causes: eval, $ or jQuery, an unknown Liquid variable or tag, <iframe> content, <base>. Fix: rewrite it with an allowed construct.

code-policy/parse-error

The checker could not read the code. Typical causes: a syntax error, or import x from '...' in customization.js. Fix: fix the syntax. customization.js is a classic script, so static import is not allowed.

code-policy/budget-exceeded

The code is too large or too complex to check. Typical causes: very large files, or very deep nesting. Fix: split or simplify the code.


URL rules, for every file type

The four kinds of URL use

The checker sorts every URL by what the browser does with it.

Kind

What the browser does, and where it appears

Navigation

Takes the shopper to the URL. <a href>, <form action>, location.href = ..., location.assign(), window.open().

Network

Sends a request, which can carry data. fetch(), navigator.sendBeacon(), WebSocket, EventSource, the ping attribute.

Resource

Loads a passive file that cannot run code. <img src>, srcset, <video>, <audio>, poster, data-src, CSS url().

Code-exec

Loads something that can run code or load more files. <script src>, <link href>, <iframe src>, <object>, <embed>, SVG <use>, CSS @import, and JavaScript el.src = ... or el.href = ....

JavaScript el.src = ... and el.href = ... count as code-exec, because the checker cannot know whether el is an <img> or a <script>.

Which hosts pass

Kind

Passes when the host is

Navigation

Your store.

Network

Your store, or a host on the allowed hosts list below.

Resource

Your store, or a host on the allowed hosts list below.

Code-exec

Your store only. The reviewed code allowlist is empty today.

⚠️ "Your store" means your .myshopify.com domain. The Code Editor does not pass your custom domain to the checker, so https://www.brand.com/cart.js is blocked. Use a relative path such as /cart.js, which always points to your store.

Allowed hosts list, for network and resource URLs only. A subdomain of a listed host also passes.

Host

Why it is allowed

cdn.shopify.com

Shopify product images and theme assets

fonts.shopifycdn.com

Shopify fonts

boost-cdn-prod.bc-solutions.net

Boost theme CDN, and icon URLs in default settings

flagcdn.com

Country flags in the back-in-stock widget

Example

Result

fetch('/cart.js')

Passes

fetch('https://shop.myshopify.com/cart.js')

Passes

fetch('https://cdn.shopify.com/s/files/x.json')

Passes

fetch('https://www.brand.com/cart.js') (custom domain)

Blocked: host-not-allowed

fetch('https://evil.example/collect')

Blocked: host-not-allowed

<img src="https://flagcdn.com/ca.svg">

Passes

<script src="https://cdn.shopify.com/s/files/x.js">

Blocked: host-not-allowed, because code-exec needs the code allowlist

<iframe src="https://www.youtube.com/embed/x">

Blocked: host-not-allowed

Allowed URL schemes

Kind

Allowed schemes

Navigation

https:

Network

https:, wss:

Resource

https:, and data: only for images (image/*) and fonts (font/*)

Code-exec

https:

Relative URLs such as /cart, ?page=2 and #top resolve to your store over https:, so they pass.

Example

Result

<a href="#top">

Passes

<a href="mailto:[email protected]">

Blocked: forbidden-scheme

<a href="tel:+100">

Blocked: forbidden-scheme

<a href="javascript:void(0)">

Blocked: forbidden-scheme. Use a <button> instead.

fetch('http://cdn.shopify.com/...')

Blocked: forbidden-scheme

url('data:image/png;base64,...')

Passes

url('data:text/html,...')

Blocked: forbidden-scheme

Building URLs from variables

The checker cannot know the value of a variable. It accepts a URL built from a variable only when the fixed text at the start already decides which site the URL goes to.

A fixed start decides the site when it is one of these:

  • A path with at least one character after the first /, such as /products/ or /search?q=.

  • A query or fragment, such as ?page= or #.

  • A full https://host/ with a /, ? or # after the host.

A bare / is not enough, because the variable could start with / and turn //evil.example into another site.

Example

Result

fetch('/search?q=' + encodeURIComponent(q))

Passes

fetch(`/products/${handle}.js`)

Passes

location.href = '/products/' + handle

Passes

fetch(location.origin + '/cart.js')

Passes. location.origin is read as your store.

fetch(Shopify.routes.root + 'cart.js')

Passes. Shopify.routes.root is read as /.

fetch('/' + path)

Blocked: unresolvable

var url = '/cart.js'; fetch(url)

Blocked: unresolvable

const API = '/cart.js'; fetch(API)

Blocked: unresolvable. The checker does not follow const values. Write the text inside the call.

<a href="/products/{{ product.handle }}">

Passes

<a href="{{ product.title }}">

Blocked: unresolvable

What the checker sees through

The checker decodes text the way a browser does before it judges a URL. These are blocked even though the raw text looks harmless:

  • HTML entities, for example <img src="//evil.example/x.png">.

  • CSS escapes in function or at-rule names.

  • Every URL in a list, for example the second URL in srcset="/a.png 1x, https://evil.example/b.png 2x".

  • Markup printed from a Liquid string.

  • Absolute URLs in any HTML attribute, for example <div data-video="https://evil.example/v.mp4">.

  • Tab and newline characters inside a URL, and user@host tricks.


JavaScript rules

These apply to customization.js and to JavaScript inside a Liquid <script>, {% javascript %}, or on* attribute. customization.js is checked as a classic script with modern syntax.

Only known global names

A name your script does not declare itself must be on the list of allowed globals. Any other global is blocked, including eval, Function, XMLHttpRequest, Reflect, Proxy, DOMParser, Worker, $ and jQuery.

Type

Allowed globals

Plain values and helpers

Array, ArrayBuffer, BigInt, Boolean, CustomEvent, DataView, Date, Error and the other error types, Event, FormData, Infinity, Intl, JSON, Map, Math, NaN, Number, Object, Promise, RegExp, Set, String, Symbol, URL, URLSearchParams, WeakMap, WeakSet, alert, confirm, prompt, console, encodeURI, encodeURIComponent, decodeURI, decodeURIComponent, isFinite, isNaN, parseFloat, parseInt, structuredClone, clearTimeout, clearInterval, cancelAnimationFrame, AbortController, AbortSignal

URL calls, where the URL is checked

fetch, EventSource, WebSocket, open

Timers, where the callback must be a function

setTimeout, setInterval, requestAnimationFrame, queueMicrotask

Objects, whose members are checked below

document, location, navigator, Shopify, localStorage, sessionStorage, screen, performance, crypto, CSS, IntersectionObserver, MutationObserver, ResizeObserver

Global object

window, self, globalThis, top, parent, frames

Example

Result

eval('1')

Blocked: unknown global eval

$('.a').hide()

Blocked: unknown global $

new XMLHttpRequest()

Blocked: unknown global XMLHttpRequest. Use fetch.

new MutationObserver(cb).observe(document.body, { childList: true })

Passes

Objects and URL functions only at the start of a chain

document, location, window, fetch and the other non-value globals may only be used at the start of a.b.c or as a direct call. Storing them or passing them around is blocked.

Example

Result

document.querySelector('.x')

Passes

var d = document;

Blocked: global document used as a value

var a = window.boostWidgetIntegration;

Blocked: member of a global object used as a value

window.boostWidgetIntegration.onBeforeAppBootstrap = function () {}

Passes

window.boostCustom = {};

Passes

Browser API members must be on the reviewed list

The checker knows every property and method name that Chrome defines, about 5,000 names. When your code uses one of those names, the name must be on the reviewed list with the right kind of use: read, write or call. Names that Chrome does not define, such as getCarouselConfig or slidesToShow, pass.

⚠️ This applies to every object, including your own objects and classes, because the checker cannot tell which object is which. If you name your own property state, target, x, go, filter or active, it is treated as the browser API with that name. Pick a distinct name instead, such as slideIndex or drawerState.

Example

Result

el.classList.add('on'), el.textContent = 'Hi', el.addEventListener(...)

Passes

document.body.attachShadow({ mode: 'open' })

Blocked: Web API member attachShadow is not allowed

var cfg = { state: 'a' }; cfg.state

Blocked: Web API member state is not allowed

var cfg = { drawerState: 'a' }; cfg.drawerState

Passes

this.x = 1 inside a class

Blocked: Web API member x is not allowed. Rename it, for example this.slideIndex.

Writes that hold a URL, markup or CSS are checked

Write

How it is checked

el.src, el.href, el.action, el.formAction, el.poster, el.srcset, el.ping, location, location.href

The value is checked as a URL. src and href count as code-exec.

el.innerHTML, el.outerHTML, insertAdjacentHTML(), document.write()

A fixed string is checked as HTML. A string built from a variable is blocked as unresolvable.

el.style.cssText, el.style.backgroundImage, el.style.setProperty()

The value is checked as CSS.

el.setAttribute(name, value)

name must be fixed text, and the value of a URL attribute is checked as a URL.

el.protocol, el.host, el.hostname, el.port, el.username, el.password

Writing is always blocked.

Example

Result

el.innerHTML = '<span class="badge">New</span>'

Passes

el.innerHTML = '<img src="https://evil.example/x.png">'

Blocked: host-not-allowed

el.innerHTML = '<span>' + name + '</span>'

Blocked: unresolvable

div.textContent = name

Passes. Use createElement plus textContent for dynamic content.

img.src = '/cdn/shop/files/' + file

Passes

img.src = 'https://cdn.shopify.com/s/files/a.png'

Blocked: host-not-allowed, because a src write is code-exec. Use the store path /cdn/shop/files/....

el.setAttribute('class', 'x')

Passes

el.setAttribute(n, 'x')

Blocked: setAttribute name must be a string literal

a.hostname = 'evil.example'

Blocked: write to origin property hostname

Clicking and submitting from code is blocked

click(), submit(), requestSubmit() and dispatchEvent() can follow a link or send a form that your theme put on the page. The checker cannot see those URLs, so these calls are always blocked as unresolvable navigation.

To move the shopper to another page, write location.href = '/path' with a store path.

Other JavaScript rules

Rule

What passes and what does not

call, apply, bind

Only on your own functions. function f() {} f.call(null) passes. Array.prototype.slice.call(arguments) is blocked, use Array.from(arguments).

createElement

The tag must be fixed text and one of a, button, div, img, input, label, li, p, span, ul. document.createElement('script') and document.createElement('section') are blocked.

Computed property names

Reading obj[key] is allowed. Writing obj[key] = value or calling obj[key]() with a key that is not fixed text is blocked. A value read with a computed key may be used directly, but is blocked when it is passed to a function, returned, or stored. Use items.forEach(function (v) { list.push(v); }) instead of list.push(items[i]).

Timers

The callback must be a function. setTimeout(function () { ... }, 10) passes, setTimeout('alert(1)', 10) is blocked.

Modules

Dynamic import('...') is blocked. Static import x from '...' is a parse-error, because customization.js is not a module.

Boost widget integration customizations

The standard pattern for extending the Boost widget integration passes the policy. For example, overriding RecommendationAPI through window.boostWidgetIntegration.onBeforeAppBootstrap and window.boostWidgetIntegration.extend('RecommendationAPI', ...) is allowed.

The code inside your overridden methods is still checked with every rule above. A fetch to a host that is not allowed is still blocked, and your own method and property names still follow the browser API rule, so avoid names such as state or x.


CSS rules

These apply to customization.css, <style>, style="" and {% css %}. CSS inside a Liquid file follows that file's rules, so a violation there rejects the save.

Example

Result

background: url('https://cdn.shopify.com/s/files/a.png')

Passes

background: url('/cdn/shop/files/a.png')

Passes

background: url('https://evil.example/a.png')

Blocked: host-not-allowed

@font-face src: url('https://evil.example/f.woff2')

Blocked: host-not-allowed

@import url('https://fonts.googleapis.com/css2?family=Inter')

Blocked: host-not-allowed, because @import loads code. Upload the font to Shopify Files and use @font-face.

--x: "https://evil.example/a.png"

Blocked: host-not-allowed. Strings in custom properties count as URLs.

--f: "Helvetica"

Passes

background: image-set(var(--x) 1x)

Blocked: unresolvable

width: expression(...)

Blocked: forbidden-construct

.a { color: red

Blocked: parse-error


Liquid and HTML rules

These apply to widget .liquid files.

Allowed Liquid tags

if, elsif, else, unless, case, when, for, break, continue, assign, capture, comment, raw, liquid, echo, render, css, javascript, style, stylesheet.

Any other tag, such as include or section, is blocked as an unknown Liquid tag.

Known Liquid variables

Every {{ variable }} must be a variable the checker knows. A variable is known when your template sets it with assign, capture or for, or when it is on the reviewed list. Anything else is blocked as an unknown Liquid value.

The list also sets how far each variable is trusted:

Trust level

Variables, and where they may go

Store host

request.shop, request.host. Allowed anywhere, including the host part of a URL.

Store path

routes.*, product.url, collection.url, page.url. Allowed anywhere, including the start of a URL.

Path segment

product.handle, collection.handle, any *.handle, locale, currentLocale, request.current_locale. Allowed inside a URL after a fixed start.

Platform image or file

*.src, *.originalSrc, *.image, source.url, product.media, product.images_info. Allowed in image and media URLs.

Merchant-entered settings

banner.link, cta.link, redirects.*, banner.imgSrc, widgetDesignSettings.bundleImageLink. Checked where you enter them.

Display data

product.*, collection.*, page.*, cart.*, customer.*, shop.*, settings.*, themeSettings.*, widgetDesignSettings.*, translations.*, forloop.*, banner.*, item.*, title, label, and about 70 more names used by the stock templates. Allowed in text, quoted attribute values, string literals in <script>, and CSS values. Inside a URL they may only come after a fixed start.

Example

Result

<span>{{ myCustomVar }}</span>

Blocked: unknown Liquid value myCustomVar

{% assign label = 'Sale' %}<span>{{ label }}</span>

Passes

{% capture u %}/products/{{ product.handle }}{% endcapture %}<a href="{{ u }}">

Passes

{% for item in product.images_info %}<img src="{{ item.src }}">{% endfor %}

Passes

<a href="{{ product.url }}">

Passes

<a href="{{ routes.cart_url }}">

Passes

Filters

Filters change how far a value is trusted. Any filter not listed here is blocked.

Filter group

Filters, and their effect

Keep trust

escape, escape_once, url_encode, downcase, upcase, capitalize, strip, lstrip, rstrip. The value keeps its trust level.

Join

append, prepend. The parts are joined, and each part keeps its own trust level.

Rewrite

replace, replace_first, remove, remove_first, truncate, truncatewords, slice, split. The result becomes display data, and any text the filter inserts is also checked.

Display

times, plus, minus, divided_by, ceil, floor, round, modulo, at_least, at_most, size, first, last, map, join, uniq, concat, sort, to_i, title, strip_html, translate, money, money_with_currency, format_currency, currency_format, numberShorten, product_metafield, simpleMd5, compare, Array, lowercase, pagination_list, string. The result becomes display data.

JSON

json, json_encode, json_parse. The result is JSON data, which is safe inside <script>.

Default

default. Both the value and the fallback are checked.

Platform file

asset_url, file_url, image_url, img_url. A relative file name becomes a platform file URL. A full URL stays as written and is checked as a URL.

Where {{ ... }} may appear

Position

Allowed?

Text between tags, <span>{{ x }}</span>

Yes

Inside a quoted attribute value, class="{{ x }}"

Yes. In a URL attribute, the URL rules apply.

Inside a string in <script>, var t = '{{ x }}';

Yes

As JSON in <script>

Yes

Directly in <script> code, var t = {{ x }};

No

Inside a start tag, <div {{ attrs }}>

No

Unquoted attribute value, class={{ x }}

No. Add quotes.

Inside an HTML comment

No

Where if, unless, case and for may appear

They may appear in text, between attributes, and inside a quoted attribute value. Every branch must end in the same HTML position where it started.

Example

Result

<div {% if product.available %}class="on"{% endif %}>

Passes

<div class="card {% if product.available %}is-on{% endif %}">

Passes

<div {% if product.available %}class="on{% endif %}">

Blocked: branches end in different HTML contexts

<a href="/collections/{% if x %}a{% else %}b{% endif %}">

Passes, because the start /collections/ already fixes the site

<a href="{% if x %}/a{% else %}/b{% endif %}">

Blocked. Put the whole <a> tag in each branch instead.

HTML attributes that hold URLs

Kind

Attributes

Navigation

a[href], area[href], form[action], button[formaction], input[formaction]

Network

ping on any element

Resource

src and srcset on img, source, video, audio, track; poster; background; data-src and data-srcset on any element; SVG image[href]

Code-exec

script[src], link[href], link[imagesrcset], iframe[src], frame[src], object[data], embed[src], input[src], SVG use[href], and src on any other element

Catch-all

Any other attribute whose value starts with a scheme or // is treated as a resource URL

Blocked elements

Element

Why

<base>

Changes where every relative URL on the page goes

<meta http-equiv>

Can redirect the page

<noscript>, <noembed>, <noframes>, <plaintext>, <xmp>, <math>

The browser reads their content in a way the checker does not model

<iframe> with content between the tags

Same reason

Inside <svg>: <use>, <script>, <style>, <title>, <desc>, <textarea>, <foreignObject>, <animate>, <set>, <animateMotion>, <animateTransform>

Same reason, or they can load or change URLs. Plain <svg><path> icons pass.

<!DOCTYPE, <?, <![CDATA[, and a null character

Not modeled

Code inside a template

Where

Rules used

<script> with no type, a JavaScript type, or type="module"

JavaScript rules

<script type="application/json"> or application/ld+json

Skipped

<script type="text/html"> or text/template

Liquid and HTML rules

<script> with any other type, such as text/x-template

Blocked

{% javascript %}

JavaScript rules

<style>, {% css %}, {% style %}, {% stylesheet %}

CSS rules

style="..."

CSS rules for declarations

onclick="..." and other on* attributes

JavaScript rules

<iframe srcdoc="...">

Liquid and HTML rules


Feel free to reach out to our dedicated support team via chat if you have any questions or require additional assistance.

Did this answer your question?