Cap 24: Claude Code en CI/CD y Tool Design
Claude Code en CI/CD y Tool Design
Este tutorial cubre dos dominios del examen Claude Certified Architect – Foundations:
- Domain 3: Claude Code en pipelines CI/CD
- Domain 2: Diseño de herramientas (Tool Design)
1. Modo No-Interactivo
Claude Code fue diseñado para correr en terminales interactivas, pero en CI/CD necesitamos que opere de forma autónoma.
Flag -p (—print)
El flag -p activa el modo no-interactivo: Claude ejecuta el prompt y termina. No espera input del usuario.
# Modo interactivo (por defecto, espera input)
claude "revisa este código"
# Modo no-interactivo: ejecuta y termina
claude -p "revisa este código y reporta issues"
Output formats
Por defecto, Claude imprime texto en formato legible para humanos. En CI necesitamos machine-parseable:
# JSON estructurado para parsear con jq o scripts
claude -p "analiza el PR" --output-format json
# Forzar un schema específico en el JSON de respuesta
claude -p "analiza el PR" \
--output-format json \
--json-schema '{"type":"object","properties":{"issues":{"type":"array"},"severity":{"type":"string"}}}'
Con --json-schema, la salida validada contra el schema llega en el campo structured_output del JSON de resultado. No la busques en el texto libre de la respuesta.
OUTPUT=$(claude -p "analiza el PR" --output-format json --json-schema "$SCHEMA")
ISSUES=$(echo "$OUTPUT" | jq '.structured_output.issues')
Control de coste por ejecución
En pipelines automáticos conviene poner un techo de gasto explícito por corrida:
claude -p "analiza el PR" \
--output-format json \
--max-budget-usd 1.50
Flag --bare: el modo recomendado para CI/SDK
--bare omite hooks, skills, plugins, servidores MCP, auto memory y CLAUDE.md. Es la configuración recomendada para CI y para el SDK, y será el default de -p en el futuro.
claude --bare -p "revisa este diff y responde en JSON" --output-format json
Ventajas en CI: arranque determinista, sin dependencias de configuración local del runner ni de extensiones que puedan cambiar el comportamiento entre corridas.
Incompatibilidades a tener presentes en este mismo capítulo:
- El patrón de la sección 5 (un
CLAUDE.mdespecífico para CI) no funciona con--bare: ese archivo no se carga. - El patrón de la sección 11 (servidores MCP en el pipeline) tampoco funciona con
--bare: MCP queda desactivado.
Es decir: o pipeline mínimo y determinista con --bare, o pipeline con contexto/MCP sin --bare. No ambas cosas a la vez.
Exit codes y límites de proceso
Claude Code usa exit codes estándar en modo -p:
| Exit code | Significado |
|---|---|
| 0 | Éxito |
| 1 | Error de ejecución |
| 2 | Error de configuración / API key inválida |
| 143 | Terminado por SIGTERM (timeout del job, cancelación del pipeline) |
Estos exit codes permiten a tu pipeline tomar decisiones con if [ $? -ne 0 ]. El 143 es el que verás cuando el runner cancela el job o se agota el timeout: distinguirlo evita reportar un “fallo de review” que en realidad fue una cancelación.
Límite de stdin: desde v2.1.128 el stdin por pipe está limitado a 10MB. Es una restricción directa sobre el patrón de este capítulo (pipear diffs completos): trunca el diff antes de pasarlo.
# Truncar el diff para no superar el límite de stdin
git diff origin/main...HEAD | head -c 8000000 | \
claude --bare -p "revisa este diff" --output-format json
Permisos en headless
En CI no hay nadie para aprobar prompts, así que el control de permisos es parte de la configuración del pipeline, no un detalle opcional.
--allowedTools acepta la sintaxis de reglas de permisos. Ojo con el espacio: en Bash(git diff *) el espacio antes del * impone frontera de palabra.
claude -p "revisa el diff y reporta issues" \
--output-format json \
--allowedTools "Read,Bash(git diff *),Bash(git log *)"
Reglas relevantes al escribirlas:
Bash(ls *)matcheals -lapero nolsof;Bash(ls*)matchea ambos.Bash(safe-cmd *)no autorizasafe-cmd && other-cmd: cada subcomando separado por&&,||,;,|o&se evalúa por separado.- Un
denyen cualquier scope de settings bloquea unallow, incluido lo que pases en--allowedTools.
Modo auto para pipelines: un clasificador aparte revisa cada acción antes de ejecutarla.
claude --permission-mode auto -p "fix all lint errors"
Advertencia operativa: con -p los bloqueos repetidos del clasificador abortan la sesión, porque no hay usuario a quien devolverle el prompt. Si tu pipeline pide acciones que el clasificador considera escaladas de alcance, la corrida termina en fallo — no en una pausa.
2. Session Isolation: Por qué importa en CI/CD
Un principio crítico: Claude no puede revisar efectivamente sus propios cambios en la misma sesión.
El problema
Cuando Claude genera código en una sesión y luego se le pide que lo revise, el modelo tiene el contexto completo de su propio razonamiento. Esto crea un bias de confirmación: tiende a validar lo que generó en lugar de detectar errores.
sequenceDiagram
participant CI as CI Pipeline
participant S1 as Sesión 1 (Generate)
participant S2 as Sesión 2 (Review)
participant Repo as Repositorio
CI->>S1: "Genera tests para auth.ts"
S1->>Repo: Escribe tests/auth.test.ts
CI->>S2: "Revisa tests/auth.test.ts (sin contexto previo)"
S2->>CI: Review objetivo con issues reales
La solución: sesión nueva por cada review
Cada paso del pipeline debe iniciar una sesión completamente nueva. Nunca reutilices una sesión para generar Y revisar.
flowchart LR
A[PR abierto] --> B[Sesión 1: Generar review]
B --> C[Parsear JSON output]
C --> D{Issues críticos?}
D -->|Sí| E[Fail pipeline]
D -->|No| F[Comentar en PR]
F --> G[Merge permitido]
3. GitHub Actions: Code Review Automático
Workflow completo para revisar PRs automáticamente con Claude Code.
# .github/workflows/claude-review.yml
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize]
jobs:
review:
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Claude Code
run: |
curl -fsSL https://claude.ai/install.sh | bash
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Get changed files
id: changed
run: |
FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | \
grep -E '\.(ts|tsx|js|jsx|py|go|rs)$' | \
head -20)
echo "files=$FILES" >> $GITHUB_OUTPUT
- name: Run Claude Review
id: review
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
DIFF=$(git diff origin/${{ github.base_ref }}...HEAD -- ${{ steps.changed.outputs.files }})
OUTPUT=$(claude -p \
--output-format json \
"Revisa este diff de código. Responde en JSON con: {\"summary\": string, \"issues\": [{\"file\": string, \"line\": number, \"severity\": \"critical|warning|info\", \"message\": string}], \"approved\": boolean}
DIFF:
$DIFF" 2>&1)
echo "output=$OUTPUT" >> $GITHUB_OUTPUT
# Fallar si hay issues críticos
CRITICAL=$(echo "$OUTPUT" | jq '[.issues[] | select(.severity == "critical")] | length')
if [ "$CRITICAL" -gt "0" ]; then
echo "❌ Se encontraron $CRITICAL issues críticos"
exit 1
fi
- name: Comment on PR
if: always()
env:
GH_TOKEN: ${{ github.token }}
run: |
OUTPUT='${{ steps.review.outputs.output }}'
SUMMARY=$(echo "$OUTPUT" | jq -r '.summary // "Sin resumen"')
ISSUES=$(echo "$OUTPUT" | jq -r '.issues[] | "- **\(.severity)** `\(.file):\(.line)` — \(.message)"' 2>/dev/null || echo "Sin issues")
COMMENT="## 🤖 Claude Code Review
$SUMMARY
### Issues encontrados
$ISSUES"
gh pr comment ${{ github.event.pull_request.number }} \
--body "$COMMENT"
Notas importantes del workflow
fetch-depth: 0es necesario para quegit difffuncione correctamente entre branches.permissions: pull-requests: writepermite que el workflow comente en el PR.- El output de Claude se parsea con
jq— por eso necesitamos--output-format json. head -20limita los archivos a revisar para no exceder el context window.
Métodos de instalación en CI
El instalador nativo (install.sh en macOS/Linux/WSL, install.ps1 en PowerShell, install.cmd en CMD) es el método recomendado y se auto-actualiza en segundo plano.
Alternativas oficiales según la imagen del runner:
| Método | Comando |
|---|---|
| Instalador nativo (recomendado) | curl -fsSL https://claude.ai/install.sh | bash |
| Homebrew cask | brew install --cask claude-code |
| WinGet | winget install Anthropic.ClaudeCode |
| Repos firmados apt/dnf/apk | paquete claude-code, canales stable o latest |
| npm (opción avanzada) | npm install -g @anthropic-ai/claude-code |
npm install -g @anthropic-ai/claude-code sigue existiendo, pero ya no es el método principal: quedó como opción avanzada. Los instaladores de paquetes (brew, winget, apt/dnf/apk) no se auto-actualizan salvo CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE=1 en brew/winget — en CI eso es deseable: fija la versión y evita cambios de comportamiento entre corridas.
Advertencia de seguridad: contenido no confiable en el pipeline
Este workflow le da a Claude el diff de un PR, que es contenido escrito por terceros. Dos hechos que cambian el modelo de amenaza respecto de una sesión interactiva:
- Con
-pla verificación de trust está desactivada. La confirmación de confianza de codebases y de servidores MCP nuevos no aparece en headless: un pipeline CI no conserva las mismas barreras de confianza que una sesión interactiva. - El diff puede contener prompt injection. Un comentario, un README o un archivo del PR pueden traer instrucciones dirigidas al agente. La documentación oficial lo enuncia así: el comportamiento del modelo puede verse influido por el contenido que procesa, y conviene evitar pipear contenido no confiable directamente a Claude.
Mitigaciones aplicables a estos pipelines:
- Aislamiento: correr el job en un contenedor efímero, sin credenciales de producción montadas.
- Mínimo privilegio:
permissions:del workflow acotado (contents: read), tokens de solo lectura donde sea posible. --barepara no exponer hooks, skills, plugins ni MCP a una sesión que procesa contenido externo.- Allowlists estrictas con
--allowedTools, en vez de dejar el set completo de herramientas disponible.
claude --bare -p "$PROMPT" \
--output-format json \
--allowedTools "Read" \
--max-budget-usd 1.00
Ningún sistema es completamente inmune: la defensa es en profundidad, no confianza en que el modelo ignore las instrucciones hostiles.
4. GitLab CI: Equivalente
# .gitlab-ci.yml
stages:
- review
- test
variables:
ANTHROPIC_API_KEY: $ANTHROPIC_API_KEY # Configurar en GitLab CI/CD Variables
claude-review:
stage: review
image: alpine:3.20
only:
- merge_requests
script:
- apk add --no-cache curl git jq bash
# Instalador nativo (alternativa: paquete del repo firmado apk/apt/dnf en la imagen del runner)
- curl -fsSL https://claude.ai/install.sh | bash
- export PATH="$HOME/.local/bin:$PATH"
- |
DIFF=$(git diff origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME...HEAD)
claude -p \
--output-format json \
"Revisa este diff. JSON: {\"issues\": [{\"severity\": \"critical|warning\", \"message\": string}], \"approved\": boolean}
DIFF: $DIFF" > review-output.json
CRITICAL=$(jq '[.issues[] | select(.severity == "critical")] | length' review-output.json)
if [ "$CRITICAL" -gt "0" ]; then
echo "Issues críticos encontrados:"
jq '.issues[] | select(.severity == "critical")' review-output.json
exit 1
fi
artifacts:
paths:
- review-output.json
expire_in: 7 days
when: always
allow_failure: false
Variables de entorno en GitLab
Configurar en Settings → CI/CD → Variables:
| Variable | Tipo | Descripción |
|---|---|---|
ANTHROPIC_API_KEY | Secret | API key de Anthropic |
MAX_MCP_OUTPUT_TOKENS | Variable | Límite de salida de las herramientas MCP (default 25.000 tokens) |
MCP_CLIENT_SECRET | Secret | Client secret preconfigurado para OAuth de servidores MCP en CI |
Para acotar el gasto por corrida no hay variable de entorno: se usa el flag --max-budget-usd en la invocación (ver sección 1). No existe una variable CLAUDE_MAX_TOKENS en Claude Code.
5. CLAUDE.md para CI/CD
Cuando Claude corre en CI, el CLAUDE.md debe ser más explícito que para uso interactivo. Claude no puede pedirte aclaraciones.
Requisito: este patrón exige no usar
--bare, porque esa bandera omite CLAUDE.md (además de hooks, skills, plugins, MCP y auto memory). Si quieres el determinismo de--bare, mueve estas instrucciones al prompt o a--append-system-prompten lugar de a un archivo.
Qué incluir en CLAUDE.md para CI
# CLAUDE.md — CI/CD Configuration
## Modo de operación
Estás corriendo en un pipeline CI/CD automatizado. No hay usuario presente.
Nunca pidas aclaraciones. Si falta información, usa valores por defecto documentados aquí.
## Output format requerido
SIEMPRE responde en JSON válido con esta estructura exacta:
{
"summary": "string — resumen de máximo 200 caracteres",
"issues": [
{
"file": "ruta/relativa/al/archivo.ts",
"line": 42,
"severity": "critical | warning | info",
"category": "security | performance | correctness | style",
"message": "descripción del issue"
}
],
"approved": true | false,
"metrics": {
"files_reviewed": 0,
"total_issues": 0
}
}
## Criterios de review
### Critical (falla el pipeline):
- SQL injection o XSS potencial
- Secrets hardcodeados (API keys, passwords)
- Funciones async sin manejo de errores
- Imports de módulos inexistentes
### Warning (comentario, no falla):
- Variables declaradas pero no usadas
- Console.log en código de producción
- Funciones con más de 50 líneas
### Info (informativo):
- Sugerencias de refactoring
- Oportunidades de optimización
## Testing standards
- Framework: Vitest
- Fixtures disponibles en: tests/fixtures/
- Mocks en: tests/mocks/
- Cobertura mínima: 80%
## Umbrales que fallan el pipeline
- Más de 0 issues de tipo "critical"
- Más de 10 issues de tipo "warning"
- Cobertura de tests por debajo del 80%
6. Generación de Tests en CI
Patrón para generar tests automáticamente cuando se agregan nuevas funciones:
flowchart TD
A[Nueva función detectada en PR] --> B[Sesión: Generar tests]
B --> C[Guardar tests generados]
C --> D[Ejecutar tests]
D --> E{Tests pasan?}
E -->|Sí| F[Commit tests al PR]
E -->|No| G[Sesión: Corregir tests]
G --> D
E -->|Fallo 3+ veces| H[Fallar pipeline con reporte]
Script de generación de tests
#!/bin/bash
# scripts/generate-tests.sh
MAX_RETRIES=3
RETRY=0
generate_and_run() {
local source_file=$1
local test_file="${source_file%.ts}.test.ts"
# Sesión nueva: generar tests
claude -p \
--output-format json \
"Lee el archivo $source_file y genera tests completos en Vitest.
Responde con JSON: {\"test_code\": \"...\", \"imports_needed\": [\"...\"]}.
Los tests deben ser ejecutables sin modificaciones.
Usa los fixtures en tests/fixtures/ si aplica.
$(cat $source_file)" > /tmp/test-output.json
TEST_CODE=$(jq -r '.test_code' /tmp/test-output.json)
echo "$TEST_CODE" > "$test_file"
# Ejecutar tests
if npx vitest run "$test_file" --reporter=json > /tmp/test-results.json 2>&1; then
echo "✅ Tests generados y pasando: $test_file"
return 0
else
return 1
fi
}
SOURCE_FILE=$1
while [ $RETRY -lt $MAX_RETRIES ]; do
if generate_and_run "$SOURCE_FILE"; then
exit 0
fi
RETRY=$((RETRY + 1))
echo "Intento $RETRY/$MAX_RETRIES falló"
done
echo "❌ No se pudieron generar tests válidos después de $MAX_RETRIES intentos"
exit 1
7. Tool Design — Domain 2
Esta sección es crítica para el examen. El diseño de herramientas determina si Claude las usará correctamente.
El problema con descripciones vagas
Claude selecciona herramientas basándose en sus descripciones. Una descripción vaga produce selecciones incorrectas.
MALO:
{
"name": "analyze_content",
"description": "Analyzes content",
"input_schema": {
"type": "object",
"properties": {
"data": { "type": "string" }
}
}
}
Problemas: ¿Qué tipo de contenido? ¿Qué devuelve? ¿Cuándo NO usar esta herramienta?
BUENO:
{
"name": "analyze_invoice_pdf",
"description": "Extrae datos estructurados de facturas en formato PDF. Input: PDF codificado en base64. Devuelve: vendor_name, total_amount, line_items[], invoice_date, currency. Úsala cuando el usuario suba una factura para procesar. NO la uses para páginas web, imágenes de texto genéricas o documentos que no sean facturas.",
"input_schema": {
"type": "object",
"properties": {
"pdf_base64": {
"type": "string",
"description": "PDF codificado en base64. Máximo 10MB."
},
"extract_line_items": {
"type": "boolean",
"description": "Si true, extrae cada línea de la factura. Default: true."
}
},
"required": ["pdf_base64"]
}
}
Reglas para descripciones efectivas
Toda buena descripción de herramienta incluye:
- Qué hace exactamente — verbo concreto, no genérico
- Formato exacto del input — tipo, encoding, límites
- Formato exacto del output — campos, tipos, valores posibles
- Cuándo USAR esta herramienta — trigger conditions
- Cuándo NO USAR esta herramienta — boundaries explícitos
- Ejemplos de queries que deben rutear a esta herramienta
Comparación completa
// MALO: Claude no sabe cómo usarla
const badTool = {
name: "search",
description: "Searches for information",
input_schema: {
type: "object",
properties: {
query: { type: "string" }
}
}
}
// BUENO: Claude selecciona correctamente
const goodTool = {
name: "search_product_catalog",
description: `Busca productos en el catálogo interno por nombre, SKU o categoría.
Devuelve: [{id, name, sku, price, stock, category}].
Úsala cuando el usuario pregunte por disponibilidad, precio o características de productos.
NO la uses para buscar información general, noticias o datos externos.
Queries que van aquí: "¿tienen el modelo X?", "precio de SKU-123", "productos de la categoría Y".`,
input_schema: {
type: "object",
properties: {
query: {
type: "string",
description: "Término de búsqueda: nombre parcial, SKU completo, o nombre de categoría"
},
max_results: {
type: "number",
description: "Máximo de resultados. Default: 10, máximo: 50."
}
},
required: ["query"]
}
}
8. Error Responses Estructuradas en MCP Tools
Las herramientas MCP deben devolver errores que permitan a Claude decidir si reintentar o no.
Estructura recomendada
interface ToolError {
isError: true;
errorCategory: "transient" | "validation" | "business" | "permission";
isRetryable: boolean;
message: string;
retryAfterMs?: number; // Solo si isRetryable: true
}
Cuándo usar cada categoría
// TRANSIENT: error temporal, Claude debe reintentar
function handleTimeout(): ToolError {
return {
isError: true,
errorCategory: "transient",
isRetryable: true,
message: "El servicio no respondió a tiempo. Reintenta en unos segundos.",
retryAfterMs: 3000
};
}
// VALIDATION: input incorrecto, Claude debe corregir el input
function handleInvalidInput(field: string): ToolError {
return {
isError: true,
errorCategory: "validation",
isRetryable: false,
message: `El campo '${field}' tiene formato inválido. Formato esperado: YYYY-MM-DD.`
};
}
// BUSINESS: dato genuinamente ausente, NO reintentar
function handleNotFound(id: string): ToolError {
return {
isError: true,
errorCategory: "business",
isRetryable: false,
message: `El producto con ID '${id}' no existe en el catálogo.`
};
}
// PERMISSION: sin acceso, NO reintentar
function handleUnauthorized(): ToolError {
return {
isError: true,
errorCategory: "permission",
isRetryable: false,
message: "Sin permisos para acceder a datos financieros. Contacta al administrador."
};
}
Distinción crítica: timeout vs vacío válido
// ❌ INCORRECTO: reintentar cuando el resultado es vacío válido
async function searchProducts_bad(query: string) {
const results = await db.search(query);
if (results.length === 0) {
return {
isError: true,
errorCategory: "transient",
isRetryable: true, // ← INCORRECTO: no hay productos, no es un error
message: "No se encontraron resultados"
};
}
return results;
}
// ✅ CORRECTO: vacío es un resultado válido
async function searchProducts_good(query: string) {
try {
const results = await db.search(query);
return { results, total: results.length }; // Devolver array vacío es válido
} catch (err) {
if (err.code === 'TIMEOUT') {
return {
isError: true,
errorCategory: "transient",
isRetryable: true,
message: "Base de datos no disponible temporalmente."
};
}
throw err;
}
}
9. Distribución de Herramientas entre Agentes
Un principio fundamental: demasiadas herramientas disminuye la confiabilidad.
Por qué menos herramientas es mejor
Cuando un agente tiene acceso a herramientas fuera de su especialización:
- Selecciona herramientas incorrectas para tareas ambiguas
- Produce resultados inconsistentes
- Es más difícil de debuggear y testear
flowchart TD
subgraph Incorrecto["❌ Incorrecto: Todas las tools en todos los agentes"]
A1[Research Agent] --> T1[web_search]
A1 --> T2[write_file]
A1 --> T3[execute_sql]
A1 --> T4[send_email]
A2[Synthesis Agent] --> T1
A2 --> T2
A2 --> T3
A2 --> T4
end
subgraph Correcto["✅ Correcto: Tools especializadas por agente"]
B1[Research Agent] --> U1[web_search]
B1 --> U2[fetch_url]
B2[Synthesis Agent] --> U3[write_file]
B2 --> U4[verify_fact]
B3[Delivery Agent] --> U5[send_email]
B3 --> U6[create_ticket]
end
tool_choice: controlar cuándo Claude usa herramientas
import anthropic
client = anthropic.Anthropic()
# tool_choice: "any" — Claude SIEMPRE debe llamar una tool
# Útil cuando necesitas output estructurado garantizado
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
tools=[search_tool, write_tool],
tool_choice={"type": "any"}, # Garantiza que siempre use una tool
messages=[{"role": "user", "content": "Analiza el repositorio"}]
)
# tool_choice: "auto" — Claude decide si usar herramientas
# Comportamiento por defecto
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
tools=[search_tool, write_tool],
tool_choice={"type": "auto"},
messages=[{"role": "user", "content": "¿Qué hace esta función?"}]
)
# tool_choice: "tool" — Claude DEBE usar esta herramienta específica
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
tools=[format_output_tool],
tool_choice={"type": "tool", "name": "format_output"},
messages=[{"role": "user", "content": "Dame el reporte final"}]
)
Cuándo dar acceso cross-role
En algunos casos, un agente necesita una herramienta de otro dominio:
// Synthesis Agent normalmente NO tiene web_search
// PERO puede tener verify_fact (versión limitada de search)
const synthesisAgentTools = [
write_document_tool,
format_output_tool,
// Acceso de solo lectura para verificar datos, no para investigar
{
name: "verify_fact",
description: `Verifica si un dato específico es correcto consultando fuentes confiables.
Solo úsala para confirmar hechos que ya tienes, no para investigar nuevos temas.
Input: el dato a verificar. Output: {confirmed: boolean, source: string}.
NO uses esto para buscar información nueva — usa el Research Agent para eso.`
}
]
10. Testing de Tool Selection
Cómo verificar que tus descripciones funcionan correctamente.
Test con requests ambiguos
import anthropic
import json
client = anthropic.Anthropic()
# Herramientas con descripciones a testear
tools = [search_product_tool, search_knowledge_base_tool, search_web_tool]
# Test cases: queries ambiguos que deben rutear a la tool correcta
test_cases = [
{
"query": "¿tienen el modelo MacBook Pro M3?",
"expected_tool": "search_product_catalog"
},
{
"query": "¿cómo configuro el adaptador?",
"expected_tool": "search_knowledge_base"
},
{
"query": "últimas noticias sobre Apple",
"expected_tool": "search_web"
}
]
def test_tool_selection():
failures = []
for case in test_cases:
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=256,
tools=tools,
tool_choice={"type": "any"},
messages=[{"role": "user", "content": case["query"]}]
)
selected_tool = response.content[0].name
if selected_tool != case["expected_tool"]:
failures.append({
"query": case["query"],
"expected": case["expected_tool"],
"got": selected_tool
})
if failures:
print("❌ Tool selection failures:")
print(json.dumps(failures, indent=2))
return False
print("✅ Todas las tool selections correctas")
return True
test_tool_selection()
Iterar sobre descripciones
Si una herramienta se selecciona incorrectamente, el proceso de mejora es:
flowchart LR
A[Ejecutar test suite] --> B{Algún fallo?}
B -->|No| C[Descripciones correctas ✅]
B -->|Sí| D[Identificar query problemática]
D --> E[Analizar por qué Claude eligió mal]
E --> F[Mejorar descripción de la tool incorrecta]
F --> G[Agregar boundary explícito en ambas tools]
G --> A
Keywords que causan confusión
Evita keywords genéricas en descripciones que se solapan con otras herramientas:
// PROBLEMA: "buscar" aparece en ambas tools
const tool1 = { description: "Busca información en el catálogo" }
const tool2 = { description: "Busca información en la base de conocimiento" }
// SOLUCIÓN: diferenciadores explícitos
const tool1Fixed = {
description: `Consulta el inventario de productos por SKU, nombre o categoría.
SOLO para preguntas sobre disponibilidad, precio y stock.
Queries: "¿tienen X?", "precio de Y", "stock de Z"`
}
const tool2Fixed = {
description: `Consulta artículos de soporte, manuales y guías de configuración.
SOLO para preguntas sobre cómo usar productos, troubleshooting o configuración.
Queries: "cómo configurar X", "error al instalar Y", "pasos para Z"`
}
11. MCP Servers en Workflows CI/CD
Configurar MCP servers para CI
Los MCP servers en CI necesitan credenciales via variables de entorno. Usa el archivo .mcp.json del proyecto (no el global del usuario).
// .mcp.json (en raíz del proyecto, commiteado al repo)
{
"mcpServers": {
"database": {
"command": "npx",
"args": ["-y", "@company/mcp-database"],
"env": {
"DB_HOST": "${DB_HOST}",
"DB_PORT": "${DB_PORT}",
"DB_NAME": "${DB_NAME}",
"DB_USER": "${DB_USER}",
"DB_PASSWORD": "${DB_PASSWORD}"
}
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_TOKEN": "${GITHUB_TOKEN}"
}
}
}
}
GitHub Actions con MCP
# .github/workflows/claude-with-mcp.yml
- name: Run Claude with MCP access
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
DB_HOST: ${{ secrets.DB_HOST }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
DB_USER: ${{ vars.DB_USER }}
DB_NAME: ${{ vars.DB_NAME }}
DB_PORT: "5432"
GITHUB_TOKEN: ${{ github.token }}
run: |
claude -p \
--mcp-config .mcp.json \
"Verifica que todas las migraciones de base de datos sean reversibles y
no rompan las queries existentes. Devuelve JSON con {safe: boolean, issues: []}."
Scoping: por qué usar .mcp.json del proyecto
flowchart TD
A["~/.claude/settings.json (global)"] --> B[Disponible para todos los proyectos]
C[".mcp.json (proyecto)"] --> D[Solo disponible en este pipeline]
style A fill:#ff6b6b,color:#fff
style C fill:#51cf66,color:#fff
B --> E[❌ Expone tools de producción en proyectos de test]
D --> F[✅ Solo tools relevantes para este CI/CD]
Usar .mcp.json en el proyecto garantiza que el CI solo tiene acceso a los backends necesarios, no a todos los MCP servers configurados globalmente en la máquina del desarrollador.
Autenticación de servidores MCP en CI
No todos los servidores se autentican con variables de entorno simples. Para OAuth 2.0 existe claude mcp login <name> desde la shell (v2.1.186+), lo que lo hace utilizable en un runner sin sesión interactiva:
# Runner sin display: imprime la URL en vez de abrir el navegador
claude mcp login database --no-browser
# Credenciales preconfiguradas (lo habitual en CI)
claude mcp login database \
--client-id "$MCP_CLIENT_ID" \
--client-secret "$MCP_CLIENT_SECRET"
El secreto también puede entregarse por la variable MCP_CLIENT_SECRET, y se guarda en el keychain del sistema, no en el archivo de configuración. claude mcp logout <name> borra las credenciales al final del job.
Para autenticación no-OAuth (Kerberos, SSO interno, tokens efímeros que caducan durante el pipeline) se usa headersHelper: un comando que escribe un objeto JSON de headers en stdout y se ejecuta en cada conexión (sin caché, timeout de 10 s).
{
"mcpServers": {
"internal-api": {
"type": "http",
"url": "https://api.interna.example.com/mcp",
"headersHelper": "./scripts/mcp-headers.sh"
}
}
}
#!/bin/bash
# scripts/mcp-headers.sh — recibe CLAUDE_CODE_MCP_SERVER_NAME y _URL
TOKEN=$(vault read -field=token secret/ci/mcp)
printf '{"Authorization":"Bearer %s"}' "$TOKEN"
Cuántos servidores conectar: el criterio ya no son los tokens
Tool search está activo por defecto: las definiciones de herramientas MCP se difieren y solo se cargan bajo demanda; al inicio entran únicamente los nombres de herramientas y las instrucciones del servidor. El coste de arranque en tokens de tener varios servidores ya no es el criterio de decisión.
Lo que sí queda como riesgo residual es la superficie de ataque y los permisos: cada servidor conectado es un backend más al que el agente puede llegar en un pipeline donde procesa contenido no confiable, y donde (por correr con -p) la verificación de trust de servidores nuevos está desactivada. Conecta solo los backends que ese job necesita y revisa que confías en cada uno: un servidor que trae contenido externo es un vector de prompt injection.
Como límite de contención adicional, MAX_MCP_OUTPUT_TOKENS acota la salida de las herramientas MCP (default 25.000 tokens).
12. Manejo Seguro de Contenido en CI
El problema: caracteres especiales rompen el JSON
Cuando se pasa un diff como string directo en el prompt, los caracteres especiales (", \n, `) corrompen el JSON que Claude intenta parsear:
# ❌ INCORRECTO: $DIFF sin escapar puede contener comillas, barras, etc.
OUTPUT=$(claude -p \
--output-format json \
"Revisa este diff: $DIFF")
# Error: unexpected token in JSON at position 47
La solución: jq -Rs .
jq -Rs . lee stdin como raw string y lo convierte en un string JSON correctamente escapado:
# ✅ CORRECTO: escapar el diff antes de interpolarlo
DIFF=$(git diff origin/${{ github.base_ref }}...HEAD)
DIFF_ESCAPED=$(echo "$DIFF" | jq -Rs .)
# Construir el prompt con jq para garantizar JSON válido
PROMPT=$(jq -n --argjson diff "$DIFF_ESCAPED" \
'"Revisa este diff de código. Responde en JSON con issues y severidad.\n\nDIFF:\n" + $diff')
OUTPUT=$(claude -p --output-format json "$PROMPT")
Workflow corregido (paso Run Claude Review)
- name: Run Claude Review
id: review
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
DIFF=$(git diff origin/${{ github.base_ref }}...HEAD \
-- ${{ steps.changed.outputs.files }})
# Escapar contenido arbitrario para uso seguro en JSON
DIFF_ESCAPED=$(echo "$DIFF" | jq -Rs .)
PROMPT=$(jq -n --argjson diff "$DIFF_ESCAPED" \
'"Revisa este diff. JSON: {\"summary\":string,\"issues\":[{\"file\":string,\"line\":number,\"severity\":\"critical|warning|info\",\"message\":string}],\"approved\":boolean}\n\nDIFF:\n" + $diff')
OUTPUT=$(claude -p --output-format json "$PROMPT" 2>&1)
echo "output=$OUTPUT" >> $GITHUB_OUTPUT
CRITICAL=$(echo "$OUTPUT" | jq '[.issues[] | select(.severity == "critical")] | length')
if [ "$CRITICAL" -gt "0" ]; then
echo "Se encontraron $CRITICAL issues críticos"
exit 1
fi
Regla general: cualquier contenido externo (diff, archivos, logs) que se interpole en un prompt debe pasar por jq -Rs . antes de usarse.
13. Testing de Tool Selection Reliability
Una descripción de herramienta puede parecer correcta pero fallar con queries ambiguas del mundo real. La única forma de saberlo es con tests.
Suite de tests en TypeScript
// tests/tool-selection.test.ts
import Anthropic from "@anthropic-ai/sdk";
import { expect, test, describe } from "vitest";
const client = new Anthropic();
const tools: Anthropic.Tool[] = [
{
name: "search_customer",
description: `Busca un cliente por nombre, email o ID de cliente.
Devuelve: {id, name, email, plan, created_at}.
Úsala cuando pregunten por datos de un cliente específico.
NO la uses para buscar órdenes o productos.
Queries: "datos del cliente X", "email de Juan", "cliente ID-123".`,
input_schema: {
type: "object",
properties: {
query: { type: "string", description: "Nombre, email o ID del cliente" }
},
required: ["query"]
}
},
{
name: "lookup_order",
description: `Consulta una orden de compra por número de orden o cliente.
Devuelve: {order_id, status, items[], total, created_at}.
Úsala cuando pregunten por el estado o contenido de una compra.
NO la uses para datos de clientes ni información de productos.
Queries: "estado de la orden #456", "compras de María", "orden de ayer".`,
input_schema: {
type: "object",
properties: {
query: { type: "string", description: "Número de orden o nombre del cliente" }
},
required: ["query"]
}
},
{
name: "get_product",
description: `Obtiene información de un producto por nombre, SKU o categoría.
Devuelve: {sku, name, price, stock, description}.
Úsala cuando pregunten por disponibilidad, precio o características de productos.
NO la uses para clientes ni órdenes.
Queries: "precio del SKU-789", "¿tienen laptops?", "características del modelo X".`,
input_schema: {
type: "object",
properties: {
query: { type: "string", description: "Nombre, SKU o categoría del producto" }
},
required: ["query"]
}
}
];
const testCases = [
{ query: "¿cuál es el email de Ana García?", expected: "search_customer" },
{ query: "¿llegó mi pedido del martes?", expected: "lookup_order" },
{ query: "¿tienen el modelo Pro en stock?", expected: "get_product" },
{ query: "cliente con ID C-9921", expected: "search_customer" },
{ query: "estado de la orden #ORD-4455", expected: "lookup_order" },
];
describe("tool selection reliability", () => {
test.each(testCases)("'$query' → $expected", async ({ query, expected }) => {
const response = await client.messages.create({
model: "claude-opus-4-8",
max_tokens: 256,
tools,
tool_choice: { type: "any" },
messages: [{ role: "user", content: query }]
});
const selected = (response.content[0] as Anthropic.ToolUseBlock).name;
expect(selected).toBe(expected);
});
});
Matriz de resultados: esperado vs obtenido
Ejecutar npx vitest run tests/tool-selection.test.ts produce una tabla clara:
FAIL tests/tool-selection.test.ts
tool selection reliability
✓ '¿cuál es el email de Ana García?' → search_customer
✗ '¿llegó mi pedido del martes?' → lookup_order
expected: 'lookup_order'
received: 'search_customer' ← "mi pedido" se confunde con cliente
✓ '¿tienen el modelo Pro en stock?' → get_product
✓ 'cliente con ID C-9921' → search_customer
✓ 'estado de la orden #ORD-4455' → lookup_order
Ciclo de iteración
flowchart LR
A[Ejecutar test suite] --> B{Algún fallo?}
B -->|No| C[Descripciones confiables]
B -->|Sí| D[Identificar query fallida]
D --> E[Leer descripción de la tool incorrecta]
E --> F["Agregar boundary explícito<br/>NO la uses cuando..."]
F --> G[Agregar query de ejemplo a ambas tools]
G --> A
Regla práctica: si dos tools comparten un keyword genérico (cliente, buscar, datos), agrega ese keyword como boundary negativo en la que NO debe responder.
14. Análisis de Cobertura Diferencial en CI
En lugar de medir cobertura total (que puede ocultar gaps en código nuevo), se analiza específicamente qué líneas del diff no tienen cobertura.
Flujo completo
flowchart TD
A[PR con cambios] --> B["Ejecutar tests con coverage<br/>npx vitest --coverage"]
B --> C[Generar lcov.info]
C --> D[Extraer diff del PR]
D --> E["Claude: analizar qué líneas del diff<br/>no aparecen en lcov.info"]
E --> F{Gaps críticos?}
F -->|Sí| G["Fail pipeline<br/>con lista de funciones sin test"]
F -->|No| H[Pass + reporte informativo]
Script de análisis
#!/bin/bash
# scripts/coverage-diff-analysis.sh
# 1. Generar coverage report
npx vitest run --coverage --reporter=lcov
LCOV_FILE="coverage/lcov.info"
# 2. Obtener funciones modificadas en el diff
DIFF=$(git diff origin/$BASE_BRANCH...HEAD)
CHANGED_FUNCTIONS=$(echo "$DIFF" | grep "^+" | grep -E "^\\+.*(function |const .+ = |=> \{)" | \
sed 's/^+//')
# 3. Enviar a Claude para análisis cruzado
LCOV_CONTENT=$(cat "$LCOV_FILE" | head -500) # primeras 500 líneas son suficientes
ANALYSIS=$(claude -p --output-format json \
"Analiza qué funciones del DIFF no están cubiertas en el LCOV report.
Responde JSON: {\"covered\": [\"fn1\"], \"uncovered\": [\"fn2\"], \"coverage_pct\": 85}
DIFF (funciones modificadas):
$(echo "$CHANGED_FUNCTIONS" | jq -Rs .)
LCOV REPORT (extracto):
$(echo "$LCOV_CONTENT" | jq -Rs .)")
UNCOVERED=$(echo "$ANALYSIS" | jq '.uncovered | length')
echo "$ANALYSIS" | jq .
if [ "$UNCOVERED" -gt "3" ]; then
echo "Más de 3 funciones modificadas sin cobertura de test"
exit 1
fi
Prompt específico para coverage gap analysis
Analiza el siguiente diff de código y el reporte de cobertura lcov.
Tu tarea: identificar qué funciones o bloques NUEVOS o MODIFICADOS en el diff
no tienen líneas correspondientes en el lcov report.
Reglas:
- Solo reporta funciones que aparecen en el diff (no las que ya existían)
- Una función está "cubierta" si su número de línea aparece en el lcov con DA:<line>,<count> donde count > 0
- Ignora comentarios y líneas de declaración de tipos
Responde con JSON:
{
"covered": ["nombre de función o descripción"],
"uncovered": ["nombre de función o descripción"],
"coverage_pct": 0-100,
"risk_assessment": "low|medium|high"
}
Integración en GitHub Actions
- name: Coverage diff analysis
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
BASE_BRANCH: ${{ github.base_ref }}
run: |
npx vitest run --coverage --reporter=lcov --silent
bash scripts/coverage-diff-analysis.sh
Resumen: Puntos Clave para el Examen
| Concepto | Regla |
|---|---|
| Modo no-interactivo | Flag -p, --output-format json |
--bare | Omite hooks, skills, plugins, MCP, auto memory y CLAUDE.md; recomendado para CI/SDK y futuro default de -p |
| Instalación en CI | Instalador nativo (install.sh/install.ps1/install.cmd); npm es opción avanzada |
| Salida estructurada | --json-schema deja el resultado validado en structured_output |
| Coste por corrida | --max-budget-usd |
| Exit codes | 0 éxito, 1 ejecución, 2 configuración, 143 SIGTERM |
| Límite de stdin | 10MB por pipe desde v2.1.128 — truncar diffs grandes |
| Permisos en headless | --allowedTools "Bash(git diff *)" (el espacio importa) y --permission-mode auto |
Auto mode con -p | Los bloqueos repetidos del clasificador abortan la sesión |
Trust con -p | La verificación de trust está desactivada: el diff del PR es contenido no confiable |
| Session isolation | Nueva sesión para generate y para review |
| CLAUDE.md en CI | Output format explícito, criterios categóricos, umbrales (incompatible con --bare) |
| Tool descriptions | Input, output, cuándo usar, cuándo NO usar |
| Error responses | isRetryable: false para datos genuinamente ausentes |
| Distribución de tools | Menos tools por agente = mayor confiabilidad |
tool_choice: "any" | Garantiza que Claude siempre llame una tool |
.mcp.json en proyecto | Scope correcto para CI/CD |
| Auth MCP en CI | claude mcp login <name> --no-browser, --client-secret/MCP_CLIENT_SECRET, headersHelper |
| Muchos servidores MCP | Con tool search por defecto el criterio es superficie de ataque, no tokens |
| Escaping en CI | jq -Rs . para contenido externo en prompts |
| Tool selection tests | Suite TypeScript con queries ambiguas + matriz esperado/obtenido |
| Coverage diferencial | Analizar solo líneas del diff, no cobertura total |
Siguiente: Sistema Multi-Agente de Investigación