← All writing
Craft · · 11 min

It's never the prompt

A plan that's easier to argue with than a diff, a simplify pass welded onto the commit, and a fence you can only get past by reading the file it wants you to read.

AI Tooling

“Okay, but what do you actually type?”

That’s the question, near enough word for word, every time somebody new to Claude Code asks me how I use it. And it’s a fair question! The interface is a text box, so of course the text box is where you’d look. But the prompting has been the least interesting part of my own setup for a good while now. I don’t think that’s just me, either.

Instead I do something closer to a habit than a system, though calling it either one oversells it. The second time I catch myself typing the same instruction, I stop typing it and put it somewhere Claude reads on its own. Everything I actually type into the box is about the task in front of me.

Plans are worthless

Starting here because it costs nothing and it’s the one thing I’d push on anybody.

Plan mode goes off, reads around the codebase, and comes back with a plan before it has touched a single file. The value isn’t that the plan is better than what it would have built anyway. Sometimes it is! Often it’s the same work, described in advance.

The value is that a plan is much easier to argue with than a set of file modifications. When the third paragraph says it’s adding an abstraction I don’t want, I point at the helper that already exists, and that costs me a minute. When the same decision arrives as a diff across five files, either I sigh and take it or I make it A Whole Thing, and now there’s a sunk cost sitting on both sides of the conversation.

So it’s plan mode for anything with more than a couple of steps in it, and not much point in the ceremony for a quick fix.

I didn’t expect the plan to also be a verdict on me. If it comes back and I can’t tell whether it’s right, that isn’t Claude being unclear, that’s me not having decided what the work is or what done looks like. Finding that out before any code exists is a much cheaper problem to have than finding it out afterward.

Kill your darlings

This one’s my favorite, mostly because of how I got to it. I have a commit skill because I got tired of relitigating commit message conventions every few days. It holds the mechanical stuff, an imperative subject, a body that explains why instead of what, and no British spellings sneaking in. None of that is gospel. It’s a set of preferences I’ve built up over years of git and I’d rather write them down once than keep re-explaining them.

The interesting part is what’s bolted onto the front of it. Before the skill stages anything or writes a word of the message, it decides whether the working tree is substantial enough to want a simplify pass, and if it is, those edits land in the same commit.

~/.claude/skills/commit/SKILL.md
Use judgment, this is a gate and not a reflex:

- **Run simplify** when the diff adds or reworks real logic: new
  functions or classes, non-trivial control flow, a multi-file
  change, or more than a couple dozen substantive lines.
- **Skip it** for trivial or mechanical changes: typo fixes, config
  bumps, lockfiles, pure renames, formatting-only diffs, one-liners.
- When it's borderline, lean toward running it.
Markdown

Two things make that work. I think the second one is the one that matters.

The first is what simplify catches. A model’s first draft tends to be correct and slightly too much: an extra layer of indirection, a helper that duplicates one sitting four files over, a function living a level lower than it needs to. None of those are bugs. That’s exactly why they survive review, mine very much included. It’s how a codebase turns into a place nobody volunteers to work in.

The second is when it runs. Back when I was invoking simplify by hand it was always against code I’d written who knows how long ago. So I’d read what it suggested without much of an opinion either way. The moment before a commit is the one moment I still have the changes in my head and haven’t started thinking about the next thing. My attention is the scarce thing here, not the tokens, so the pass is worth the most when it’s welded onto something I was doing anyway.

And the gate matters as much as the pass does. An early version ran on everything. You bump a version number, and something goes off spending oodles of tokens contemplating your package.json before reporting back that the file looks fine.

Good fences

The problem with a skill is that a skill is a document sitting on a disk. The failure mode isn’t that the document is wrong, it’s that it gets followed most of the time. Some fraction of the time Claude writes a plain git commit and none of the above happens. I suspect it’s nothing more interesting than a long session getting crowded, with the skill file as one document among a lot of other things.

So there’s a PreToolUse hook that sees every Bash command starting with git. It looks for a real git commit subcommand and denies it unless the command carries a marker.

enforce-commit-skill.sh
# Amends and fixups aren't new commits, let them through.
if printf '%s' "$command" | grep -qE '(^|[[:space:]])--(amend|fixup|squash)([[:space:]=]|$)'; then
  exit 0
fi

# Skill-emitted commit, carries the marker.
if printf '%s' "$command" | grep -qE 'CLAUDE_SKILL=commit([^A-Za-z0-9_-]|$)'; then
  exit 0
fi
Bash

The prefix does nothing. It sets an environment variable for the git process and never touches the commit itself. It’s a token, and the only thing it proves is that the skill got loaded.

