Back to writings

Building Production-Ready Claude Code Agents: A Complete Implementation Guide

13 min read

The gap between a demo agent and a production agent isn't capability—it's architecture.

I've built dozens of Claude Code agents. Most of them worked great in my terminal. Then I tried to scale them. Token costs exploded. Error handling was nonexistent. Context windows collapsed mid-session. The agent that looked brilliant at 2am suddenly looked naive in production.

This guide covers what actually works when you build AI agents with Claude Code for real environments. Not theory. Not hype. The patterns that have kept agents running reliably for months.

Understanding Claude Code as an Agent Platform

Claude Code is an agentic tool where developers work with Claude directly from their terminal—delegating tasks from code migrations to bug fixes. But it's more than a coding assistant. When you build Claude Code agents, you're building autonomous systems that need authentication, error recovery, cost management, and observability.

The first thing to understand: Claude Code agents aren't just about code generation. You can interact with a computer using natural language to describe objectives and outcomes rather than implementation details. Provide Claude (the CLI) an input such as a spreadsheet, a codebase, a link to a webpage and then ask it to achieve an objective. It then makes a plan, verifies details, and executes it.

This is exactly what makes production deployment tricky. You're not just managing code—you're managing autonomous decision-making systems that need guardrails.

Authentication and Credential Management

Before your agent does anything, it needs to authenticate.

On macOS, API keys, OAuth tokens, and other credentials are stored in the encrypted macOS Keychain. Supported authentication types include Claude.ai credentials, Claude API credentials, Azure Auth, Bedrock Auth, and Vertex Auth.

For production Claude Code agents, I implement a three-tier authentication strategy:

  1. Environment-based API keys - Store your ANTHROPIC_API_KEY in a secure vault (AWS Secrets Manager, HashiCorp Vault, or similar). Never commit credentials to git.

  2. Custom credential helpers - The apiKeyHelper setting can be configured to run a shell script that returns an API key. By default, apiKeyHelper is called after 5 minutes or on HTTP 401 response. Set CLAUDE_CODE_API_KEY_HELPER_TTL_MS environment variable for custom refresh intervals.

  3. Token refresh logic - Build a refresh mechanism that rotates credentials before expiry, preventing mid-execution failures.

Here's what I use in production:

// credential-manager.ts
import { exec } from "child_process";
import { promisify } from "util";

const execAsync = promisify(exec);

class CredentialManager {
  private lastRefresh: number = 0;
  private refreshInterval: number = 5 * 60 * 1000; // 5 minutes

  async getApiKey(): Promise<string> {
    const now = Date.now();
    if (now - this.lastRefresh > this.refreshInterval) {
      return this.refreshCredential();
    }
    return process.env.ANTHROPIC_API_KEY || "";
  }

  private async refreshCredential(): Promise<string> {
    try {
      const { stdout } = await execAsync(
        "aws secretsmanager get-secret-value --secret-id claude-api-key --query SecretString --output text"
      );
      this.lastRefresh = Date.now();
      return stdout.trim();
    } catch (error) {
      console.error("Credential refresh failed:", error);
      throw new Error("Failed to refresh API credentials");
    }
  }
}

export const credentialManager = new CredentialManager();

Token Optimization: The Real Cost Driver

Token costs are where most production Claude Code deployments go wrong. I've seen agents that work fine in testing suddenly cost $500/month in production because no one optimized token usage.

Claude Code uses prompt caching by default to reduce costs and latency. Master Claude Code pricing and token optimization to reduce costs by 70%. Learn proven strategies to maximize value from your API or subscription.

Here's my token optimization framework:

1. Model Selection Strategy

Start every session with Sonnet. Only switch to Opus when you need deep analysis or complex refactoring.

The cost difference is dramatic. For a typical feature:

  1. Exploration (Haiku): 8k tokens = $0.006
  2. Planning (Sonnet): 12k tokens = $0.036
  3. Implementation (Sonnet): 85k tokens = $0.255
  4. Testing (Sonnet): 18k tokens = $0.054
  5. Review (Opus): 12k tokens = $0.180
  6. Total per feature: ~$0.59

That's sustainable. Without optimization, the same feature costs 10x more.

2. Context Window Management

Use the /compact slash command to summarize and compress your conversation history. Compaction is now instant - Claude maintains a continuous session memory in the background, so compaction just loads that summary into a fresh context.

