Claude Code: Part 1 - The Foundation

Claude Code: Part 1 - The Foundation

“I am rarely happier than when spending an entire day programming my computer to perform automatically a task that it would otherwise take me a good ten seconds to do by hand.” — Douglas Adams

This is Part 1 of a three-part series on Claude Code configuration. Part 2 covers enforcement hooks, and Part 3 explores real-world workflow patterns.


I’ve been using Claude Code for months now, and I’ve developed opinions. Not the polite kind you share at dinner parties. The kind that make you write wrapper scripts at 2 AM and yell at YAML files.

The thing about Claude Code: it’s genuinely powerful. Like, “I built a sophisticated agentic AI system with tiered memory, deployed on AWS Lambda, with a Discord interface (44,000 lines of code; 3,000 tests; 100% code coverage1) in about 20 hours of part-time work” powerful. But power without discipline is how you end up with a codebase that looks like it was written by a very enthusiastic Furby with commit access.

The thesis of this series is nuanced: out-of-the-box, Claude Code can do great things. It’ll help you build simple programs with remarkable ease. But as code complexity grows, Claude Code requires deliberate configuration to not drown you in AI-generated mediocrity. Left to its defaults, any LLM will happily generate reams of plausible-looking code that subtly misses the point. On a small app, you might notice the problem and fix it. On complex software, those little niggles compound. You’ll miss at least some of them (that’s what “subtle” means), and the whole pile accumulates into an unmaintainable mess—if it ever works in the first place. The solution isn’t to use Claude Code less. It’s to use it better.

The Smart Wrapper: Your First Line of Defense

I never run claude directly; instead, every invocation goes through a wrapper script that acts like a good bouncer at an exclusive club: it checks your ID, verifies you’re on the list, and makes sure you’re not going to cause trouble inside.

Here’s ~/.claude/claude-smart.sh in its entirety:

#!/usr/bin/env zsh
# Smart Claude wrapper with project detection and mixin support
# Loads plugin directories and their .mcp.json configs
#
# Mixins can be specified via:
#   1. CLAUDE_MIXINS env var (comma or space separated): CLAUDE_MIXINS="web,mobile"
#   2. .claude-mixins file in project root (one per line, # for comments)
#
# Priority: env var > marker file
# Priority for mixin plugins: ./.claude/ > ~/.claude/
# Auto-detected: typescript (via tsconfig.json or package.json)

set -euo pipefail

# Build up a set of plugin directories to load
PLUGIN_DIRS=()

# Detect TypeScript project
if [[ -f "tsconfig.json" ]] || \
   { [[ -f "package.json" ]] && command -v jq &>/dev/null && \
     jq -e '.devDependencies.typescript // .dependencies.typescript' package.json &>/dev/null 2>&1; }; then

    [[ -d ~/.claude/plugins/typescript ]] && PLUGIN_DIRS+=(~/.claude/plugins/typescript)
fi

# Detect and apply mixins (env var takes priority over marker file)
MIXINS=()
if [[ -n "${CLAUDE_MIXINS:-}" ]]; then
    # Parse from environment variable (comma or space separated)
    IFS=', ' read -rA MIXINS <<< "$CLAUDE_MIXINS"
elif [[ -f ".claude-mixins" ]]; then
    # Parse from marker file (one per line, skip comments and blank lines)
    while IFS= read -r line || [[ -n "$line" ]]; do
        # Strip leading/trailing whitespace and skip comments/blank lines
        line="${line%%\#*}"  # Remove comments
        line="${line## }"     # Remove leading spaces
        line="${line%% }"     # Remove trailing spaces
        [[ -n "$line" ]] && MIXINS+=("$line")
    done < .claude-mixins
fi

# Add mixin plugin directories if they exist (local .claude/plugins takes priority over ~/.claude/plugins)
for mixin in "${MIXINS[@]}"; do
    if [[ -d "./.claude/plugins/${mixin}" ]]; then
        PLUGIN_DIRS+=("./.claude/plugins/${mixin}")
    elif [[ -d ~/.claude/plugins/${mixin} ]]; then
        PLUGIN_DIRS+=(~/.claude/plugins/${mixin})
    fi
done

