Claude Code best practices live in the harness, not the prompt

On 2026-07-24 a developer asked Boris Cherny, who created Claude Code, why the new Opus model still over-commented its code. The answer was a one-line shell command that appends a rule to CLAUDE.md: avoid code comments unless explicitly asked. One of the replies put the objection in a single sentence: “Reliance on claude.md memories is non deterministic.”

I had already learned that the expensive way. My global instructions carried a softer version of the same rule: keep comments short. On 2026-08-24, with that rule loaded into every session, three scratchpad scripts opened with comments narrating the change they made. Nobody had argued with the rule. It had a judgement call in it, “short”, and the model made the call.

So the rule lost its judgement call. It now says no comments at all, and a PreToolUse hook refuses any edit that adds one. The sentence I wrote into the rule afterwards is the thesis of this post: “A rule with a judgement call in it gets talked past, so there is no judgement call.”

Most Claude Code best practices are advice

Search for Claude Code best practices and you get lists. A Reddit thread, a GitHub repository of tips, a Medium post, a DataCamp tutorial. They are mostly good tips. Plan before coding, keep CLAUDE.md short, give the model a way to check its work.

The problem is where a tip goes once you adopt it. It goes into a prompt or a CLAUDE.md file, and Anthropic’s own memory documentation is blunt about what happens there:

Claude treats them as context, not enforced configuration. To block an action regardless of what Claude decides, use a PreToolUse hook instead.

The best-practices page says the same thing from the other side: “Unlike CLAUDE.md instructions which are advisory, hooks are deterministic and guarantee the action happens.” Practitioners put it more bluntly. @akshay_pachaar put it as “CLAUDE.md is just a suggestion. Hooks are a guarantee.” in March.

Advice in context is weighed against everything else in context. A comment rule competes with a task that seems to want a comment. A “read the file first” rule competes with a model that is confident it knows what the file says. Sometimes the task wins, which is what you should expect from a model weighing competing inputs.

There is a second reason comment rules in particular deserve a hook. A wrong comment in one of my projects once misattributed a component to a third party, and that error was copied into documentation published on a live site. A confident comment reads like a source. Nothing tests or type-checks it, so it drifts.

Four layers, four jobs

The practices that survive move out of the prompt into the harness around the model. In my setup they fall into four layers, and the useful thing about separating them is that each one fails differently.

Layer Job Fails when
Hooks Enforce: deterministic checks the model cannot argue with They fire on things that do not matter, or block without telling the model how to fix it
Skills Teach: know-how loaded on demand when the task matches They duplicate access an MCP server should provide, or never trigger
Memory and CLAUDE.md Remember: context that is not in the code Memories go stale and get trusted as current, or they store what the repo already records
Worktrees Isolate: parallel agents without collisions Two agents share one checkout, or a shared contract changes mid-flight

Every layer has a characteristic way of producing confident wrong behavior, and most “Claude Code ignored my rule” stories are a practice sitting in the wrong layer: a prohibition written as memory, a procedure written as a prohibition, a fact written as a skill nobody triggers.

Hooks enforce, but only before the tool runs

My global hooks live in ~/.claude/settings.json. PreToolUse hooks require a file to be read before it is edited, block new code comments, screen Bash commands, scope rm, and keep WebFetch off x.com. PostToolUse hooks check for em dashes and file size, run ESLint on the edited file, and look for swallowed errors.

The split between before and after matters more than any single hook. The hooks reference has a table of what exit code 2 does per event. For PreToolUse it “Blocks the tool call.” For PostToolUse it “Shows stderr to Claude; the tool already ran.”

So a PostToolUse hook can only report something that already happened. A reply in the same thread as Boris’s answer shows the gap: @Semmonix wrote that they have “my claude.md, memory.md and even a post hook, and a skill made just for this” and comments still appear. I cannot see that setup. But if “post hook” means PostToolUse, it ran after the comment was already written and could only report it.

The second half of the failure column is the one people skip: a hook that blocks without saying how to fix it teaches the model to route around it. My comment hook’s message names the offending lines, then says what to do instead: a better name, a type, a named constant, a small function, or a test whose name states the constraint.