I implement this as an automated check:

# Monitor token usage in your agent loop
check_token_usage() {
  local usage=$(claude --status | grep "tokens" | awk '{print $2}')
  if [ "$usage" -gt 80 ]; then
    echo "Token usage at ${usage}%. Running compaction..."
    claude /compact
  fi
}

# Run before expensive operations
check_token_usage
claude "Implement the payment processing module"

3. MCP Server Configuration

Each enabled MCP server adds tool definitions to your system prompt, consuming part of your context window. Use /context to identify MCP server context consumption, then disable servers not needed for your current task with @server-name disable or /mcp (v2.0.10+). This is especially valuable when approaching context limits.

In your .claude/CLAUDE.md:

## MCP Configuration

disabledMcpServers:
  - github    # Only enable when doing git operations
  - railway   # Only for deployment tasks
  - memory    # Disable if not needed

## File Access Rules

allowedDirectories:
  - src/
  - tests/
  - config/

forbiddenDirectories:
  - node_modules/
  - .git/
  - dist/
  - coverage/

This prevents Claude from reading unnecessary files and burning tokens.

Error Handling Architecture

Production agents fail. APIs timeout. Rate limits hit. Networks drop. Your agent needs to handle all of it gracefully.

Errors will happen in production. APIs fail, rate limits hit, networks timeout. Your workflows need to handle failures elegantly without crashing or losing data.

I implement a four-layer error handling system:

1. Input Validation Layer

// validation.ts
import { z } from "zod";

const agentInputSchema = z.object({
  task: z.string().min(10).max(500),
  codebase: z.string().optional(),
  constraints: z.array(z.string()).optional(),
  maxTokens: z.number().min(1000).max(100000).default(50000),
});

async function validateAndExecute(input: unknown) {
  try {
    const validated = agentInputSchema.parse(input);
    return await executeAgent(validated);
  } catch (error) {
    if (error instanceof z.ZodError) {
      return {
        status: "validation_failed",
        errors: error.errors,
        timestamp: new Date().toISOString(),
      };
    }
    throw error;
  }
}

2. Execution Wrapper with Retries

// agent-executor.ts
interface RetryConfig {
  maxRetries: number;
  baseDelay: number;
  maxDelay: number;
  backoffMultiplier: number;
}

async function executeWithRetry<T>(
  fn: () => Promise<T>,
  config: RetryConfig
): Promise<T> {
  let lastError: Error | null = null;

  for (let attempt = 0; attempt < config.maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;

      // Don't retry on validation errors
      if (lastError.message.includes("validation")) {
        throw lastError;
      }

      // Calculate exponential backoff
      const delay = Math.min(
        config.baseDelay * Math.pow(config.backoffMultiplier, attempt),
        config.maxDelay
      );

      console.log(
        `Attempt ${attempt + 1} failed. Retrying in ${delay}ms...`
      );
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }

  throw new Error(
    `Failed after ${config.maxRetries} attempts: ${lastError?.message}`
  );
}

3. Circuit Breaker Pattern

// circuit-breaker.ts
enum CircuitState {
  CLOSED = "closed",
  OPEN = "open",
  HALF_OPEN = "half_open",
}

class CircuitBreaker {
  private state: CircuitState = CircuitState.CLOSED;
  private failureCount: number = 0;
  private lastFailureTime: number = 0;
  private readonly threshold: number = 5;
  private readonly timeout: number = 60000; // 1 minute

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === CircuitState.OPEN) {
      if (Date.now() - this.lastFailureTime > this.timeout) {
        this.state = CircuitState.HALF_OPEN;
      } else {
        throw new Error("Circuit breaker is OPEN");
      }
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  private onSuccess() {
    this.failureCount = 0;
    this.state = CircuitState.CLOSED;
  }

  private onFailure() {
    this.failureCount++;
    this.lastFailureTime = Date.now();
    if (this.failureCount >= this.threshold) {
      this.state = CircuitState.OPEN;
    }
  }
}

4. Graceful Degradation

