← All writing
Craft · · 9 min

The repo with no code in it

Templates that fork at birth, a CLAUDE.md documenting the wrong repo, and one question you have to answer first.

Shopify Tooling

Every new Shopify project at work used to start the same way, which is to say it started by copying the last one. You’d clone whichever client repo was most recently in okay shape, rip out the theme, nuke the git history, and start typing. And that worked, in the narrow sense that it produced a repo. But you also inherited that project’s specific weirdness: whatever hardcoded store URL somebody left in a script, whatever half-finished workflow file, whatever npm run alias that only made sense in the context of a ticket from eight months ago. Every project was a slightly corrupted photocopy of the one before it, and nobody could tell you which generation of the copy they were looking at.

So I built a template repo. Extremely boring answer to the problem, and I want to be upfront that the boring part isn’t the interesting part.

The interesting part is that it ships no theme code at all.

That felt wrong when I landed on it. A template with nothing in it? But Shopify themes don’t live in your repo, not really. They live on the store. The actual 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 somebody’s marketing team made last Tuesday. So the very first thing you do in a new project is npm run pull:live, and the entire theme lands: sections/, snippets/, templates/, layout/, assets/, config/, locales/. There is nothing for a template to seed. Shipping a starter theme would be worse than useless, because step one would be deleting it.

What’s left when you take the theme out is just infrastructure. A package.json that’s nothing but thin wrappers around the Shopify CLI. A shopify.theme.toml for the store config. Two GitHub Actions workflow files. An .mcp.json. A README. ls on main returns about 11 things, and I think that’s the honest shape of the thing rather than an embarrassment. The template isn’t a head start on the code. It’s a head start on everything around the code.

The first real design decision was where the store URL goes. Originally it lived in package.json, baked right into the script strings, which meant package.json was different in every single project. Moving it into shopify.theme.toml (which the Shopify CLI reads natively, so this isn’t even a clever trick, it’s just using the tool as intended) meant package.json became byte-for-byte identical everywhere.

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

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

I did not appreciate at the time how much that one move would matter. My instinct going in was that a good template is highly configurable, lots of knobs, adapt to anything. It turns out the opposite is closer to true: what makes a template maintainable is pushing as much of it as possible toward being generic, so there’s exactly one file a human is expected to edit and everything else can be treated as identical across every project forever. Configurability is drift with better PR.

The commented block in there is for redesign projects, where the new build lives on an unpublished theme sitting alongside the client’s live one. Those get their own named CLI environment, and there’s a small gotcha I lost 20 minutes to.

That populated block does double duty: it’s also the signal our deploy workflow uses to decide whether a merge to main goes to the live theme or the redesign theme. Which is the kind of thing that feels clever for about a day and then starts to worry you, because now the difference between “deploys to a preview nobody sees” and “deploys to the client’s actual storefront” is whether three lines are commented out. So an uncommented block still holding the placeholder values hard-fails the deploy on purpose. Fill it in or comment it out, no third option. But I’ve written about that side of it already.

Anyway. Here’s the thing I got wrong, and it’s the reason any of this is worth a post.

gh repo create --template is a copy. One time, at birth, and then the connection is severed. There is no upstream. The template isn’t a dependency the client repo tracks, it’s a photograph of the template taken on whatever day that repo happened to get made. So a year in you have ten client repos, each frozen at a different point in the template’s history, and every improvement you make to the template applies to precisely zero of the projects you already have. You’ve done the thing where you hand out photocopies of a form and then quietly change the form.

Which meant the actual work wasn’t the template. It was writing something that could go back 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 cannot write that until you can answer one question for every file in the repo, out loud, without hedging: who owns this?

Four answers, as it turned out. Some files are template-owned, like the workflow callers and .mcp.json, and the sync just overwrites them, because a client project has no business having opinions about those. Some are shared, like package.json and shopify.theme.toml and the README, where you take the template’s structure and scripts but preserve every value the client filled in. Some are client-owned and get touched under no circumstances whatsoever: all the theme directories, the real store URL, the theme IDs.

And then there’s a fourth category I hadn’t even considered until I went looking, which is files that should not exist in a client repo at all.

Because 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 shipped with tooling for making client projects. Along with the template’s own CLAUDE.md, which described, in loving and confident detail, a repo that isn’t the one you’re in. Anyone opening a client project and reading that file would come away believing their repo contained no theme code, which is a pretty impressive thing to be wrong about while standing inside 200 Liquid files.

The fix for the skills was one git rm -r during onboarding. The fix for the CLAUDE.md was more interesting, because my first instinct was to ship a skeleton version with blanks in it, and I’m glad I didn’t. A half-filled-in skeleton is genuinely worse than an empty repo, because it reads as documentation. It has headings. It looks authoritative. It just happens to be describing nothing. So now onboarding pulls the live theme first and then runs /init, which reads the theme that’s actually there and writes about that: which base theme it is, what version, how the CSS is organized, which third-party integrations are wired in. Documentation generated after the facts exist instead of before.

The most recent addition is smaller and I like it more than it deserves. There’s a hook committed into the repo’s .claude/ that intercepts any Shopify theme push (the push:* scripts, the raw CLI they wrap, chained commands, all of it) and asks you to confirm before it runs. It asks, it doesn’t block, and every early exit in the script allows the command, so if it breaks it breaks in the direction of letting you work.

The part I care about is that it’s committed rather than sitting in my personal global config. A safety rule that lives on one person’s laptop isn’t a safety rule. It’s a personal habit that happens to have a shell script attached, and it protects exactly one person from exactly one class of mistake while everyone else is out there raw-dogging shopify theme push on a Friday. Putting it in the repo makes it a property of the project instead of a property of me, which is the only version that survives me being on vacation.

So the thing I set out to build was a starting point, and what I actually ended up with is a claim about ownership. The template’s real content isn’t the 11 files. It’s the assertion that these files belong to us and are safe to overwrite, and those files belong to the client and we don’t get to have feelings about them, and this third pile needs careful hands. The scaffolding is just where that claim happens to be written down.

There’s a second one now, for the projects where we’re putting a Laravel front end over a Magento back end, and building it was much easier because the taxonomy transferred wholesale even though not one of the 11 files did. Which I take as weak evidence that the taxonomy was the actual artifact.

Take all of this with a grain of salt, obviously. This is one agency’s setup for two platforms, and Shopify’s whole deal is that the theme lives somewhere you don’t control, which is a fairly unusual constraint that shapes basically every decision above. If your source of truth is your repo, most of this is solving a problem you don’t have. But if you’ve got a template that ten projects were born from and no way to reach any of them, I’d start with the taxonomy rather than the tooling. The tooling is easy once you know who owns what.

Read similar posts
9 min

Fail closed

How our Shopify theme deploys went from someone running a push command on a laptop to one repo of reusable GitHub Actions workflows, and the backup-cleanup bug that had to get wrong three times before I stopped trying to be clever about it.

9 min

Write it down once

Most of what makes Claude Code work for me isn't prompting, it's a few files I wrote once: a plan-mode habit, a /simplify pass welded onto /commit, a hook that won't let me hand-write a commit, and a CLAUDE.md that's mostly scar tissue.