Código con IA que escala: Claude Code Hooks y buenas prácticas para proyectos mantenibles

Patrón para evitar complejidad accidental en proyectos con IA: hooks automáticos, orquestación de agentes y arquitectura simple.

Contributors: Ivan Garcia

From Prompter to Architect: How to Guide AI Toward Scalable Solutions

I’ve seen developers generate a complete feature with AI in 15 minutes. It works perfectly. Three months later, nobody wants to touch that module because it has become a tangled mess of nested conditionals and cross-dependencies. This is the dilemma of speed without direction: AI optimizes for solving the current prompt, but your job is to optimize for the long-term project.

In this post I explain how to configure Claude Code Hooks, orchestrate multiple agents, and shift your mindset from “prompter” to systems architect. It’s not about limiting AI, but about guiding it toward solutions that scale.

Why Does AI Generate Accidental Complexity?

The difference between “easy” and “simple” explains the problem. An agent can generate an endpoint that handles 5 business cases with a 200-line monolithic block. It’s easy: it works immediately. But it’s not simple: every new requirement forces you to modify the same block.

// Easy but not simple: an endpoint that does everything
app.post('/api/process', async (req, res) => {
  if (req.body.type === 'payment') {
    if (req.body.method === 'card') {
      // 50 lines of card logic
    } else if (req.body.method === 'transfer') {
      // 40 lines of transfer logic
      if (req.body.currency === 'USD') {
        // USD-specific validations
      }
      // more nested conditionals...
    }
  } else if (req.body.type === 'refund') {
    // another 80-line block
  }
  // 5 more cases...
});

The simple version uses a strategy pattern:

// Simple: each case is an independent module
const processors = {
  payment: {
    card: new CardProcessor(),
    transfer: new TransferProcessor()
  },
  refund: new RefundProcessor()
};

app.post('/api/process', async (req, res) => {
  const processor = processors[req.body.type]?.[req.body.method]
    || processors[req.body.type];
  return processor.handle(req.body);
});

The second approach requires more upfront design, but each new case is an independent module. It’s not a flaw of AI — it’s simply a different role.

How to Shift from “Prompter” to Architect

Spec-Driven Development: Design Before Code

The most common mistake is opening the terminal and directly asking “implement a notification system”. Without context or constraints, the agent improvises a solution that works but doesn’t fit your architecture.

Create a design document before writing code:

# spec.md - Notification System

## File Structure
- src/notifications/
  - core/NotificationService.ts (main interface)
  - channels/EmailChannel.ts, PushChannel.ts
  - templates/TemplateEngine.ts
  - queue/NotificationQueue.ts

## Data Flow
User → NotificationService → TemplateEngine → Channel → Queue → Delivery

## Allowed Patterns
- Strategy for channels (email, push, SMS)
- Factory for templates
- Observer for delivery events

## Prohibited Patterns
- No Singleton for NotificationService
- No template logic inside channels
- No direct dependencies between channels

With this spec, the prompt changes from “implement notifications” to “Following the spec in spec.md, implement the EmailChannel module respecting the Channel interface and using the TemplateEngine to render content”.

CLAUDE.md: Your Project’s Constitution

Claude Code automatically reads the CLAUDE.md file at the project root and adapts its proposals accordingly. It works as the “constitution” of your codebase:

# CLAUDE.md

## Tech Stack
- Node.js + TypeScript
- Express + Zod for validations
- Prisma + PostgreSQL
- Jest for testing

## Code Conventions
- Functions: 20 lines maximum
- Maximum 3 levels of nesting
- Absolute imports from `@/`
- One concept per file

## Architectural Rules
- Clear separation between controllers, services, and models
- No business logic in controllers
- Services with no side effects in constructors
- Models with no external dependencies

## Allowed Dependencies
- For validation: Zod only
- For HTTP: Axios only
- For dates: date-fns only

With this file, when you ask “add validation to this endpoint”, the agent uses Zod automatically. When you ask “implement authentication”, it respects the layered separation.

What Are Claude Code Hooks and Why Do You Need Them?

Hooks are automated quality controls that execute at deterministic points in the cycle. They don’t rely on AI “deciding” to do things right — they are inflexible rules.