# Prepare flags array
CLAUDE_FLAGS=()

# Add plugin directories
for dir in "${PLUGIN_DIRS[@]}"; do
    [[ -d "$dir" ]] && CLAUDE_FLAGS+=(--plugin-dir "$dir")
done

# Execute via bunx (always fetch latest)
# Don't use exec so trap cleanup can run
bunx --smol --bun @anthropic-ai/claude-code@latest "${CLAUDE_FLAGS[@]}" "$@"

Why a Wrapper Script?

You might be thinking: “Can’t I just configure plugins in .claude/settings.json?” You can. But there are two problems with that approach.

Problem 1: JSON configuration is a maze. Want to add a Hugo plugin to your blog project? With raw settings, you’re navigating multiple layers of confusion. First, the JSON syntax itself—hoping you didn’t miss a comma, remembering the exact structure. Then: does this go in .claude/settings.json or .claude/settings.local.json? And what exactly is the plugin path? The location of plugin.json? The top-level plugin directory? Just the plugin name? The marketplace name and the plugin name? Who knows! And which parts of the .claude directory do you commit and push to your repo?

With the wrapper, you just create a .claude-mixins file and stick it in git:

# Project-specific mixins
hugo
serverless

That’s it. One mixin per line, comments supported, no JSON required. The wrapper translates this into --plugin-dir flags, pulling from your project’s .claude/ directory first, then falling back to ~/.claude/ for cross-project reuse.

Problem 2: Repetitive configuration across similar projects. Every TypeScript project needs TypeScript-specific behavior. Without the wrapper, you’d copy-paste plugin configuration into every TypeScript project’s settings. With auto-detection, you configure your TypeScript plugin once globally, and every project with a tsconfig.json automatically gets it. Zero per-project configuration for the common case.

The script sniffs for tsconfig.json (or TypeScript in your package.json) and automatically loads my TypeScript plugin—specialized agents, quality standards, the whole stack. I don’t do anything special in a TypeScript project to get TypeScript-appropriate behavior. It just works. You could easily look for other clues for other project types/languages to pull in “standard” plugin sets for those - found a go.mod ==> load the go plugin; found a Gemfile ==> load the rails plugin, etc, etc.

Bonus: Always fetch latest. That bunx @anthropic-ai/claude-code@latest line means you’re never stuck on a stale version. Claude Code updates frequently, and you want those improvements.

The Orchestrator-Only Philosophy

Here’s where things get philosophical—or at least as philosophical as configuration files get.

My global ~/.claude/CLAUDE.md contains what I call the Orchestrator Mandate:

## CRITICAL: ORCHESTRATOR-ONLY MODE

**You are STRICTLY an orchestrator. NEVER do implementation work yourself.**

### Core Mandate
- **NEVER** write code, edit files, or implement solutions directly
- **NEVER** make commits, run builds, or execute implementation commands
- **ALWAYS** delegate ALL work to specialized sub-agents[^3] via the Task tool
- Your ONLY job is to: understand requests, plan, delegate, validate, and report

### Why This Matters
- Preserves your context window for orchestration decisions
- Each sub-agent gets fresh context for thorough work
- Enables longer, more complex task completion
- Prevents context exhaustion on multi-step projects

This might seem counterintuitive. You have this incredibly capable AI, and you’re… telling it not to do things?

Yes. Exactly.

Think about the hero image for this post—a conductor’s podium. The conductor doesn’t play every instrument. They don’t rush from the violin section to the timpani to the oboe. Their power comes from making other people powerful, from shaping the overall performance rather than getting lost in individual notes.

Claude Code’s context window is precious real estate. Every line of code it writes, every file it reads, every command it executes—it all eats into that context. When you ask Claude to both orchestrate and implement, you’re asking the conductor to also play first violin. It can technically do it, but something gets lost.

The orchestrator pattern doesn’t reduce total token usage—it redistributes tokens strategically. Each sub-agent starts fresh, preventing context pollution from earlier unrelated tasks.

By forcing strict orchestration, you get:

  1. Fresh context for each sub-task. The sub-agent implementing your database migration doesn’t carry the baggage of the three previous tasks. It starts clean.

  2. Better separation of concerns. Planning and doing are different cognitive modes. Mixing them leads to “well, I’m already here, might as well…” syndrome.

  3. Longer working sessions. I can run complex, multi-step projects without the orchestrator running out of context halfway through.