// fallback-strategy.ts
async function executeWithFallback(
  primaryTask: () => Promise<string>,
  fallbackTask: () => Promise<string>
): Promise<{ result: string; source: "primary" | "fallback" }> {
  try {
    const result = await executeWithRetry(primaryTask, {
      maxRetries: 3,
      baseDelay: 1000,
      maxDelay: 10000,
      backoffMultiplier: 2,
    });
    return { result, source: "primary" };
  } catch (primaryError) {
    console.warn("Primary task failed, attempting fallback:", primaryError);

    try {
      const result = await fallbackTask();
      return { result, source: "fallback" };
    } catch (fallbackError) {
      return {
        result: "Unable to complete task. Please try again later.",
        source: "fallback",
      };
    }
  }
}

Real-World Deployment Patterns

I've learned these patterns the hard way. They work.

Pattern 1: Agent Loop with State Management

Running multiple Claude agents allows for specialization. While a few agents are tasked to solve the actual problem at hand, other specialized agents can be invoked to maintain documentation, keep an eye on code quality, or solve specialized sub-tasks.

// agent-loop.ts
interface AgentTask {
  id: string;
  type: "implementation" | "testing" | "review" | "documentation";
  description: string;
  status: "pending" | "in_progress" | "completed" | "failed";
  result?: string;
  error?: string;
}

class AgentOrchestrator {
  private tasks: Map<string, AgentTask> = new Map();
  private completedTasks: Set<string> = new Set();

  async runAgentLoop(initialTask: string) {
    let currentTask = this.createTask("implementation", initialTask);

    while (!this.isComplete(currentTask)) {
      try {
        console.log(`Executing: ${currentTask.description}`);
        currentTask.status = "in_progress";

        const result = await this.executeAgent(currentTask);
        currentTask.status = "completed";
        currentTask.result = result;

        // Determine next task based on result
        const nextTaskType = this.determineNextTask(currentTask);
        if (nextTaskType) {
          currentTask = this.createTask(
            nextTaskType,
            `Follow-up: ${currentTask.description}`
          );
        }
      } catch (error) {
        currentTask.status = "failed";
        currentTask.error = String(error);
        break;
      }
    }

    return this.generateReport();
  }

  private determineNextTask(
    completed: AgentTask
  ): AgentTask["type"] | null {
    const sequence: AgentTask["type"][] = [
      "implementation",
      "testing",
      "review",
      "documentation",
    ];
    const currentIndex = sequence.indexOf(completed.type);
    return currentIndex < sequence.length - 1
      ? sequence[currentIndex + 1]
      : null;
  }

  private async executeAgent(task: AgentTask): Promise<string> {
    // Execute Claude Code agent for this specific task
    const prompt = this.buildTaskPrompt(task);
    return await claudeCodeAgent.execute(prompt);
  }

  private isComplete(task: AgentTask): boolean {
    return task.status === "completed" || task.status === "failed";
  }

  private createTask(
    type: AgentTask["type"],
    description: string
  ): AgentTask {
    return {
      id: `${type}-${Date.now()}`,
      type,
      description,
      status: "pending",
    };
  }

  private generateReport() {
    return Array.from(this.tasks.values());
  }
}

Pattern 2: Observability and Monitoring

Observability is no longer a "nice-to-have" but a critical capability. When an agent fails or produces an unexpected output, observability tools provide the traces needed to pinpoint the source of the error. This is especially important in complex agents that might involve multiple LLM calls, tool interactions, and conditional logic. AI agents often rely on LLMs and other external APIs that are billed per token or per call. Observability allows for precise tracking of these calls, helping to identify operations that are excessively slow or expensive.

// observability.ts
interface AgentTrace {
  traceId: string;
  timestamp: Date;
  agentType: string;
  prompt: string;
  response: string;
  tokensUsed: {
    input: number;
    output: number;
  };
  cost: number;
  duration: number;
  status: "success" | "failure";
  error?: string;
}

class AgentObserver {
  private traces: AgentTrace[] = [];

  async traceExecution<T>(
    agentType: string,
    prompt: string,
    fn: () => Promise<T>
  ): Promise<T> {
    const traceId = this.generateTraceId();
    const startTime = Date.now();

    try {
      const result = await fn();

      // Record successful trace
      this.recordTrace({
        traceId,
        timestamp: new Date(),
        agentType,
        prompt,
        response: String(result),
        tokensUsed: { input: 0, output: 0 }, // Get from API response
        cost: 0,
        duration: Date.now() - startTime,
        status: "success",
      });

      return result;
    } catch (error) {
      // Record failed trace
      this.recordTrace({
        traceId,
        timestamp: new Date(),
        agentType,
        prompt,
        response: "",
        tokensUsed: { input: 0, output: 0 },
        cost: 0,
        duration: Date.now() - startTime,
        status: "failure",
        error: String(error),
      });

      throw error;
    }
  }

