Best Practices & Patterns

The Claude Code codebase is a rich source of production-tested patterns for building large TypeScript applications. This page distills the most interesting and reusable architectural decisions found throughout the source.

Pattern 1: Feature Flags via Dead Code Elimination

Claude Code's most distinctive pattern is its use of Bun's feature() macro for dead-code elimination at build time. Rather than runtime feature flags that add overhead and complexity, this approach compiles out entire subsystems for builds where they're disabled:

// Before: runtime conditional (loads all code, checks at runtime)
import * as reactiveCompact from './services/compact/reactiveCompact.js'
if (process.env.FEATURE_REACTIVE_COMPACT) {
  reactiveCompact.apply(...)
}

// After: build-time DCE with Bun (unused branch is never included)
const reactiveCompact = feature('REACTIVE_COMPACT')
  ? (require('./services/compact/reactiveCompact.js')
      as typeof import('./services/compact/reactiveCompact.js'))
  : null

// Usage - null-safe pattern
if (reactiveCompact) {
  reactiveCompact.apply(...)
}

The advantages of this pattern are significant:

  • Zero runtime overhead, the feature check is evaluated once at build time, not on every code path.
  • Smaller bundle, disabled features don't contribute any bytes to the output.
  • No module evaluation, a disabled feature's imports are never executed, so its dependencies don't run.
  • Type safety, the typeof import() in the ternary's true branch provides full type checking for the conditional module.

This pattern requires that feature strings be string literals, Bun cannot evaluate computed strings at build time. The convention in this codebase is ALL_CAPS identifiers (e.g., REACTIVE_COMPACT, COORDINATOR_MODE, KAIROS).

Pattern 2: React Hooks for CLI State

Using Ink (React for terminals) enables a surprising insight: all of React's hook patterns apply perfectly to CLI state management. The hooks/ directory has 30+ custom hooks, each encapsulating a piece of interactive CLI behavior:

// hooks/useCanUseTool.tsx - encapsulates the permission prompt lifecycle
export function useCanUseTool(
  permissionContext: ToolPermissionContext,
  setAppState: SetAppStateFn,
): CanUseToolFn {
  const pendingPermissions = useRef(new Map<string, PermissionResolver>())

  // Returns a stable function that can be called from async tool code
  return useCallback(async (tool, input, context, message, toolUseID) => {
    const result = await evaluatePermission(tool, input, permissionContext)
    if (result.behavior === 'ask') {
      // Mount the permission dialog via state update
      return new Promise(resolve => {
        pendingPermissions.current.set(toolUseID, resolve)
        setAppState(prev => ({
          ...prev,
          pendingPermissionRequest: { toolUseID, tool, input, message }
        }))
      })
    }
    return result
  }, [permissionContext, setAppState])
}

// hooks/useCancelRequest.ts - wraps AbortController lifecycle
export function useCancelRequest() {
  const abortControllerRef = useRef(createAbortController())
  const cancel = useCallback(() => {
    abortControllerRef.current.abort()
    abortControllerRef.current = createAbortController()
  }, [])
  return { signal: abortControllerRef.current.signal, cancel }
}

This approach means complex interactive behaviors, permission dialogs, progress spinners, interrupt handling, autocomplete, are each isolated in their own hook and composed together in the top-level screen components. Testing hooks independently is much simpler than testing a monolithic interactive loop.

Pattern 3: The Tool Abstraction

The Tool<Input, Output, Progress> interface is a masterclass in "everything a tool needs to be" captured in one place. It bundles:

  • Input validation (Zod schema)
  • Business logic (call())
  • Permission checking (checkPermissions())
  • System prompt contribution (prompt())
  • UI rendering (renderToolUseMessage(), renderToolResultMessage())
  • Serialization for the API (mapToolResultToToolResultBlockParam())
  • Security classification (toAutoClassifierInput())
  • Behavior metadata (isReadOnly, isConcurrencySafe, isDestructive)

Contrast this with a naive approach of having separate registries for each concern:

// Naive approach - separate registries (BAD)
const toolSchemas = { 'FileEdit': fileEditSchema, ... }
const toolHandlers = { 'FileEdit': handleFileEdit, ... }
const toolRenderers = { 'FileEdit': renderFileEdit, ... }
const toolPermissions = { 'FileEdit': checkFileEditPermissions, ... }

// Problem: easy for registries to get out of sync, hard to discover
// what all properties a tool should have, no type safety across concerns