I’m a little smug that the deny message never names the marker. It says raw commits are blocked and to go invoke the skill. That’s all it says. So the only route past the fence runs through the file, and by the time you’re through you’ve read the conventions anyway. I can’t help but think that does more work than the block does, because a fence that teaches you the rule while it’s stopping you doesn’t have to be right about much else.

Amends and fixups walk straight through, since a reword isn’t a new commit and the history-rewriting skills would be dead in the water otherwise. And if the directory isn’t a git repo at all the hook shrugs and gets out of the way, on the theory that git’s own error message is better than anything I’d write. That last one is a decision and not an accident, which I’d rather say out loud, having spent a couple months this spring finding out what it looks like when a hook takes that exit on its own.

There are three siblings to it. One does the same job for gh pr create. One checks git identity and only enforces inside ~/Web/cadencelabs, deliberately leaving everything else alone. Some work repos live under ~/Herd right next to personal ones, so outside that one directory the path tells you nothing, and a hook that’s confidently wrong about who you are is worse than no hook.

The third blocks commits on master, main, staging and dev. It’s the one that got persnickety in an interesting way. The first version trusted the working directory the session reported, which is fine right up until the session is standing in one repo and committing to a sibling with git -C, which is a thing I do constantly with our Shopify theme template. So now it resolves the repo the command will actually commit to, honoring a leading cd and any -C, then asks that repo what branch it’s on. Before that it was blocking legitimate feature-branch commits because the directory I happened to be standing in was on main, and waving through the ones that mattered. (The template itself is a whole other post, including the hook that lives inside it instead of in my global config.)

Permissions I leave on auto, and the deny list is two entries, both of them docker prune. In Claude we trust 🙃

Please match this vibe

Claude Code is good at a lot of things and it is not good at front of the front end work. Ask for a nice page and you’ll get one. Ask for another one next week and you’ll get another one, nice in a slightly different way, and by the fifth you have five color scales and a spacing system that’s less a system and more a set of coincidences.

So the design happens upstream now, in Claude Design, which hands back tokens and a set of components with real style objects behind them instead of a comp and a mood. The handoff to Claude Code stops being “here’s a vibe, please match it” and becomes “here’s the spec, please transcribe it.” The token values at the top of this site’s stylesheet are a verbatim copy of the design system’s own token files, every component’s CSS is a transcription of that component’s style object, and the project’s CLAUDE.md carries both of those rules plus the one that makes them mean anything: go and read the component, never work from memory.

The change in the shape of the failures got me. When the build is wrong now, it’s wrong in a way I can check. This padding doesn’t match that token. That’s a bug with a correct answer instead of a taste argument I’m having with a language model.

The hour I already paid

Everybody’s first CLAUDE.md is a pile of preferences and mine is no different, so mine has American English, BEM, alphabetized CSS declarations, and a whole little ordering for HTML attributes with an exception for when the class value is a wall of Tailwind and belongs at the end instead. Useful, unremarkable, saves me re-typing myself twice a week.

The gotchas earn their place, being the things no amount of reading the code gets you back. Every one of them is something that already cost me an hour once, written down so it costs nothing forever after. This site’s file has a note that a Liquid assign inside an include leaks back out into the caller and survives into the next iteration of the enclosing loop, which is why every card on the projects page was quietly wearing the previous project’s tag color. I would not have found that twice. I barely found it once.

I’ve made this argument about the skills we use at work already so I’ll keep it short here, but the steps are the least valuable thing in any of these files. The steps are the part I’d have gotten right on my own.

A pile, not a plan

The whole setup is noticing you’ve typed something twice and then spending 10 minutes on it. Written out in one post it looks deliberate, like something somebody sat down and designed. But it accumulated over months, one repetition at a time, and a fair bit of it I’d forgotten was even there until I went looking to write this.

Take the specifics with a grain of salt, of course. All of it got assembled around my projects and my habits, and a good half of it would be actively wrong for somebody who works differently than I do. I’d actually hand over something smaller than any of the files. The second time you type an instruction is the signal to go put it somewhere, not the fifth time, when you’re finally sure it’s a pattern. I still miss it constantly and catch myself explaining the same thing for the third time in a week, so, y’know, do as I say.

Read similar posts
9 min

Documentation for future-you

I came back to a project after a year away and found three notes I'd left myself in one confident voice, two of which saved me an afternoon and one of which cost me one.

17 min

Exit 0 means allow

Porting an internal Claude Code safety tool from Linux to macOS took a couple months, and nearly every failure I found on the new platform failed in the permissive direction while printing a checkmark on the way out.