Who owns this file?

A template repo is a copy the moment you use it, so keeping client projects current starts with deciding who owns each file.

#shopify #tooling

Back in June a coworker opened one of our client repos, read the CLAUDE.md sitting in the root, and asked me why it said the project had no theme code in it. They were looking at a couple hundred Liquid files while they typed that.

The file wasn’t wrong, exactly. It was accurate about a completely different repo.

Every Shopify project we start now comes from a template repo I set up in April. That template really does have no theme code in it. gh repo create --template copies the whole thing, documentation and all. So every client project we’d made was carrying a confident little description of the template it came from instead of itself.

That’s the small funny version of a problem I spent most of the spring on. So let me back up to the plumbing first.

Batteries not included

Every new Shopify project used to start by copying the last one. Clone whichever client repo was most recently in decent shape, rip the theme out, nuke the git history, start typing. Which works, in the narrow sense that it produces a repo.

It also hands you that project’s specific weirdness: a hardcoded store URL somebody left in a script, a half-finished workflow file, an npm run alias that only ever made sense next to one ticket. Every project was a slightly corrupted copy of the one before it. Nobody could tell you which generation of the copy they were looking at.

So, a template repo, which is the dullest answer available. Except that the template has nothing in it.

That felt wrong when I landed on it. A template with no code? But Shopify themes don’t really live in your repo. They live on the store.

The source of truth for the theme you’re about to work on is the client’s published theme. It sits in their admin, carrying whatever content edits their marketing team made last week. So in a new project you start with npm run pull:live and the entire theme lands, sections/ and snippets/ and templates/ and layout/ and assets/ and the rest of it.

There’s nothing for a template to seed. Including a starter theme would be worse than useless because step one would be deleting it.

Take the theme out and you’re left with infrastructure. A package.json that’s thin wrappers around the Shopify CLI and nothing else, a shopify.theme.toml for the store config, two workflow files that call the shared ones I wrote in March, an .mcp.json so everybody’s MCP servers match, a README. ls on main comes back with 11 or so things, which I think is about right. It’s a head start on everything around the code instead of a head start on the code.

The only file you fill in

The first real decision was where the store URL goes. Originally it lived in package.json, baked into the script strings, which meant package.json was different in every single project. Moving it into shopify.theme.toml made package.json identical everywhere. The CLI reads that file natively anyway, so this is using the tool the way it wants to be used.

shopify.theme.toml
[environments.default]
store = "STORE_URL"

# [environments.redesign]
# store = "STORE_URL"
# theme = "REDESIGN_THEME_ID"
TOML

I didn’t appreciate at the time how much that one move would do. Going in, my instinct was that a good template is configurable. Lots of settings, adapts to anything, the whole nine yards.

I think the opposite is closer to true. A template stays maintainable when you push as much of it as you can toward generic, until there’s exactly one file a human is expected to edit. Everything else can be treated as identical across every project forever.

The commented block is for redesign projects, where the new build lives on an unpublished theme sitting next to the client’s live one. Those get their own named CLI environment.

Named environments don’t inherit from the default one, so store has to be repeated two lines under the store that’s already right there. On screen that looks exactly like a copy/paste mistake somebody forgot to clean up. It cost me an hour before I believed it.

That block does double duty because it’s also the signal our deploy workflow reads to decide whether a merge to main goes to the live theme or the redesign one. That feels neat for about a day and then it starts to worry you. The difference between deploying to an unpublished theme and deploying to the client’s storefront is now whether three lines have a # in front of them.

So an uncommented block still holding its placeholder values stops the deploy on purpose. Fill it in or comment it out, no third option.

Not a subscription

Anyway, on to the part I got wrong.

gh repo create --template is a copy. It happens once at creation and then the connection is severed. There’s no upstream. A client repo is a snapshot of the template as it stood on whatever day that repo happened to get made.

So give it a year and you’ve got 10 client repos, each frozen at a different point in the template’s history. Every improvement you make to the template reaches the next project you create and not one of the ones you already have.

I’d built a thing that gets better over time for an audience of repos that don’t exist yet. That’s a great deal for hypothetical future-me and no help whatsoever to the person doing a maintenance ticket today.

The real work was never the template. It was writing something that could walk into a repo that already exists, months later, and update the parts it’s allowed to update without touching the parts it isn’t. And you can’t write that until you can answer one question about every single file in there without hedging.

Four piles

Who owns this?

Three answers, and then a fourth I hadn’t thought about at all.

Some files are template-owned, like the workflow callers and the .mcp.json. The sync overwrites those and doesn’t ask, because a client project has no business deviating from them.