// Claude Code approach - single object (GOOD)
export const FileEditTool = buildTool({
  name: 'FileEdit',
  inputSchema: z.object({ ... }),
  async call(args, ctx) { ... },
  async checkPermissions(args, ctx) { ... },
  async prompt() { ... },
  renderToolUseMessage(args) { ... },
  mapToolResultToToolResultBlockParam(result, id) { ... },
})

Pattern 4: Immutable State + Updater Pattern

Rather than using Redux (too heavy) or Zustand (good but external), Claude Code implements its own minimal state management pattern that is surprisingly simple:

// state/AppStateStore.ts - the pattern
type Updater<S> = (prev: S) => S

class AppStateStore {
  private state: AppState
  private listeners = new Set<(s: AppState) => void>()

  // Immutable update - always creates new state
  set(updater: Updater<AppState>) {
    this.state = updater(this.state)
    this.listeners.forEach(l => l(this.state))
  }

  get() { return this.state }

  subscribe(listener: (s: AppState) => void) {
    this.listeners.add(listener)
    return () => this.listeners.delete(listener)
  }
}

// Usage in components
function MyComponent({ setAppState }) {
  // Always spread prev to preserve other state
  const updateMode = (mode: PermissionMode) => {
    setAppState(prev => ({ ...prev, permissionMode: mode }))
  }
}

The key insight is that setAppState is an updater function rather than a direct setter, it receives the previous state and returns the new state. This prevents race conditions in concurrent tool execution and makes the state transitions auditable (each update is a pure function).

Pattern 5: Lazy Requires for Circular Dependencies

Large TypeScript codebases often develop circular imports. Claude Code handles this with a deliberate pattern: lazy require() calls at usage time instead of top-level import:

// main.tsx - lazy require to avoid circular dependency
// Problem: teammate.ts → AppState.tsx → ... → main.tsx
// Solution: don't import at module level, import when needed

const getTeammateUtils = () =>
  require('./utils/teammate.js') as typeof import('./utils/teammate.js')

// Usage:
const { createTeammate } = getTeammateUtils()

// Another example from QueryEngine.ts:
// MessageSelector.tsx pulls React/ink - only needed at query time
const messageSelector =
  (): typeof import('src/components/MessageSelector.js') =>
    require('src/components/MessageSelector.js')

The pattern uses a zero-arg factory function that returns the require() result. The as typeof import() cast provides full TypeScript types while keeping the import lazy. The module is cached by Node's/Bun's module system after the first call, so there is no repeated evaluation overhead.

The codebase has consistent ESLint annotations: // eslint-disable @typescript-eslint/no-require-imports before lazy require blocks and re-enable after, making it easy to audit all lazy imports with a simple grep.

Pattern 6: Context-Window-Aware Message Management

Building applications on top of LLMs requires treating the context window as a scarce resource. Claude Code's approach is instructive:

// The "token warning state" calculation (autoCompact.ts)
type TokenWarningState =
  | 'none'          // < 80% of context window used
  | 'warning'       // 80-95% used - show warning to user
  | 'critical'      // > 95% used - trigger auto-compact

// The auto-compact check after each turn:
const warningState = calculateTokenWarningState(usage, maxTokens)
if (warningState === 'critical' && isAutoCompactEnabled()) {
  await triggerAutoCompact(messages, context)
}

// Message normalization before API calls:
// - Strip UI-only messages (never sent to API)
// - Apply compact boundary (only post-boundary messages sent)
// - Count tokens before sending to catch prompt-too-long errors
// - Apply per-tool result size limits

// The "tool result storage" pattern:
// Tool results > maxResultSizeChars are persisted to disk
// Claude receives: "Result saved to /tmp/cc_result_abc123.txt (45,230 chars)"
// This keeps the context window manageable for large outputs

Pattern 7: Progress Callbacks for Long Operations

Tools that take a long time (subagent runs, large file operations) use a progress callback pattern to stream updates to the UI without blocking:

// Tool.ts - progress callback type
type ToolCallProgress<P extends ToolProgressData> = (
  progress: ToolProgress<P>,
) => void

// Example: AgentTool emits progress events as subagent streams
export const AgentTool = buildTool({
  async call(args, context, canUseTool, parentMessage, onProgress) {
    for await (const event of subagent.run(args.prompt)) {
      if (event.type === 'text') {
        onProgress?.({
          toolUseID: args.toolUseId,
          data: { type: 'agent_progress', text: event.text }
        })
      }
    }
    return { data: finalResult }
  },

  renderToolUseProgressMessage(progressMessages, { verbose }) {
    // Render accumulated progress messages as a live feed
    return <AgentProgressDisplay messages={progressMessages} />
  },
})

