Home / Blog / Claude Code

Claude Code best practices from 21 client accounts

Most Claude Code advice is about prompting. Almost none of it is about configuration, and configuration is where the difference between an impressive demo and something trustworthy on a client repo actually lives. The tool is only as good as the boundary you put around it, and almost everyone sets that boundary too late. Here are the practices that survived 21 client accounts, and the ones I dropped.

Start default-deny, not default-allow

The instinct is to approve things as they come up and keep working. It's the wrong default on any repo you don't personally own.

Approving per-prompt trains you to click yes. After the fortieth approval you're not reading them, and the one that matters looks exactly like the thirty-nine that didn't. The fix is to decide the boundary once, when you're calm and not mid-task, and write it into the permission rules in settings.json. Allow the reads and the builds. Deny the things you'd regret. Leave ask for the genuinely ambiguous middle.

The practice that made this stick: I write the deny list first, before the allow list. It takes ten minutes and it's the only ten minutes that reliably prevents a bad afternoon.

What I dropped: maintaining elaborate per-project permission sets. Three tiers covering most repos beat twenty-one bespoke ones nobody keeps current. A config you don't maintain is a config that quietly stops matching reality.

The base tier, written deny-first, looks like this:

{
  "permissions": {
    "deny": [
      "Bash(rm -rf:*)",
      "Bash(git push --force:*)",
      "Bash(git reset --hard:*)",
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(./secrets/**)"
    ],
    "ask": [
      "Bash(git push:*)",
      "Bash(docker:*)"
    ],
    "allow": [
      "Bash(git status)",
      "Bash(git diff:*)",
      "Bash(npm test:*)",
      "Bash(npm run build:*)"
    ]
  }
}

Deny is evaluated before ask, and ask before allow, so the destructive entries win regardless of what the allow list says. The secrets entries matter more than the rm one in practice: the command you regret is usually rarer than the file you didn't mean to put in context.

Where the boundary actually sits Model wants a toolany tool call PreToolUse hookruns first, every call Blockedno permission prompt Permission rulesdeny, ask, allow Tool runsonce rules clear it exit 2 otherwise allowed scalably.io
The hook runs before the permission prompt, on every tool call. A hook that exits 2 stops the call before the permission rules are even consulted.

Treat CLAUDE.md as a token budget

Every token in CLAUDE.md is loaded into context at the start of every session and carried for the whole session. That makes it the most expensive text in your project, and it's usually treated as the cheapest.

The test I apply to every line: would this change what the model does on a typical task? Architecture decisions and non-obvious constraints pass. "This project uses TypeScript" does not, because the model can see that from the files. Restating what's already visible in the repo is paying rent on information you already own.

Things worth their tokens, in my experience:

  • Constraints that aren't derivable from the code. Which service is production. What must never be deleted. Which directory is generated and shouldn't be hand-edited.
  • The commands that aren't guessable. If your test runner is wrapped in a script with three required flags, say so once and stop re-explaining it.
  • Corrections that recurred. If you've said the same thing twice in two sessions, it belongs in the file.

Things that aren't: restating the framework, generic style preferences the linter already enforces, and long aspirational process documents nobody follows.

What I dropped: one large CLAUDE.md per repo. Splitting rules by directory, so a subdirectory carries its own, keeps the always-loaded root file small.

Hooks enforce what instructions only suggest

There's a real difference between telling the model not to do something and making it impossible.

Instructions are advisory. A PreToolUse hook is not. If a command would be genuinely destructive on your setup, the instruction belongs in a hook that blocks it, because a hook is deterministic and an instruction is a strong suggestion competing with everything else in context.

The practical rule I use: anything I'd be upset to explain to a client goes in a hook. Everything else can be an instruction. That's a much shorter list than people expect, which is the point. A hook that fires constantly is a hook you'll disable within a week.

One honest tradeoff: hooks add latency to every matching tool call, and a badly-scoped matcher makes the whole session feel sluggish. Scope them narrowly.

And one caveat worth knowing before you rely on this as a security boundary: a hook that times out does not block the call. It continues through the normal permission flow. So a hung hook fails open, which is another reason the permission rules underneath still have to be right.

Delegate less than you think

Subagents get their own context window, which is genuinely useful when a task would otherwise flood the main thread with output you don't need to keep. A wide search across an unfamiliar codebase is the clear case.

The clear non-case is anything you could finish in a handful of tool calls yourself. A subagent starts from nothing. Everything it needs has to be in the prompt, and writing that prompt properly often costs more than doing the work. When the brief is vague, you get back a confident summary of the wrong thing, and you won't always notice.

The heuristic that works: delegate when the output is large but the instruction is small. If the instruction has to be large to be correct, do it yourself.

Let it fail before you help

The strongest habit I've picked up, and the least intuitive one.

When something breaks, the instinct is to explain what you think is wrong. That instinct is frequently the problem, because it anchors the model on your theory, and your theory is a guess. Handing over the error and letting it investigate produces a better diagnosis more often than my opening hypothesis does.

This holds up in the same way it does with people. A good engineer given a symptom and access will out-diagnose the same engineer given a symptom, access, and a manager's confident theory about the cause.

What actually breaks in production

The failures aren't dramatic and they're rarely the model. Between February and June 2026, one platform I run executed 5,329 task-runs and 109 of them errored out, a 98% success rate. Almost none of those 109 were the model reasoning badly. They were third-party APIs timing out, rate limits, and credentials expiring mid-run. The model was rarely the failure point. The integrations were.

The configuration lesson that follows: spend your effort on the boundary and the plumbing, not on prompt-tuning. Prompt-tuning is the visible work. The boundary is the work that decides whether you can leave it running.

Frequently asked questions

What are the best settings to use in Claude Code?

Start with default-deny permissions: allow reads and builds, deny anything destructive, and reserve ask for genuinely ambiguous cases. Decide the boundary once and write it into settings.json rather than approving prompts as they arrive, because per-prompt approval trains you to stop reading them.

How do I stop Claude Code from doing something dangerous?

Use a PreToolUse hook rather than an instruction. Instructions are advisory and compete with everything else in context, while a hook is deterministic and blocks the call before it runs. Reserve hooks for actions you would genuinely regret, because a hook that fires constantly gets disabled.

What should go in a CLAUDE.md file?

Only what changes the model's behavior on a typical task: constraints not derivable from the code, commands that are not guessable, and corrections that have recurred. Leave out anything visible in the repo already, such as the framework in use, because CLAUDE.md is loaded into context at the start of every session and carried for the whole session.

When should I use a subagent in Claude Code?

Delegate when the output is large but the instruction is small, such as a wide search across an unfamiliar codebase. Do it yourself when the instruction has to be long to be correct, because a subagent starts with no context and writing a sufficient brief often costs more than the task.