They are configured in .claude/settings.json or using the /hooks command. There are three types:

PreToolUse Hooks: The Input Filter

Executes before Claude uses a tool (writing a file, running a command). If the hook returns exit code ≠ 0, the action is canceled.

{
  "hooks": {
    "preToolUse": {
      "command": "./scripts/pre-action-check.sh",
      "description": "Validates permissions before writing files"
    }
  }
}
#!/bin/bash
# scripts/pre-action-check.sh

# Prevent modifications outside /src/features/
if [[ "$CLAUDE_TOOL_NAME" == "Edit" || "$CLAUDE_TOOL_NAME" == "Write" ]]; then
  FILE_PATH="$CLAUDE_TOOL_ARGS_FILE_PATH"
  if [[ ! "$FILE_PATH" == *"/src/features/"* ]]; then
    echo "❌ Access denied. Only changes in /src/features/ are allowed."
    exit 1
  fi
fi

# Block direct commits to main
if [[ "$CLAUDE_TOOL_NAME" == "Bash" && "$CLAUDE_TOOL_ARGS_COMMAND" == *"git commit"* ]]; then
  BRANCH=$(git branch --show-current)
  if [[ "$BRANCH" == "main" ]]; then
    echo "❌ Direct commits to main are not allowed. Create a feature branch."
    exit 1
  fi
fi

PostToolUse Hooks: Automatic Verification

Executes after each file modification. Turns every edit into a mini local CI cycle:

{
  "hooks": {
    "postToolUse": {
      "command": "./scripts/post-edit-validation.sh",
      "description": "Automatic lint + tests after each change"
    }
  }
}
#!/bin/bash
# scripts/post-edit-validation.sh

if [[ "$CLAUDE_TOOL_NAME" == "Edit" || "$CLAUDE_TOOL_NAME" == "Write" ]]; then
  FILE_PATH="$CLAUDE_TOOL_ARGS_FILE_PATH"

  # Automatic lint
  if [[ "$FILE_PATH" == *.ts || "$FILE_PATH" == *.js ]]; then
    echo "🔍 Running ESLint on $FILE_PATH..."
    npx eslint "$FILE_PATH" --fix
    if [ $? -ne 0 ]; then
      echo "❌ Lint errors detected. Claude must fix them."
      exit 1
    fi
  fi

  # Tests for the affected module
  TEST_FILE="${FILE_PATH%.*}.test.ts"
  if [ -f "$TEST_FILE" ]; then
    echo "🧪 Running tests from $TEST_FILE..."
    npx jest "$TEST_FILE"
    if [ $? -ne 0 ]; then
      echo "❌ Tests failing. Claude must fix them before continuing."
      exit 1
    fi
  fi
fi

Prompt-Based Hooks: The Lightweight Reviewer

Instead of a terminal script, the hook is a prompt evaluated by a fast model (Claude Haiku). Ideal for semantic reviews that a linter can’t perform:

{
  "hooks": {
    "postToolUse": {
      "prompt": "Does this function exceed 30 lines or have more than 3 levels of nesting? Respond JSON: {\"pass\": true/false, \"reason\": \"...\"}",
      "model": "claude-haiku",
      "description": "Check cyclomatic complexity"
    }
  }
}

When Do You Need Agent Orchestration?

When a project grows, a single agent isn’t enough. Orchestration allows you to divide tasks among specialized agents while keeping context scoped.

Pattern: Planner Agent + Executors

ComponentResponsibilityContext
Planner agentReceives complete task, generates plan, decomposes into subtasksEntire project
Executor agentsImplement each specific subtaskOnly relevant files
Final verifierEvaluates coherence between modulesPlan + results
// Example with Inngest for orchestration
export const implementFeature = inngest.createFunction(
  { id: "implement-feature" },
  { event: "feature.requested" },
  async ({ event, step }) => {

    // 1. Planner agent
    const plan = await step.run("generate-plan", async () => {
      return plannerAgent.decompose(event.data.requirements);
    });

    // 2. Executors in parallel
    const implementations = await step.run("parallel-implementation", async () => {
      return Promise.all(
        plan.tasks.map(task =>
          executorAgent.implement(task, { scope: task.files })
        )
      );
    });

    // 3. Final verification
    const verification = await step.run("verify-integration", async () => {
      return reviewerAgent.checkIntegration(plan, implementations);
    });

    if (!verification.passed) {
      await step.sendEvent("implementation.retry", {
        feedback: verification.issues
      });
    }
  }
);