The progress callback pattern decouples the tool's async logic from the UI rendering. The tool doesn't need to know anything about Ink or React, it just calls onProgress() with typed data, and the renderToolUseProgressMessage() method handles the presentation.

Pattern 8: Aggressive Startup Prefetching

Claude Code prefetches everything that might be needed in the first few seconds, even before the user types anything. The pattern is consistent: fire I/O operations in parallel as early as possible, await their results only when needed.

// main.tsx - startup sequence (comments condensed)
profileCheckpoint('main_tsx_entry')   // 1. Mark entry

startMdmRawRead()         // 2. Fire MDM subprocess reads (parallel)
startKeychainPrefetch()   // 3. Fire keychain reads (parallel)

// ... ~135ms of imports evaluate ...

async function main() {
  // 4. Fire more parallel prefetches
  const [
    bootstrapData,      // GrowthBook flags, subscription status
    policyLimits,       // Enterprise policy limits (MDM)
    remoteManagedSettings,
  ] = await Promise.all([
    fetchBootstrapData(),
    loadPolicyLimits(),
    loadRemoteManagedSettings(),
  ])

  // 5. Start MCP connections in parallel
  // (don't await - let them complete while user reads first prompt)
  prefetchOfficialMcpUrls()
  prefetchPassesEligibility()

  // 6. By the time user presses Enter, keychain reads are done
  await ensureKeychainPrefetchCompleted()
}

The startup profiler (utils/startupProfiler.ts) measures the time between checkpoints and logs it in debug mode. This helps identify regressions, if a new import adds 50ms to startup, the profiler catches it.

Pattern 9: Exhaustive Discriminated Unions

The codebase makes heavy use of TypeScript discriminated unions for all message types, permission results, and event types. This enables exhaustive switch statements that the TypeScript compiler verifies:

// types/message.ts
type Message =
  | { type: 'user';      role: 'user'; content: ContentBlockParam[] }
  | { type: 'assistant'; role: 'assistant'; content: ContentBlock[] }
  | { type: 'system';    subtype: SystemSubtype; ... }
  | { type: 'progress';  data: ToolProgressData; ... }
  | { type: 'tombstone'; originalId: string }

// Exhaustive handling at every boundary:
function processMessage(msg: Message) {
  switch (msg.type) {
    case 'user':      return handleUser(msg)
    case 'assistant': return handleAssistant(msg)
    case 'system':    return handleSystem(msg)
    case 'progress':  return handleProgress(msg)
    case 'tombstone': return null
    // TypeScript error if a new case is added and not handled here
  }
}

// normalizeMessagesForAPI uses type predicates to strip UI-only messages:
function isAPIMessage(msg: Message): msg is UserMessage | AssistantMessage {
  return msg.type === 'user' || msg.type === 'assistant'
}

Pattern 10: Schema-First Design with Zod

Every tool input is defined as a Zod schema first, and TypeScript types are derived from it via z.infer<>. This means the runtime validation, TypeScript types, and JSON Schema for the API are all derived from a single source of truth:

// One schema, three uses:
const BashInputSchema = z.object({
  command: z.string().describe('The bash command to execute'),
  timeout: z.number().optional().describe('Timeout in milliseconds'),
  restart: z.boolean().optional().describe('Restart the shell session'),
})

// 1. TypeScript type (zero runtime cost)
type BashInput = z.infer<typeof BashInputSchema>

// 2. Runtime validation in tool dispatch
const parsed = BashInputSchema.safeParse(rawInput)
if (!parsed.success) {
  return errorResult(parsed.error.message)
}

// 3. JSON Schema for the Anthropic API (auto-generated by Zod)
const jsonSchema = zodToJsonSchema(BashInputSchema)

Zod v4 (used here) is significantly faster than v3 for schema evaluation, important when validating tool inputs on every single tool call in a long session.

Frequently Asked Questions

What is the recommended way to edit files?
It is highly recommended to use the targeted FileEditTool rather than relying on standard sed commands or asking the agent to rewrite entire files. This prevents context exhaustion and mistakes.
How should I write a CLAUDE.md file?
Provide brief, high-signal project overview, conventions, and constraints. Do not put overly generic coding advice inside it; treat it as the DNA of your repository.
Should I keep the agent task running indefinitely?
No, use `/compact` or start fresh sessions regularly to reset context overhead, keep inference fast, and dramatically reduce API block costs.
How can I debug agent loops?
Pay attention to the tool executions and use the verbose mode if available to see exactly what prompts are being serialized to the runtime API.
What is the best way to utilize skills?
Encode heavily repeated multi-step tasks into explicit skills rather than typing out the same verbose manual instructions every single session.