The OODA Loop for AI Orchestration

My CLAUDE.md also includes an OODA-inspired framework for handling complex tasks:

### 1. OBSERVE
- Analyze existing codebase structure
- Review tests, documentation, and logs
- Identify affected components
- Check monitoring/metrics if available

### 2. ORIENT
- Map requirements to architecture layers
- Identify relevant patterns and conventions
- Assess technical constraints
- Consider security/performance implications

### 3. DECIDE
- Break down into atomic tasks
- Identify dependencies (parallel vs sequential)
- Assign to appropriate sub-agents
- Define acceptance criteria per task
- Set quality gates

### 4. ACT
- Delegate to specialized agents
- Monitor progress via tmux/outputs
- Validate against acceptance criteria
- Iterate based on feedback

If you recognize OODA loops from military strategy or Boyd’s work on decision-making, congratulations on your eclectic reading habits. The core insight transfers surprisingly well: effective action requires rapid cycling through observation, orientation, decision, and action—not getting stuck in any one phase.

For Claude Code, this means the orchestrator reads a lot, thinks carefully, delegates precisely, and validates obsessively. It does not mean diving straight into implementation and hoping for the best.

The Mixin System Architecture

Let’s talk about how this all composes. Configuration in Claude Code flows through multiple layers:

LayerSourceWhat It Provides
Base~/.claude/Global CLAUDE.md, settings.json, shared plugins
Project./.claude/Project-specific CLAUDE.md, settings, local plugins
Mixins.claude-mixins filePlugin names to load (one per line)
OverrideCLAUDE_MIXINS env varReplaces .claude-mixins if set - handy for one-time/temporary additions on the CLI

Each layer extends the previous. Project settings can’t remove global safety rails or delete instructions applicable to all projects, but can add project-specific behavior. The env var exists for one-off testing without editing files—for example, CLAUDE_MIXINS=experimental claude to test a new plugin before committing it to your .claude-mixins file. Note that mixins add to whatever plugins are already configured in your settings.json files.

You can still use claude plugins install or manually configure plugins in ~/.claude/settings.json or ./.claude/settings.json. But once you’ve built up a palette of “standard” plugins you use across projects, .claude-mixins is just easier—one line per plugin, no JSON, commits cleanly to git.

Got a Hugo site? Add hugo to your mixins. Working with Serverless Framework? Add serverless. Each mixin loads its own plugin directory with specialized prompts, MCP configurations, and tool permissions.

Local plugins (in ./.claude/plugins/) take priority over global ones (in ~/.claude/plugins/). This means you can have a global typescript plugin with sensible defaults, but override it with project-specific TypeScript configuration when needed.

Global Settings: The Safety Net

The other half of global configuration lives in ~/.claude/settings.json:

{
  "permissions": {
    "allow": [
      "WebSearch",
      "WebFetch",
      "Bash(gh run list:*)",
      "Bash(gh run view:*)",
      "Bash(jq:*)"
    ],
    "deny": [],
    "ask": [],
    "defaultMode": "plan"
  },
  "model": "opus",
  "hooks": {
    "Notification": [],
    "PreToolUse": []
  },
  "statusLine": {
    "type": "command",
    "command": "bunx -y ccstatusline@latest"
  },
  "alwaysThinkingEnabled": true,
  "sandbox": {
    "enabled": true,
    "autoAllowBashIfSandboxed": true
  }
}

Several things happening here:

The most important thing is at the end: sandbox mode. By default, all my projects run sandboxed with permission to run any Bash command that stays inside the sandbox. This massively reduces approval prompts. In a sandbox, Claude Code can run for hours on complex tasks—reading files, editing code, running scripts—all safely contained. If something blows up, it blows up inside the sandbox where it can’t do real damage.

Permissions are pre-authorizations for escaping the sandbox. These are operations that reach outside the sandbox without requiring approval. Since they’re in ~/.claude/settings.json, they apply to all my projects by default. So every project can access the web, check GitHub Actions status, and run jq for JSON processing—no approval prompts needed. I periodically review my project-specific authorizations and promote common ones here if they make sense across the board.