Pattern: Code Review Between Agents

After one agent generates code, another agent evaluates it with a strict reviewer prompt:

const reviewerPrompt = `
You are a senior code reviewer. Evaluate this code:

CRITERIA:
- Does it respect the patterns in the CLAUDE.md file?
- Maximum 20 lines per function?
- Descriptive names without abbreviations?
- Sufficient tests for edge cases?
- Clear documentation for public APIs?

Respond JSON: {
  "approved": boolean,
  "issues": [{"line": number, "severity": "error|warning", "message": string}],
  "suggestions": [string]
}
`;

const reviewResult = await reviewerAgent.evaluate(generatedCode, reviewerPrompt);
if (!reviewResult.approved) {
  return originalAgent.fix(generatedCode, reviewResult.issues);
}

How to Limit Each Agent’s Scope

Never give an agent access to the entire project. Scope it to the relevant files and modules:

❌ Bad✅ Good
”Implement authentication in the application""Implement AuthService in src/auth/ using the interfaces in src/types/auth.ts”
Full access to the src/ directoryOnly src/auth/ and related type files
50 files in context5–8 specific files

This reduces errors, prevents unwanted changes, and makes it easier to audit results.

Common Mistakes

The Hook That Blocks Everything

Configuring hooks so strict that Claude can’t work. If the PreToolUse hook rejects 90% of actions, the problem is the configuration, not the agent.

Symptom: Claude gets stuck in loops trying to perform the same action over and over.

Solution: Start with permissive hooks and gradually tighten them.

Agents with Excessive Context

Giving an agent access to 100 files “just in case”. The result: unexpected changes in unrelated modules.

Symptom: Asking “fix this bug” and the agent modifies configuration files or tests from other modules.

Solution: Minimum viable context. Add files only when strictly necessary.

Hooks That Duplicate Work

Configuring a PostToolUse hook that runs full tests after every minor change.

Symptom: Each edit takes 2–3 minutes because of the tests.

Solution: Fast checks in hooks (lint, unit tests for the file). Full tests in CI.

Vague Prompts with Specialized Agents

Using executor agents as if they were generalists. “Implement the payments feature” to an agent that should only create interfaces.

Symptom: Agents stepping outside their assigned responsibility.

Solution: Specific prompts that reinforce the role: “As an interface agent, define only the TypeScript types for the payments module, without implementation”.

Implementation Checklist

  • CLAUDE.md file created with stack, conventions, and architectural rules
  • At least one hook configured (PreToolUse for permissions or PostToolUse for lint)
  • Design documents (spec.md) before implementing complex features
  • Agent context limited to relevant files (maximum 10–15 files)
  • Automatic code review process between agents (for large projects)
  • Hook scripts tested on a small project first
  • Quality metrics tracking (test coverage, cyclomatic complexity)

Frequently Asked Questions

Which hooks should I configure first?

Start with a PostToolUse hook that runs automatic lint after each change. It provides the most value with the least friction. Once it’s working well, add a PreToolUse hook to block changes outside allowed directories.

Do hooks slow down development significantly?

A lint hook takes 1–3 seconds. A unit test hook for the modified file takes 5–15 seconds. That’s much faster than fixing bugs in production or cleaning up legacy code later. For large projects, configure hooks that run only related tests, not the full suite.

How many agents should I use for a feature?

For small features (1–3 files), a single agent is enough. For medium features (4–10 files), use a planner + executor. For large features (10+ files), add a verifier agent that checks coherence between modules.

What if the verification hook rejects all changes?

It means the rules are too strict or the CLAUDE.md is missing context. Review the hook’s error messages and adjust progressively. It’s better to start with permissive hooks and tighten them than to block everything from the start.

Does Claude Code Hooks work with editors other than VS Code?

Hooks are terminal scripts that run independently of the editor. They work with any tool that uses Claude Code: terminal, Cursor, editors with Anthropic extensions, or custom integrations via API.