10 KiB
10 KiB
Configuration Best Practices
Concrete, actionable patterns. No generic advice.
CLAUDE.md
- Optimise for prompt-cache stability. Place stable content in the first 30 lines (cache-friendly prefix); volatile content (timestamps, dynamic counts, rolling activity logs) goes below that threshold or moves to an
@import-ed file outside the cache prefix. On Opus 4.7 the dominant cost lever is cache reuse, not file length.1 - Use
@importfor specs/docs.@path/to/spec.mdinlines the file at session start. Max 5 hops, but keep chains ≤ 2 hops — every@importboundary fragments the prompt-cache prefix. Keeps the main file scannable. - Use HTML comments for maintainer notes.
<!-- Updated 2026-01-01: reason -->is stripped before context injection — zero token cost. - Put personal dev notes in
CLAUDE.local.md, notCLAUDE.md. AddCLAUDE.local.mdto.gitignore. Team members' sandbox URLs should never appear in git. - Write
~/.claude/CLAUDE.mdfor preferences that apply everywhere. Communication style, preferred tools, output format — not project-specific config. - Use clear markdown headers (
##sections). Claude uses the structure to navigate; unstructured text is harder to follow selectively. - Avoid contradicting project settings.json. CLAUDE.md is a user message; settings.json permissions take precedence. Don't document permissions in CLAUDE.md — put them in settings.json where they're enforced.
settings.json
- Add
$schemato every settings.json."$schema": "https://json.schemastore.org/claude-code-settings.json"enables autocomplete in VS Code and Cursor. Takes 2 seconds, saves every future edit. - Use all three scopes: user, project, local. User (
~/.claude/settings.json) for personal defaults. Project (.claude/settings.json) for team agreements. Local (.claude/settings.local.json) for personal project overrides. - Put env vars in
settings.jsonenvblock, not shell.{"env": {"NODE_ENV": "development"}}ensures they're always set in Claude sessions, regardless of how the shell was launched. - Set
defaultMode: "acceptEdits"for active development projects. Eliminates per-file permission prompts. Use"plan"for infrastructure repos where you want read-only analysis by default. - Deny
.envandsecrets/explicitly.{"permissions": {"deny": ["Read(./.env)", "Read(./secrets/**)"]}}— Claude cannot read these even if it reasons it should. - Pre-allow repetitive safe commands.
{"permissions": {"allow": ["Bash(npm run *)", "Bash(git status)", "Bash(git log *)"]}}— eliminates constant prompts for read-only git operations. - Configure
attributionfor org identity.{"attribution": {"commit": "Generated with Claude Code [bot]", "pr": ""}}— keeps commit history clean and attributable. - Set
effortLevelper project, not per prompt.{"effortLevel": "high"}for complex codebases,"low"for simple scripts. Avoids forgetting to set it each session.
Hooks
- Add a
Stophook before anything else.Stophook on session end is the most useful starting point — session summary, auto-commit prompt, notification. Many users have zero hooks; one Stop hook delivers immediate value. - Use
PostToolUseon Write/Edit for auto-formatting.{"PostToolUse": [{"matcher": "Write|Edit", "hooks": [{"type": "command", "command": "prettier --write ${CLAUDE_TOOL_OUTPUT_PATH}"}]}]}— eliminates manual format steps. - Use
PreToolUseon Bash for security. Validate shell commands before execution. Exit code 2 blocks the tool call with an error message shown to Claude. - Use
SessionStartfor context injection. Inject git branch name, active Linear issue, or CI status into context at session start. Cheaper than asking Claude to fetch it. - Add
Notificationhook for desktop alerts. When Claude needs input (permission prompt, idle), get a system notification. Without this, long sessions require constant manual checking. - Match MCP tools precisely.
"mcp__.*__write.*"matches all write tools from all MCP servers."mcp__filesystem__.*"matches all filesystem tools. Use patterns, not exact names. - Keep hook scripts fast (< 2s for PreToolUse). Blocking hooks run synchronously. Slow PreToolUse hooks add latency to every tool call. Use async for logging/reporting.
- Use
${CLAUDE_PLUGIN_ROOT}for paths in plugin hooks. Absolute paths break when plugins move.${CLAUDE_PLUGIN_ROOT}/hooks/scripts/check.shis portable.
Rules (.claude/rules/)
- Use
paths:frontmatter on every rules file. Rules withoutpaths:load for every file. A TypeScript rules file withpaths: ["**/*.ts", "**/*.tsx"]only loads for TypeScript work — zero overhead otherwise. - One rules file per domain or language.
typescript.md,python.md,testing.md,migrations.md— not one bigcoding-rules.md. Granular files = granular loading. - Put project rules in
.claude/rules/, user rules in~/.claude/rules/. Project rules are team-specific and committed; user rules are personal preferences across all projects. - Symlink shared rule sets. If multiple projects share rules, symlink:
ln -s ../../shared/rules/security.md .claude/rules/security.md. Claude follows symlinks. - Test path globs before committing.
paths: ["src/**"]doesn't match./src/file.ts— leading./matters. Test with the actual file paths Claude will encounter.
MCP
- Commit
.mcp.jsonto git. Team-shared MCP servers belong in.mcp.jsonat project root, not in individual~/.claude.jsonfiles. One commit, everyone gets the servers. - Set
enableAllProjectMcpServers: truein project settings.json for zero-friction team onboarding. New team members don't have to manually approve each server. - Set trust levels explicitly.
"trust": "workspace"for project-specific servers;"trust": "trusted"only for servers you fully control. Default is untrusted (sandboxed). - Use
@server:resource/pathfor dynamic data.@github:repos/owner/repo/issuespulls live data into context. More reliable than asking Claude to fetch and parse. - Deny MCP tools you don't want Claude to invoke.
{"permissions": {"deny": ["mcp__filesystem__write_file"]}}— even with a server connected, specific tools can be blocked.
Skills
- Add
descriptionto every skill. Withoutdescription, Claude never auto-invokes the skill. The description is the trigger — be specific about when to use it. - Set
disable-model-invocation: trueon deploy/delete skills. Side-effect commands should only run when the user explicitly types/deploy, not when Claude decides it's appropriate. - Use
!git diff HEAD`` for dynamic context. Dynamic shell execution inlines current state at invocation time. Better than hardcoded file references that go stale. - Use
context: forkwith a custom agent for isolated research. Forks run in a separate context (and optionally a separate model), keeping research overhead out of the main session. - Add
argument-hintto all parameterized skills.argument-hint: "[issue-number]"shows in the/menu autocomplete. Without it, users forget the expected argument format. - Store large reference docs in skill subdirectory, not SKILL.md. SKILL.md describes when to load each reference file. The references themselves stay separate so they're only loaded when needed.
Agents
- Restrict tools to the minimum needed. A read-only research agent should have
tools: ["Read", "Glob", "Grep"], not all tools. Scoped agents are safer and faster. - Match model to task complexity. Haiku for file discovery and scanning; Sonnet for implementation; Opus for architecture and analysis. Don't use Opus for tasks that are primarily file reading.
- Set
maxTurnson autonomous agents. Without a turn limit, a misconfigured agent can run indefinitely.maxTurns: 20is a reasonable default for most tasks. - Write
descriptionas a trigger condition, not a title. "Use when analyzing TypeScript files for type errors" beats "TypeScript analyzer". Claude uses the description to decide delegation. - Use
isolation: worktreefor agents that make file changes. Agents running in their own worktree can't interfere with the main session. Changes are reviewable before merge. - Enable
memory: "user"for domain-expert agents. A security-reviewer agent that accumulates codebase knowledge across sessions gets better over time. Addmemory: "user"to the frontmatter.
Permissions
- Start with
defaultMode: "acceptEdits"for most projects. Then add specificdenyrules for sensitive paths. More productive than prompting for every file write. - Block secrets files by pattern, not by name.
"deny": ["Read(./.env*)", "Read(./**/secrets/**)", "Read(./**/*.pem)"]— catch all variants, not just.env. - Use
additionalDirectoriesfor cross-repo work. If Claude regularly reads../shared-lib/, add it:{"additionalDirectories": ["../shared-lib/"]}. Otherwise Claude can't access it without prompts. - Configure
autoMode.environmentbefore using auto mode. Without it, Claude's background safety classifier triggers false positives on your org's internal tool names and domains. - Add
Agent()deny rules for sensitive agents.{"deny": ["Agent(general-purpose)"]}prevents the most powerful agent from running without explicit permission.
-
The "keep CLAUDE.md under 200 lines" threshold was a Sonnet-era adherence heuristic — Sonnet's attention quality dropped on longer files, so trimming raw line count was the optimisation lever. Opus 4.7 uses prompt-cache structure as the dominant cost driver: the first 30 lines must stay byte-stable across turns to keep the cache hit, and
@importboundaries fragment the cached prefix. A 400-line CLAUDE.md with stable structure outperforms a 150-line file whose top contains a daily-rolling activity log. Seeknowledge/opus-4.7-patterns.mdfor detection IDs (CA-TOK-001..003). ↩︎