Some are shared, like package.json and shopify.theme.toml and the README, where you take the template’s structure and its scripts and preserve every value the client filled in. That’s the pile that needs real code. It’s why the sync isn’t a cp.

Some are client-owned and don’t get touched under any circumstances. That’s every theme directory, the real store URL and the theme IDs. If the sync ever writes to one of those it’s a bug no matter how good its reasoning was.

And then there’s the fourth pile, files that shouldn’t be in a client repo at all. I only found it because I went looking for the first three.

gh repo create --template copies everything. Everything included the three lifecycle skills that exist to create and maintain client repos, so every client project we’d ever made came with the tooling for making client projects 😐

That one’s just stupid. The fix was one git rm -r during onboarding.

And it included the CLAUDE.md my coworker was reading.

A skeleton with blanks in it

Anyone opening a client project and reading that file came away believing their repo contained no theme code, which is an impressive thing to be wrong about while looking directly at sections/.

I keep running into versions of this. A .gitignore line that was correct and had never done anything last September, and now a CLAUDE.md that was accurate about a repo nobody was standing in. Neither one looks wrong when you open it. That’s the whole trouble with both of them.

The fix for the skills was a delete. The CLAUDE.md was more interesting because I was tempted to keep a skeleton version with the blanks left in. I’m glad I talked myself out of that. A half-filled-in skeleton is worse than leaving the file out.

It reads as documentation. It has headings and it looks authoritative and it just happens to be describing nothing. Nobody can tell which sections somebody meant and which ones came with the box.

So onboarding pulls the live theme down first and then runs /init, which reads the theme that actually landed and writes about that. Which base theme it is and what version, how the stylesheets are organized, which third-party apps have their fingerprints in the layout.

The file gets written after the repo has contents in it, not before. That sounds obvious typed out like that. The template still went two months without it.

In defense of a speed bump

One of the later additions is small and I like it more than I expected to. There’s a hook committed into the repo’s .claude/ that reads every command Claude is about to run and looks for anything pushing a theme (the push:* scripts, the raw CLI they wrap, either of those buried in the middle of a chained command). When it finds one, Claude has to stop and ask you before it runs.

.claude/hooks/confirm-theme-push.sh
# Not a theme push, so there is nothing to ask about.
printf '%s' "$command" | grep -qE 'npm run push:|shopify theme push' || exit 0

cat <<'JSON'
{"hookSpecificOutput": {"hookEventName": "PreToolUse",
  "permissionDecision": "ask",
  "permissionDecisionReason": "This pushes a theme to a live store."}}
JSON
Bash

It asks and never blocks. Every early exit in the script allows the command, so when it breaks it’ll break in the direction of letting you work.

I know, I know. I said in June that a confirmation prompt isn’t a process and I still think so. This isn’t the process. The process is the pipeline, where you merge to main and something backs the theme up and deploys it and posts to Slack.

The hook is for the case the pipeline can’t see. That’s Claude in the middle of a task with the CLI on its path and a store URL already filled in, deciding the quickest way to check its own work is to push it. A speed bump is the wrong answer for a deploy and roughly the right answer for that.

I actually care that it’s committed instead of sitting in my personal global config. In my config it protects me, on my machine. It’s a personal habit that happens to have a shell script attached to it. In the repo it’s a property of the project, which means it’s there for whoever picks the thing up in a year, and for the version of me who’s approving commands without reading them late on a Friday.

Sort first, script second

I set out to build a starting point and ended up with a claim about ownership. Some of these files belong to us and are safe to overwrite, some belong to the client and we don’t get to have opinions about them, and the shared ones in between have to be merged carefully. The 11 files are just where that claim happens to be written down.

I’m just one data point here, with one agency’s setup. Shopify’s whole deal is that the theme lives somewhere you don’t control. That’s a genuinely unusual constraint and it shapes every decision up there. If your source of truth is your own repo then a good half of this is solving a problem you don’t have.

But if you’ve got a template that 10 projects were created from and no way to reach any of them, I’d start with the taxonomy instead of the tooling. Go and write the file list out and say who owns each one out loud. Pay attention to the ones where you hedge because those are the ones that’ll bite.

The sync itself took an afternoon once I had the list. Getting the list took weeks. I’d have guessed it the other way around before I started.

Read similar posts

View posts by tag

#accessibility #agency #ai #careers #cross-browser-compatibility #css #culture #debugging #documentation #ecommerce #forms #git #html #images #javascript #performance #php #privacy #scss #security #shopify #small-print #tooling #typography #wordpress