  private recordTrace(trace: AgentTrace) {
    this.traces.push(trace);

    // Send to observability platform (Langfuse, Datadog, etc.)
    this.sendToObservabilityPlatform(trace);
  }

  private async sendToObservabilityPlatform(trace: AgentTrace) {
    // Implementation depends on your observability platform
    // Example: Langfuse, Azure AI Foundry, or custom logging
  }

  private generateTraceId(): string {
    return `trace-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }

  getCostMetrics() {
    const totalCost = this.traces.reduce((sum, t) => sum + t.cost, 0);
    const avgDuration =
      this.traces.reduce((sum, t) => sum + t.duration, 0) / this.traces.length;
    const successRate =
      (this.traces.filter((t) => t.status === "success").length /
        this.traces.length) *
      100;

    return { totalCost, avgDuration, successRate };
  }
}

Pattern 3: Structured Deployment

Deploy Claude Code agents like you'd deploy any critical system:

# deployment.yaml
apiVersion: v1
kind: Service
metadata:
  name: claude-code-agent
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0

  env:
    - name: ANTHROPIC_API_KEY
      valueFrom:
        secretKeyRef:
          name: claude-credentials
          key: api-key
    - name: CLAUDE_MODEL
      value: "claude-opus-4-6"
    - name: MAX_TOKENS
      value: "50000"
    - name: ENABLE_OBSERVABILITY
      value: "true"

  resources:
    requests:
      memory: "512Mi"
      cpu: "250m"
    limits:
      memory: "2Gi"
      cpu: "1000m"

  livenessProbe:
    httpGet:
      path: /health
      port: 8080
    initialDelaySeconds: 30
    periodSeconds: 10

Common Pitfalls and How to Avoid Them

Pitfall 1: Ignoring Context Window Limits

Context window constraints represent a fundamental limitation of transformer-based language models. The context window defines the maximum amount of information a model can process simultaneously, typically measured in tokens. Production AI agents handling extended conversations or processing large documents encounter situations where relevant information exceeds available context capacity. When conversation history exceeds the context window, agents must discard information to accommodate new inputs. Naive truncation strategies remove the earliest conversation turns, potentially eliminating critical context established during initial interactions.

Solution: Implement proactive context management with tiered documentation.

Pitfall 2: Underestimating Failure Rates

Current research reveals that agents succeed approximately 50% of the time, highlighting significant room for improvement in agent capabilities through better planning and robust error recovery.

Solution: Design for 3-15% error rates. Build fallback strategies, not perfect agents.

Pitfall 3: Missing Security Considerations

Organizations are deploying agents with access to databases, payment systems, and customer data. Recent attack vectors demonstrate the evolving threat landscape: ChatGPT's Deep Research vulnerability, Google Gemini's memory system, and database deletion incidents.

Solution: Implement least-privilege access. Use row-level security in databases. Audit all agent actions.

Connecting to Related Resources

For deeper context on building reliable agents, check out Building Reliable AI Tools which covers structured output patterns that prevent hallucinations. You'll also want to review Claude vs GPT-4 for Production Agents to understand model selection for your specific use case.

Real-world deployment stories are covered in Building Production AI Agents: Lessons from the Trenches, and for scaling to enterprise environments, see Enterprise AI Integration Patterns: Lessons from Real-World Anthropic Claude Deployments.

The Path Forward

Building production-ready Claude Code agents isn't about using the latest model or the fanciest framework. It's about:

  1. Authenticating securely - Protect your credentials like they're production database passwords
  2. Optimizing tokens relentlessly - Your costs depend on it
  3. Handling errors gracefully - Assume everything fails
  4. Observing everything - You can't optimize what you can't measure
  5. Testing realistically - Demo success and production success are different things

The organizations shipping reliable Claude Code agents aren't the ones betting on perfect AI. They're the ones building solid engineering practices around imperfect agents.

Start with one small agent. Get it reliable. Add observability. Optimize costs. Then scale. This approach compounds. In six months, you'll have systems that handle thousands of tasks reliably.

The gap between demo and production is real. But it's predictable. And it's manageable.

Ready to build production-ready Claude Code agents? Get in touch and let's talk about your specific use case.