I was working on a client's Shopify theme, and the task was simple: remove an unused Reviews section from the About page. I edited templates/page.about.json, deleted the Reviews block, and pushed:
shopify theme push --only=templates/page.about.jsonThe push reported success, no errors. I opened the live About page, and the Reviews section was gone as expected. Done, I thought. Until a few hours later the client messaged me asking why the image had changed. The hero image on the About page, which had been a product photo they had chosen, was now back to the theme's default fallback, product-3in1-box.webp. The client had to re-upload their image manually through the admin.
My first instinct: did I delete the wrong block? I checked the diff — no. I had only touched the Reviews block. But the hero image, which I never went near, had changed too. It felt like my push had reached into something I never touched.
Why this happens
Here is what was actually going on, and it is a classic trap when a live theme is shared with a client who also edits through the Shopify theme editor.
When a client uploads an image or changes text through the theme editor, those changes do not land in the Liquid markup files. They are stored as settings inside templates/*.json (and config/settings_data.json), server-side. Settings like image_picker, text, and other block settings live in that JSON, not in the template code. So the hero image the client uploaded was really an image_picker field inside the server's copy of templates/page.about.json.
The problem: my local working copy did not have that field. My copy was still an older version, from before the client uploaded the image. When I edited that local JSON to remove Reviews and pushed, I overwrote the entire contents of the server's templates/page.about.json with my local version. The hero image_picker field holding the client's uploaded image was not in my local version, so Shopify fell back to the Liquid fallback, and up came product-3in1-box.webp.
The key thing to understand: a push is a file-overwrite operation, not a smart merge. The push does not know which changes are "mine" and which are "the client's". It just replaces the server file with the local one. Any admin-side work not present in my local copy is gone.
The fix
The core rule: before touching any file the client can edit through the editor, pull the server version first, then edit on top of it.
Before pushing anything that touches templates/*.json, config/settings_data.json, or a section with a media picker, I now always pull that specific file first with --nodelete:
shopify theme pull --path=<dir> --store=<store> --theme=<id> --only=templates/page.about.json --nodeleteThe --nodelete matters so this pull does not remove other local files that are absent on the server. After pulling, I diff it to see what settings and blocks the client added:
git diffThat diff is where the hero image_picker field holding the client's image shows up — the one that was missing from my local copy. Only then do I make my edit, removing the Reviews block, on top of a version that already includes the client's work. Then push:
shopify theme push --only=templates/page.about.jsonNow my push carries both the client's hero image and my edit at once, without overwriting anything valuable.
What is safe and what is not
What makes this trap slippery: not every file needs this pull-first ritual. Pure code files — Liquid markup, CSS, and JS — are safe to push directly. Why? Because the theme editor cannot change those files. The client cannot edit sections/hero.liquid through the editor, so there is no server-side work there for me to overwrite.
The dangerous files are specifically the ones that store values the client can change: templates/*.json, config/settings_data.json, and any section with settings of type image_picker, video, or file_reference. That is where the client's uploads and copy edits live, and that is where a plain push can wipe them.
The takeaway
On a theme shared with a client, templates/*.json and settings_data.json are not just mine — they are shared territory. The server is the source of truth for anything the client typed or uploaded through the editor. A push overwrites, it does not merge, so pushing without pulling first will silently wipe the client's admin-side work. Since this incident, my flow is always: pull --only=<file> --nodelete, diff to see what the client added, edit on top, then push. For pure code files I skip the ritual, but the moment an image_picker or any media picker is involved, I pull first without exception. One extra command beats explaining to a client where their photo went.
