Configuring Claude Code: From Defaults to Engineering Partner
The settings that genuinely move the needle in Claude Code: a lean context, correct hooks, permission modes, effort control and subagents — with the real mechanisms instead of the half-truths making the rounds.
by Jean Pierre Kolb ·
Claude Code's default settings are good enough to be productive right away — and that's exactly the trap. "Good enough" leaves a surprising amount on the table: speed, but above all control. Claude Code isn't a black box you accept as it comes; it's a configurable engineering platform. The quality you end up with depends not only on the model but at least as much on the system you build around it.
This isn't a beginner's guide. It's for people who already use Claude Code daily and want more consistent, more reliable results. I'll walk through the knobs that actually pay off — in four blocks: context, safety & automation, how you work with the agent, and more perspectives through models and subagents. For each setting I name the real mechanism, because several of these attract configurations that simply don't work. Every example is yours to copy directly.
Block 1 — Feed context deliberately
Everything starts with what Claude sees in every session before you type a single word. Be generous here and you think you're doing the model a favor. The opposite is true.
Keep CLAUDE.md small
CLAUDE.md is your permanent session context: its contents land in the prompt of every conversation before anything else happens. The temptation is to grow this file into an encyclopedia — architecture, conventions, domain knowledge, onboarding, deployment steps, API contracts, project history. "More context, better answers," so the assumption goes.
It doesn't hold. Large language models don't weigh every part of a long document equally; information buried in the middle of long texts reliably gets lost — the well-known "lost in the middle" effect. The bigger CLAUDE.md grows, the more likely Claude is to overlook the very rule you cared about. So you pay twice: more tokens per session and less reliable attention.
The rule of thumb, therefore: CLAUDE.md should hold only what belongs in every single session.
- technology stack
- cross-cutting coding principles
- hard architectural guardrails
- project-wide rules
- links to further documentation
Everything else moves out. Setting a specific number as law would be dishonest — but if your CLAUDE.md grows into the five-digit character range, that's a reliable alarm that too much situational detail is stuck in it.
Move detail out modularly — with the real mechanisms
The question then is: where does the rest go? Claude Code offers two clean routes, and it's worth not confusing them.
@ imports in CLAUDE.md. A line consisting only of @path/to/file.md pulls that file in at session start. This keeps the main CLAUDE.md lean as a table of contents while the detail lives in focused files:
# Project conventions
Stack: NestJS, PostgreSQL, TypeScript (strict).
Architecture: clean architecture, the repository pattern is mandatory.
Details when needed:
@docs/architecture.md
@docs/testing-strategy.md
@docs/api-contracts.mdThis is still permanent context — the imported files are loaded along with it. @ imports organize; they don't reduce. For genuine on-demand loading you need the second route.
Skills — loaded when they're needed. A skill is not some arbitrary Markdown lying around that gets read automatically. A skill is a directory under .claude/skills/ with a SKILL.md, and that file carries a mandatory name and description in its YAML front matter:
.claude/skills/
├── nestjs-conventions/
│ └── SKILL.md
├── migration-playbook/
│ └── SKILL.md
└── code-review-checklist/
└── SKILL.md---
name: nestjs-conventions
description: Conventions for NestJS modules — when to load and what for
---
## Module structure
domain/ · application/ · infrastructure/ · presentation/
## Rules
- Business validation never uses class-validator.
- The repository pattern is mandatory.
- Lifecycle hooks belong only in the application layer.The decisive difference: Claude reads the description and loads the skill only when it fits the task at hand — the NestJS conventions for a NestJS module, the review checklist for a pull-request review. The rest stays out. This is lazy loading for project knowledge: less noise, fewer tokens, and a far smaller chance that rules from unrelated parts of the project get mixed up.
Key point:
@imports structure permanent context; skills load it on demand. A smallCLAUDE.mdplus a curated set of skills beats any monster file — regardless of how clever your prompts are.
The same pattern works globally, by the way: a lean ~/.claude/CLAUDE.md for your personal, cross-project ground rules, with project-specific knowledge staying in each repo.
Block 2 — Safety and safe automation
The second block is where most setups are either too timid (constant approval prompts) or too reckless (everything allowed). Both can be tuned precisely — once you know how the mechanisms actually work.
settings.json: get permissions right
Claude spends a lot of time on harmless things: reading files, checking Git history, running tests, searching the project. None of it is destructive, yet by default the agent still asks. The fix is an allow list for risk-free read and verification commands, while everything that changes something stays behind manual approval:
{
"permissions": {
"allow": [
"Bash(git status:*)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(npm test:*)",
"Bash(npm run lint:*)",
"Bash(npm run typecheck:*)"
],
"deny": [
"Bash(git push:*)",
"Bash(git reset --hard:*)",
"Bash(rm -rf:*)"
]
}
}The effect is immediate: instead of twenty harmless approvals per session you only sign off on what can actually change something. It feels far more like pair programming than remote control.
It's important to understand how these rules match — this is where many well-meaning configurations fall apart. The pattern inside Bash(...) is applied as a prefix glob against the actual command string Claude wants to run. So Bash(git diff:*) matches any command starting with git diff. What does not follow from this: rules like Bash(drop table:*) or Bash(delete from ...) are useless. SQL virtually never runs as a bare shell command; it's embedded — e.g. psql -c "drop table users". The real command string then starts with psql, not drop table, and the rule matches nothing. If you want to catch dangerous SQL, you need a hook that inspects the command's contents — which brings us to the next point.
Hooks — the correct way
Hooks are small shell programs Claude Code runs before or after a tool call. They're one of the most powerful and, at the same time, most frequently miswired features. The essentials you have to know:
- A hook receives its data as JSON on
stdin— not as a positional argument. The command to be run sits in.tool_input.command. - A
PreToolUsehook blocks with exit code2. Whatever the hook writes tostderrreaches Claude as the reason. Exit0means "no objection" (the normal approval flow continues); any other code counts as a hook error and does not block.
So a hook that reads the command from $1 and tries to block with exit 1 simply does nothing. Here's a guard that actually bites:
#!/usr/bin/env bash
# PreToolUse hook: refuse obviously destructive shell commands.
# Claude Code passes the tool call as JSON on stdin.
input=$(cat)
command=$(printf '%s' "$input" | jq -r '.tool_input.command // empty')
dangerous='rm -rf /|mkfs|dd if=.*of=/dev/|DROP TABLE|TRUNCATE|DELETE FROM.*WHERE 1=1'
if printf '%s' "$command" | grep -qiE "$dangerous"; then
echo "Blocked: refusing to run a destructive command." >&2
exit 2 # exit code 2 tells Claude Code to block the call
fi
exit 0You register it in settings.json. Here the matcher filters on the tool name (Bash), not on a command:
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/guard.sh"
}]
}]
}
}Instead of using exit codes, a hook can also finish with 0 and return a JSON object that makes the decision explicit (permissionDecision: "deny" with a reason) — finer-grained, but for the simple blocking case exit 2 is entirely enough.
Two more things for context: the matcher is a tool-name filter only for PreToolUse/PostToolUse. For Stop or Notification hooks it matches on other fields (the trigger, say), so an empty matcher on a Stop hook does not mean "every tool." And a Notification hook that pings you via notify-send or osascript when Claude is done saves you from constantly staring at the terminal — trivial, but in practice the removal of that context switch is worth a surprising amount.
acceptEdits deliberately — and understanding the permission modes
acceptEdits removes the confirmation before every file change and makes the agent feel noticeably faster. The first time, you wonder why it isn't always on. The answer: because "fast" is deceptive here.
The mode is exactly right for tasks that are mechanical and automatically verifiable — migrating JavaScript to TypeScript, replacing any types, renaming files, updating imports, formatting, generating tests for existing functionality, editing docs. Here the test suite reliably catches a mistake.
For anything that requires judgment, manual approval stays on: new features, architectural refactoring, authentication, payment flows, the database layer — in short, any task whose "done" can't be checked by a machine in seconds. The reason isn't distrust of the model but a simple experience: an agent already editing the files will happily "help" by refactoring services on the side that nobody wanted touched. Technically not broken, often even plausible — but no better than the architecture you deliberately chose. What should have been five minutes of cleanup turns into thirty minutes of review.
The rule behind it: enable acceptEdits only where tests reliably detect a mistake. If no test can tell you something went wrong, the extra click of manual approval is well spent.
acceptEdits is only one of several permission modes. It pays to know the whole ladder:
| Mode | Behavior |
|---|---|
default | Asks before changing actions; reading runs through |
acceptEdits | Auto-approves file edits and common filesystem commands |
plan | Read-only; Claude explores and proposes a plan without changing anything |
auto | A classifier checks each action against a blocklist (research preview) |
dontAsk | Auto-denies prompts; only pre-approved tools run |
bypassPermissions | Skips all checks — only defensible inside a container/VM |
Two additions for everyday use: the built-in bash sandbox (/sandbox) fences shell commands via OS primitives and reduces prompts without switching off safety checks. And if you want fewer prompts without blindly waving everything through, the auto mode with its classifier is the more honest route than bypassPermissions. The latter (--dangerously-skip-permissions) belongs strictly inside a real isolation boundary — more on that shortly.
Treat environments differently — the real mechanism
Your local development machine isn't production. It's natural to grant different environments different rights — generous locally, strict near production. Except this does not work via an environment variable pointing at a profile file (there is no such thing), and the command is simply claude, not claude code. The real mechanism is the settings hierarchy, and it's more capable:
~/.claude/settings.json— your user defaults across all projects.claude/settings.json— project settings, committed, for the whole team.claude/settings.local.json— local override, git-ignored, per machine
These layers are merged automatically (later ones override earlier ones). The shared, safe baseline thus lives in the committed settings.json, and each machine adds its local freedoms in settings.local.json (running docker compose down or npm run db:reset without a prompt, say) — with no alias acrobatics. If you deliberately want to pull a specific file for a single run, there's the --settings flag:
claude --settings .claude/settings.ci.jsonFor teams and organizations, a managed policy sits right at the top: /etc/claude-code/managed-settings.json (Linux) is read with the highest priority and overrides everything below it — ideal for anchoring central guardrails nobody can override locally. And if you want to move the entire configuration directory, set CLAUDE_CONFIG_DIR to the new path (that controls the directory, not a single file).
In practice: permission rules govern what may run and whether it asks first. But they are not a wall: a command that gets through can reach whatever the process can reach. So if you want to give the agent real autonomy — keyword
bypassPermissions— pair the permissions with isolation (container, VM, sandbox runtime). How to do that cleanly is covered in the in-depth piece Claude Code in a Container.
Block 3 — How you work with the agent
Configuration is one half. The other is how you deal with the agent while it runs — this is where you save time without any setting at all.
Recognize and abort context rot
Every long session eventually reaches a point where quality tips over. The symptoms are remarkably consistent: the agent proposes ideas you rejected long ago; it wants to change code it wrote itself five minutes earlier; it asks for context that has been chewed over several times. That's context rot — the context has become so full and contradictory that fresh signals get lost.
Pushing on doesn't help; it only gets more expensive. The fastest fix is almost always a clean restart. Claude Code gives you the tools for it:
/contextshows how full the context window actually is — often the trigger to notice./compactsummarizes the conversation and frees up room without losing the thread entirely./clearstarts completely fresh.
Instead of laboriously keeping a rotting session alive, write a short handoff when you restart. It costs less than five minutes and is more reliable than copying the whole transcript:
## Context
### Goal
One sentence on the objective.
### Done
- File A: complete
- File B: complete
### Already rejected
- Approach X
- Approach Y
### Next step
One concrete task.My personal threshold: if three consecutive messages fail to move the project forward, I restart without hesitation.
The two-corrections rule
Closely related and at least as valuable: if you've corrected Claude twice on the same issue in one conversation, stop. Not because the model is stubborn, but because the conversation has become inefficient — each further clarification only brings marginal improvement, and you're going in circles.
In almost all cases one of three causes is to blame, and none of them is solved by the third correction:
- The task is too large. Break it up. Claude solves three small, clearly bounded problems more reliably than one nested big one.
- The expected result is unclear. Don't describe it — show it. A concrete example beats any abstract instruction.
- The starting point is too thin. Instead of "build a service," sketch the interface, define the boundaries, or lay out a rough scaffold. Claude is dramatically more reliable at filling in an existing structure than inventing one from scratch.
The smartest prompt is sometimes not another correction but a fresh start with better starting material.
Match effort to the task
Not every task deserves the same depth of thought. A typo needs no five-minute planning phase; a database migration very much does. Here too a setting makes the rounds that doesn't exist: you'll search in vain for a /effort slash command. What's real is several levers that work together:
- The
--effortflag at startup controls reasoning depth across the levelslow,medium,high,xhigh,max. - Plan mode (cycled with Shift+Tab or via
--permission-mode plan) forces Claude to analyze first and present a plan before changing anything. - Thinking keywords in the prompt ("think hard," "ultrathink") deepen the reasoning at specific points.
- And the model choice (
/model) is the coarsest, often most effective lever of all.
A rough mapping:
| Effort | For what |
|---|---|
| Low — implement immediately, no planning | Utility scripts, tiny bugfixes, throwaway tooling |
| Medium — brief plan, then code | Everyday features, routine refactoring |
| High — detailed planning up front | New components, public APIs, larger architectural changes |
| Maximum — plan mode, approval, then implementation | Auth, core infrastructure, schema migrations, security, production-critical work |
A nice side effect: just deciding whether a task deserves the maximum level clarifies its risk. If max feels excessive, the task is smaller than you thought — if it feels right, you're wise to slow down.
Block 4 — More perspectives: models and subagents
The final block flips the perspective. Instead of asking one model for the "best" solution, you let several tackle the same problem from different angles — especially valuable for decisions that are expensive to reverse: application architecture, module boundaries, database design, service contracts, event-driven systems, large refactors.
Models as a review panel
The trick is a shift in mindset, not a feature. You ask the same question deliberately with a different focus — and, where it makes sense, with different models from the current lineup (Opus 4.8, Sonnet 5, Haiku 4.5, switched via /model):
Design this system with reliability and explicit contracts as the top priority.
Design the same system with development velocity and maintainability as the priority.
Solve the same problem with as little complexity and abstraction as possible.The results rarely coincide. You usually get a conservative architecture, one optimized for speed, and a surprisingly elegant, minimalist variant. You're not looking for consensus but for trade-offs. Finally, in another session, you have the strongest ideas from all proposals synthesized under your project's constraints. If you work alone or don't always have a second experienced head available for design reviews, this comes surprisingly close to a real architectural sounding board.
Subagents — the clean route there
The manual model panel is useful but clumsy. Claude Code has a dedicated feature for exactly these patterns that many people overlook: subagents. A subagent is a Markdown file under .claude/agents/ that takes on a bounded task with isolated context and, optionally, its own model:
---
name: architecture-critic
description: Independent reviewer for architecture proposals
model: opus
---
You are a skeptical senior architect. Assess the proposed design
uncompromisingly along trade-offs: coupling, testability, operational
cost, reversibility. Name the weakest assumption first.The gain is twofold. First, a subagent runs in its own context — it doesn't burden your main session with its mass of intermediate steps, actively working against context rot. Second, you can start several in parallel, each with a different focus or model, and get genuinely independent perspectives instead of one opinion colored by the same context. The review panel from above thus turns from manual copy-paste into a repeatable, cleanly separated workflow — and is at the same time the better way to handle limited context.
Conclusion
None of these settings is complicated. Together, though, they transform Claude Code from a helpful autocomplete into something close to a reliable teammate: a lean CLAUDE.md and modular skills instead of a monster file, clear safety boundaries via correctly wired hooks and the real settings hierarchy, a deliberate way of working against context rot, and more perspectives through models and subagents.
The bigger lesson underneath: the quality of your AI assistant depends not on the model alone but at least as much on the system you build around it. Invest in that system, and the gains compound with every session. If you want to take the safety part seriously and give the agent real autonomy, you'll find the matching isolation layer in Claude Code in a Container.