Every block or warning from these hooks also appends a JSON line to .harness/ledger.jsonl, which is gitignored. In the repo for this site it holds 31 events: file-size 11, lint 9, read-before-edit 6, em-dash 5. The clusters are specific. File size under src/pages accounts for 9 events, read-before-edit under src/content for 5. A /reflect command reads the ledger, finds recurring rule and path pairs, and proposes a rule, a threshold change, a lint rule or an architecture decision record, and applies nothing without approval. In fairness, no reflection has been written for this repo yet.

To set one up, put hooks under a hooks key in .claude/settings.json for one project or ~/.claude/settings.json for all of them. This is the shape of my comment guard:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$HOME/.claude/hooks/check-new-comments.py\"",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

The script reads the tool call as JSON on stdin, inspects tool_input, and exits 2 with a message on stderr to refuse. Put prohibitions on PreToolUse. Put feedback, like lint output, on PostToolUse, and expect it to arrive after the fact.

Skills teach, and a hook can point at one

A skill is a SKILL.md file under .claude/skills/<name>/ with YAML frontmatter. Claude sees each skill’s description every session and loads the full content only when the task matches, which is what makes skills the right home for know-how that only some tasks need.

The best pair in my setup is a hook and a skill working on the same problem. WebFetch does not work on x.com: a real post URL returns HTTP 402 with no body, which reads like a dead link. The block-x-webfetch hook denies the call and says why. The x-research skill teaches the path that works, through the bird CLI, and it encodes traps that calling the CLI from memory walks straight into: the response changes shape when a query paginates, so jq 'length' reports 2; search is chronological only and needs an engagement floor; search results hit a hard cap. The hook stops the wrong path, and the skill teaches the right one.

Skills fail in two ways. The first is never triggering, or triggering the wrong one. When I asked for a “Codex review”, the agent kept reaching for a Codex plugin skill that routed the request through a forwarder subagent and a companion script, when I meant the codex CLI. A global rule now says Codex means the CLI. The same episode carried a second lesson: a guessed --model flag failed outright with a 400, because that model was not available on the account type. I wrote about why I run Codex next to Claude Code at all. This routing failure was a harness problem.

The second failure is a skill that tries to be an integration. Anthropic’s extension overview draws the line cleanly: “MCP connects Claude to external services. Skills extend what Claude knows, including how to use those services effectively.” I hit the edge of that line with the Search Console MCP server. It gives access to queries, URL inspection and sitemaps, but its OAuth scope turned out to be read-only, so sitemaps_submit returns “Permission denied” even though the account owns the site. No skill can fix that, because access is the server’s job. The skill’s job is knowing what to do with the access.

When you write a skill, write the description first and put the trigger in it, because the description is what Claude matches against. Keep SKILL.md short and move reference material into files beside it. If your .claude directory is gitignored, as it is in the repo for this site, note that the worktree docs say a worktree without its own .claude/skills loads the main checkout’s project skills. That is convenient, and it also means those skills exist on exactly one machine.

Memory remembers what the repo cannot

CLAUDE.md and memory are the layer most best-practice lists are about, and the layer with the most specific failure: a memory is a claim that was true when it was written.

My file-based memory keeps one fact per file with frontmatter naming its type (user, feedback, project or reference), plus a MEMORY.md index that loads every session. Claude Code’s own auto memory uses the same shape and loads the first 200 lines or 25KB of the index, whichever comes first. Feedback and project memories carry a “Why:” line and a “How to apply:” line, because a rule without its reason cannot be applied to an edge case.

The rule that does the most work is about trust: “A memory that names a specific function, file, or flag is a claim that it existed when written.” Before recommending anything from memory, check the file exists or grep for the function.

