Who owns this file?
A template that stops being a template the moment you use it, a CLAUDE.md confidently describing a repo nobody was standing in, and one question you have to answer before any of the tooling makes sense.
A coworker opened one of our client repos last week, 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 gets created from a template repo I put together over the winter, and that template really does have no theme code in it. gh repo create --template copies the whole thing, documentation very much included, 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 winter 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, and 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, sitting 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 what’s left is 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 the fall, an .mcp.json so everybody’s MCP servers match, a README. ls on main comes back with about 11 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, which the CLI reads natively so this is using the tool the way it wants to be used, meant package.json became identical everywhere.
[environments.default]
store = "STORE_URL"
# [environments.redesign]
# store = "STORE_URL"
# theme = "REDESIGN_THEME_ID"
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. The opposite turns out to be closer to true. A template is 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 and 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, and 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. Which feels neat for about a day and then starts to worry you, since the difference between deploying to a preview nobody sees 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. The part I got wrong.
gh repo create --template is a copy. One time, at creation, and then the connection is severed. There’s no upstream. The template isn’t a dependency the client repo tracks, it’s a snapshot of the template taken on whatever day that repo happened to get made.
So a year in you’ve got 10 client repos each frozen at a different point in the template’s history, and every improvement you make to the template applies to none of them. It 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, which is a great deal for hypothetical future me and no help whatsoever to the person doing a maintenance ticket today.
So the actual 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. The workflow callers, 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 the reason the sync isn’t a cp.
Some are client-owned and get touched under no circumstances whatsoever. Every theme directory, the real store URL, 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, which is files that should not be in a client repo at all, and which I only found because I went looking for the first three.
gh repo create --template copies everything, and 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, and 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 back in 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 fix for the CLAUDE.md was more interesting, because my first instinct was to include 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 an empty repo. It reads as documentation. It has headings, it looks authoritative, it just happens to be describing nothing, and 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 instead of before, which sounds obvious typed out like that, and the template went two months without it.
In defense of a speed bump
The most recent addition is small and I like it more than I expected to. There’s a hook committed into the repo’s .claude/ that watches for anything pushing a theme, the push:* scripts, the raw CLI they wrap, either of those buried in the middle of a chained command, and asks you to confirm before it runs.
# 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
It asks, it doesn’t block, and 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 the fall that a confirmation prompt isn’t a process and I stand by that. 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, and nobody’s laptop is anywhere near it. The hook is for the case the pipeline can’t see, which is a person in a terminal at the end of a long week with the CLI in their hand and a store URL already filled in. 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 has stopped being careful about this.
Sort first, script second
I set out to build a starting point and what I ended up with is a claim about ownership. The template’s real content isn’t the 11 files. It’s the assertion that some of them belong to us and are safe to overwrite, some belong to the client and we don’t get to have opinions about them, and a third group needs a person to look at it. The scaffolding is just where that claim happens to be written down.
Take all of this with a grain of salt, naturally. It’s one agency’s setup, and Shopify’s whole deal is that the theme lives somewhere you don’t control, which is 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, and 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.