Amakuru.net

A lean Claude Code setup that fits in a dotfiles repo

Version-controlled Claude Code configuration tuned for serious daily use: per-project plugin loading to keep the context window cheap, sandbox + deny-list guardrails that don't rely on the model behaving, hooks for deterministic safety, subagent model pinning, and a SessionStart nudge that suggests per-project tuning when there isn't any.

I started using Claude Code seriously a few months ago, and like every tool I lean on every day, it eventually accreted enough configuration that I wanted it under version control. The result is dotfiles-claude. Two design tensions kept coming back as the setup grew: between “Claude can do anything” and “Claude can’t break anything”, and underneath that, between “the model has all the tools it could need” and “the prompt isn’t 50,000 tokens before the user has typed anything.”

The constraint that drives almost every choice below: context costs whether or not it’s used. Loaded tool schemas, idle MCP servers, dormant skills — they all show up in every turn’s prompt, in latency, in cost, and in the model’s tendency to wander. The cheapest token is the one that was never sent.

Lean global config, tuned per project

The default mode for tools like this is to load every plugin, every skill, every MCP server globally. This works for a session or two, then bloats every prompt and slows every response. A typical “load everything” setup can spend twenty to forty thousand tokens on system instructions alone before the user types — every turn, every day.

Loading per project flips it. The global default stays tiny: about eleven universally-useful plugins (basic shell, git, simple file editing). Stack-specific tooling — Terraform reviewers, dbt linters, Glue debuggers, AWS MCP servers — is re-enabled per repo by an auto-configure-project skill that detects the stack on first session start and drafts a .claude/settings.json. A Python-only repo doesn’t load Terraform tooling; a frontend-only repo doesn’t load the AWS MCP server.

The measurable effect was the global default going from ~30k tokens of system context per turn to under 8k, with response latency dropping correspondingly.

Deferred MCP tools

Even within a project’s enabled tooling, not every tool needs to be loaded into the prompt. Claude Code supports deferred tools: tools whose existence is announced but whose JSON schema is only loaded when the model asks for it. The schemas for things like WebSearch, WebFetch, the various Google Workspace MCP tools, Playwright — they aren’t free; each tool definition can be a few hundred to a couple of thousand tokens. Deferring them means a session that never needs browser_click doesn’t pay for browser_click’s schema.

The trade-off is one extra round-trip when a deferred tool is called for the first time. In practice that round-trip is invisible against the cost of a tool I almost certainly wasn’t going to use.

A safety net at the harness layer

Sandbox and deny list block destructive shell commands and reads of .env, .ssh/, AWS credentials, Terraform state, and a long list of other “you almost certainly didn’t mean to read this” paths. The point of putting it at the harness layer rather than relying on the model to remember is that a deny list fails only when its configuration changes; a habit fails on the day you’re tired.

The deny-list approach (allow everything except a denylist) is what makes auto mode usable for long iterative tasks. An allowlist tends to grow into a denylist with extra steps, or to get bypassed because the friction outweighs the theoretical safety. The denylist is a sharp, short list of “not this, ever”.

Explicit confirmation prompts cover the shared-state actions: pushing to remotes, creating PRs, sending Slack messages, running migrations. Local edits are reversible by the next edit; pushed commits and sent messages are not. The prompts are configured at the harness level, so they fire even if the model is certain the action is fine.

Hooks: deterministic guardrails the model can’t disable

Hooks are shell commands the harness runs at specific lifecycle points (PreToolUse, PostToolUse, SessionStart, etc.). They can block tool calls or augment them with context. The model doesn’t see them happen and can’t opt out, which is what makes them the right place for non-negotiables.