This site’s own memory has supplied three receipts:

  • The deploy memory said there was no in-repo deploy config and no .github/workflows directory. Both became false when a scheduled-publish workflow was added, and future-dated posts stopped going live on merge. The memory was corrected on 2026-08-23, and the correction mattered more than the original fact, because the old version would have told the next session that merging publishes.
  • On 2026-10-08 the publishing memory still said the queue “runs through 2026-09-16”. The sentence was accurate. What nobody had noticed was that nothing had been published since that date.
  • An SEO memory written on 2026-08-23 recorded that the Google sitemap submission was blocked, and said: “Do not re-attempt the API call expecting a different result.” On 2026-10-08 that was still true. A memory that tells the next session what not to retry saves a wasted call.

The other half of the failure column is storing what the repo already records. Code structure, git history, fix recipes and anything already in CLAUDE.md are derivable, and a memory copy of them is a second source that drifts from the first.

In practice, keep CLAUDE.md under the 200 lines the docs recommend, and run each line through the question the best-practices page asks: “Would removing this cause Claude to make mistakes?” Give memory one fact per file, an index under its load limit, and a reason on every rule. Date anything that describes current state, so the next reader knows how old the claim is.

Worktrees isolate, and isolation includes the contract

On 2026-08-31 I dispatched ten workers in parallel on one of my projects. The expensive failures were collisions, not tokens. A rebuild of a composed tree repeatedly wiped node_modules under a running dev server. Next.js’s per-directory lock forced two workers to assemble throwaway trees just to run their gates. Both of them also reported files in git status that were not theirs, one careless git add -A away from committing a peer’s half-finished work.

Contract drift bit twice in the same run. A backend worker renamed an endpoint, and later removed a field from every message schema, each time after the frontend worker consuming it had finished and reported green. “All gates pass” was true and stale at the same time.

The same fan-out also set no model on any worker, so every one inherited the most expensive model. About two million tokens burned, and three workers died at a session limit, two of them at once because they shared one budget. I argued in the subagents post that what extra agents buy is context isolation. This run tried to buy throughput with them and paid in a shared budget.

The rules that came out of it are worktree rules: one worker per worktree, and parallel work only on independent surfaces. When two halves share an API, dispatch them in sequence or publish the contract first. Stage by path, never git add -A, while another worker is live. Rerun the dependent half’s gates after the contract half lands. Agent teams state the same constraint from their side: two teammates editing one file overwrite each other.

The harness enforces part of this now. On 2026-10-08 a session on this site started in the shared main checkout with uncommitted files, and my SessionStart hook flagged that a branch flip there could silently revert another session’s edits. The fix went into a fresh worktree branched from origin/main. Inside it, Claude Code refused a compound shell command because it could not prove the git in it stayed inside the worktree. The worktree docs list that as a check you cannot turn off. Not everything is isolated, though. The git stash stack is shared across all worktrees, so a bare git stash pop in one can pop another session’s work.

To use them, start a parallel session with claude --worktree <name>, which creates it under .claude/worktrees/ on its own branch. Add isolation: worktree to a custom subagent’s frontmatter to give every run of it a temporary worktree. Use a .worktreeinclude file to copy gitignored files like .env into each new worktree. Before removing a worktree after its PR merges, confirm its source tree matches main.

The harness has a bill too

The counter-argument is strong, and the strongest version comes from Anthropic. At Y Combinator’s Startup School, as quoted by @rohanpaul_ai, Boris Cherny told Claude Code users to delete their CLAUDE.md, skills and hooks every six months and see what the model does, because the model “may no longer need the extensive instructions that were necessary for previous models.” @undefinedKi summarized the talk with a line that names two of my hooks directly: “Corrections go. Do not add comments, read the file first, all of that.”

My own ledger supports part of this. On 2026-10-08, while adding a trailing slash to a link in this site’s Intro.astro, the file-size hook fired. The file was 486 lines against a 400 limit, and the change was one character. Because that hook is PostToolUse, the edit had already landed. What exit code 2 did was put “You MUST split it before proceeding” in front of the model, with advice to split by concern and not to flatten scoped styles into a global stylesheet. The fix moved the recent-writing list and tag cloud into their own component and took Intro.astro to 359 lines.