Default mode is plan. When Claude starts a session, it’s in read-only mode by default. It can look around, understand the codebase, and make recommendations—but it can’t make changes until you explicitly unlock that capability. Think before acting - very OODA.

Model selection is deliberate. For orchestration specifically, I need a model that excels at decomposition and planning—and that’s where Opus is just a fucking beast. On complex reasoning benchmarks, Opus 4.5 scores nearly 3× higher than Sonnet 4.5 (37.6% vs 13.6% on ARC-AGI-2). More importantly, Opus plans better and uses 48-76% fewer tokens on hard problems because it traverses shorter solution paths2. For an orchestrator that needs to break down complex tasks and coordinate multiple agents, that planning ability is worth the extra cost. Sub-agents can use Sonnet where raw speed matters more than strategic thinking. With the Claude Max plan, I have enough Opus tokens to use on the orchestration with sub-agents mostly using Sonnet or Haiku.

Hooks are powerful. The Notification and PreToolUse hooks are where enforcement happens—but that’s a topic for Part 2 . You can get a push notification on your phone when Claude needs your attention, and you can run validation scripts before any tool executes to auto-allow or prevent things based on complex criteria.

Status line integration. That ccstatusline command provides real-time feedback in your terminal about what Claude is doing. It’s a small thing, but watching the status update as Claude works through a complex task is surprisingly reassuring—at a glance you can see context window usage, uncommitted changes, and Max plan block timers3.

Configuration as Investment

All this configuration takes time. You could just run claude directly in your project and start prompting. It would work! For simple tasks, it would probably work fine.

But here’s the paradox of AI tooling: more powerful AI requires more disciplined configuration. While Claude Code massively accelerates the rate of code production, the flip side is that I tend to spend about 1/4-1/3 of my time in Claude Code improving my Claude Code tooling! Writing custom agents4, skills5, MCP servers6, better hooks, refining my global and project CLAUDE.md files, etc. The investment is well worth it. Putting in this extra time and effort increases overall long-term velocity—with bad tooling, at scale Claude Code breaks down, and when it does so, it breaks down hard.

A weak tool can’t do much damage. A powerful tool can refactor your entire codebase in ways that are plausible but wrong. The more capable the AI, the more important it is to constrain and guide that capability. The more complex the project, the more likely it’ll do something horrific and it will take a while for you to notice, by which time the damage will have spread.

I think of configuration as investment. An hour spent on wrapper scripts and CLAUDE.md files pays dividends across every session. The conductor’s podium took time to build, but it enables performances that would be impossible without it.


Coming Up: The Enforcers

In Part 2 , we’ll look at the enforcement layer—the hooks and validators that keep AI-generated code honest. Topics include:

  • PreToolUse hooks that reject bad habits before they happen
  • Notification hooks for mobile alerts when Claude needs attention
  • Custom plugins: Codex, TypeScript, and tmux
  • The LSP situation (constructive criticism ahead)
  • Quality gates that prevent shipping substandard work

The configuration we’ve built here is the foundation. The enforcers are what make it robust.

Part 2: “Claude Code: The Enforcers” — coming soon.


  1. Not just line coverage—100% mutation score. Every line of code has been proven necessary by Stryker mutation testing , which introduces artificial bugs and verifies tests catch them. ↩︎

  2. Benchmark data from Anthropic’s Opus 4.5 announcement and independent comparisons ↩︎

  3. Max plan blocks are rate-limited thinking sessions. When you hit the limit, you have to wait for a cooldown before Claude can do more work without incurring extra API charges. The status line shows when that timer resets. ↩︎

  4. Agents are specialized sub-processes that handle specific types of work. You define them with a prompt and optional model override, then the orchestrator delegates tasks to them via the Task tool. See the Claude Code agents documentation ↩︎

  5. Skills are reusable prompt templates that Claude can invoke when needed—lighter weight than agents, useful for common patterns. See the Claude Code skills documentation ↩︎

  6. MCP (Model Context Protocol) servers extend Claude Code’s capabilities by providing additional tools—things like database access, API integrations, or custom automation. See the MCP documentation ↩︎