The three I get the most out of:

  • Format on edit. A PostToolUse:Edit hook runs ruff format / terraform fmt / prettier on the touched file. The model never has to remember formatting, and the working tree never carries an “I forgot to format” diff.
  • Read tracking. A PostToolUse:Read hook injects a marker so the transcript later shows which files the model actually read in the session — useful for de-duplicating exploration.
  • SessionStart memory load. Every session start, a hook loads a short summary of prior decisions, conventions, and gotchas for the project, when one exists. A few hundred tokens of curated memory tends to be worth several thousand of re-discovery.

Hooks are also where team policy belongs. A rule like “never commit without running tests” is more reliable as a PreToolUse hook on git commit (exit non-zero unless the test suite has passed recently) than as a CLAUDE.md instruction the model might trade off against urgency.

Suggestions over enforcement

A SessionStart hook surfaces a banner suggestion when I enter a project that has no .claude/settings.json. It doesn’t do anything — it reminds me (or the model) to run /auto-configure-project. Hard enforcement would mean every fresh clone gets blocked or auto-configured, which is the kind of paternalism that gets safety features turned off wholesale. A soft nudge keeps the option to clone, look around, and leave without ceremony.

The hard rails are kept for non-negotiables (no rm -rf /, no commits to main without confirmation). Everything else is a nudge.

Subagents: which model to spend on what

Subagents (Claude Code’s name for delegated sub-tasks) default to a fast, small model — Haiku, currently. That default is correct for the agents I use most: Explore for codebase searches, fast-explorer for “what does this directory do,” any bounded research lookup. These need throughput and cheap reads, not deep reasoning.

It’s the wrong default for code review and architectural design. A reviewer at Haiku quality misses subtle bugs and proposes plausible-but-wrong refactors confidently; an Opus-quality reviewer catches things I missed and pushes back on my framing. The setup pins reviewer and architect agents — code-reviewer, terraform-reviewer, python-reviewer, feature-dev:code-architect, electron-security-reviewer, type-design-analyzer — to Opus explicitly.

Haiku is roughly an order of magnitude cheaper per token than Opus, so defaulting to Haiku and pinning the few agents that need Opus is materially cheaper than running everything on Opus, with no quality loss on the agents that do the deep reasoning.

Memory across sessions

Claude Code can persist a memory file per project — short notes the model writes itself, hand-edited by me when needed. The setup encourages a specific shape:

  • User memories: who you are, how you work, what tone you want.
  • Feedback memories: rules learned from corrections (“don’t add error handling for things that can’t happen”), with the why next to the rule so future-me can judge edge cases.
  • Project memories: context about ongoing work that isn’t in the code (deadlines, why a refactor is happening, who blocked it).
  • Reference memories: pointers to where information lives (Linear project IDs, dashboard URLs, internal wikis).

A few hundred tokens of accurate memory tend to be worth several thousand of re-derivation, but only while the memory file stays short — past a couple of hundred lines, the value per token drops fast and the model starts treating it as background noise. Memory works better as a curated index than as a journal.

What it actually saves, in numbers

Rough order-of-magnitude, on my workload (a few hours of Claude Code per day across ~10 projects):

  • System context: ~30k → ~8k tokens per turn (per-project loading + deferred MCP tools).
  • First response latency: noticeably faster — most turns feel instant rather than waiting a beat.
  • Subagent costs: Opus-by-default would have been ~10× the Haiku-default bill. Pinning the few agents that need Opus claws back the quality without the cost.
  • Prompt-injected memory: under 500 tokens for most projects, replacing 2–3k of “let me re-read the README” exploration on every session.

Was it worth it?

Keeping the whole setup as a dotfiles repo — ~/.claude symlinked to a git checkout — means every config edit is immediately tracked. The cost of trying a new pattern is one commit and the cost of reverting it is one revert, and that low ceremony is what made the configuration evolve at all. When a session feels weirdly slow or sloppy, git log is what tells me what changed.

The repo is public and MIT-licensed, but it’s my setup, with my Lyon-mountain snobbery and Amsterdam-data-engineer biases baked in. The README has a “Fork and adapt” section listing the files to edit before relying on it.

dmorel69/dotfiles-claude — MIT