The hook did not care that the change was one character. That is the point, and it is also the cost: a one-character task became a refactor. The same session showed a second cost. Lint-on-edit reported two “unsafe call” errors after the first of two edits, because the call was written before its import was added. A rerun after the second edit was clean. A PostToolUse hook sees intermediate states and reports them as if they were final.

Boris made the cost argument in July, in the post that announced /checkup, whose job list includes “Turn off slow hooks.” Hooks are friction you chose, and they can be wrong about what matters.

Where I part ways with the six-month purge is on what it costs to keep each layer. A CLAUDE.md line costs context on every request whether or not it is relevant. A hook, per the extension overview, costs zero context unless it returns output. So the purge is right about corrections that live in the prompt, and the best-practices page puts the whole trade in one line: “If Claude already does something correctly without the instruction, delete it or convert it to a hook.” For hooks, the better question is whether they still fire, and on what. That is what the ledger is for. A rule that has not fired in months is a candidate for deletion. A rule that keeps firing on the same path is telling you something about the path.

Where a rule should live

Before writing a practice down anywhere, run it through one question per layer. The last branch is the one most lists leave out.

   a practice you want to keep
              │
              ▼
   can a script check it,
   every time, with no judgement?
      │ yes ──────────────► HOOK
      │                     PreToolUse to refuse,
      │ no                  PostToolUse to report
      ▼
   is it know-how that only
   some tasks need?
      │ yes ──────────────► SKILL
      │                     description is the trigger
      │ no
      ▼
   is it a fact the code
   cannot tell the model?
      │ yes ──────────────► CLAUDE.md or MEMORY
      │                     dated, with its reason
      │ no
      ▼
   is it about agents
   working at the same time?
      │ yes ──────────────► WORKTREE
      │                     one agent per checkout
      │ no
      ▼
   delete it. the model
   probably does it already

The no-comments rule went through this diagram twice. The first time, as “keep comments short”, it failed the first question because “short” needs judgement, so it landed in CLAUDE.md and got talked past. The second time the judgement was removed, the answer to the first question became yes, and it became a hook.

That is the pattern under most of the Claude Code best practices worth keeping: the practice moves to the layer where the model no longer has to agree with it.

Frequently asked questions

What are the most important Claude Code best practices?
Decide where each practice lives before you write it down. Anything that must happen every time belongs in a hook, because CLAUDE.md instructions are advisory and a PreToolUse hook can refuse the tool call outright. Know-how that only some tasks need belongs in a skill, which loads on demand. Facts the code cannot tell Claude belong in CLAUDE.md or memory, kept short. Parallel agents each get their own git worktree. A rule that fits none of those is usually a rule you can delete.
Why does Claude Code ignore my CLAUDE.md?
Because CLAUDE.md is loaded as context, and context is weighed rather than obeyed. Anthropic's memory documentation says Claude treats memory files as context, not enforced configuration, and points to a PreToolUse hook for anything that has to be blocked regardless of what Claude decides. Long files make it worse: the best-practices guide says a bloated CLAUDE.md causes Claude to ignore your actual instructions, and recommends keeping each file under 200 lines.
What is the difference between a PreToolUse and a PostToolUse hook?
A PreToolUse hook runs before the tool call and can block it: exit code 2 stops the call and sends your stderr message back to Claude. A PostToolUse hook runs after the tool has already executed, so it cannot undo anything. Exit code 2 there only shows stderr to Claude. That makes PreToolUse the layer for prohibitions and PostToolUse the layer for feedback such as lint results or file-size warnings.
Should I delete my CLAUDE.md, skills and hooks when a new model ships?
Boris Cherny has recommended trying exactly that every six months, on the grounds that newer models may not need instructions written for older ones. It is good advice for corrections that live in the prompt, since every CLAUDE.md line costs context on every request. Hooks are a different trade: they cost zero context unless they fire, so the better test is whether a hook still fires at all, and on what. A ledger of hook events answers that with data instead of a guess.
Nick Sawinyh
Nick Sawinyh

Web3 BD & product strategist with 10+ years in crypto, specializing in turning complex technical products into clear strategies that drive adoption and grow ecosystems.