Módulo 8: Proyecto integrador: Tu primer proyecto con Claude Code

Setup: CLAUDE.md, Skills y Hooks

Setup: CLAUDE.md, Skills y Hooks

Objetivo del proyecto

Construir una herramienta CLI funcional usando el workflow completo de Claude Code. En esta cápsula completas la Fase 1: Setup — la configuración profesional que diferencia a un usuario casual de uno que sabe lo que hace.


Qué construiste en módulos anteriores

  • Módulo 03: Aprendiste a crear un CLAUDE.md profesional con las 6 secciones esenciales
  • Módulo 05: Aprendiste a crear skills (slash commands) y configurar hooks (lifecycle events)

En esta cápsula aplicas ambos conocimientos en un proyecto real.


Qué agregarás en este módulo

Al terminar esta cápsula tendrás:

your-cli-project/
├── CLAUDE.md                          ← Contexto profesional
├── .claude/
│   ├── skills/
│   │   ├── create-command.md          ← Skill 1: crear comandos
│   │   └── add-tests.md              ← Skill 2: generar tests
│   ├── settings.json                  ← Hooks configurados
│   └── settings.local.json            ← Settings personales
├── src/                               ← (vacío por ahora)
└── tests/                             ← (vacío por ahora)

Paso a paso guiado

Paso 1: Crear el directorio del proyecto

Abre tu terminal y crea el proyecto:

mkdir my-cli-project
cd my-cli-project
git init

Reemplaza my-cli-project con el nombre de tu CLI. Por ejemplo, si elegiste el organizador de archivos: fileorg. Si elegiste el CLI de notas: notes-cli.

Verifica que git está inicializado:

git status

Deberías ver:

On branch main

No commits yet

nothing to commit (create directory if you want)

Paso 2: Crear la estructura de directorios

Para Python:

mkdir -p src tests .claude/skills
touch src/__init__.py tests/__init__.py

Para TypeScript:

mkdir -p src tests .claude/skills

La estructura debería verse así:

my-cli-project/
├── .claude/
│   └── skills/
├── src/
└── tests/

Paso 3: Crear CLAUDE.md

Este es el archivo más importante del setup. Claude Code lo lee al inicio de cada sesión y lo usa como contexto para todas sus respuestas.

Crea el archivo CLAUDE.md en la raíz del proyecto.

CLAUDE.md para Python (Click):

# [Nombre de tu CLI]

CLI para [descripción en 1 frase]. Herramienta de línea de comandos
construida con Python y Click.

## Stack
- Python 3.12
- Click 8.1 (framework CLI)
- pytest 8.0 (testing)
- Ruff (linter y formatter)

## Estructura
src/
├── __init__.py
├── cli.py              → Entry point, grupo de comandos Click
├── commands/            → Un archivo por comando
│   ├── __init__.py
│   ├── command_one.py   → Primer comando
│   ├── command_two.py   → Segundo comando
│   └── command_three.py → Tercer comando
└── utils.py             → Funciones helper compartidas

tests/
├── __init__.py
├── test_command_one.py
├── test_command_two.py
└── test_command_three.py

## Convenciones
- snake_case para archivos, funciones, y variables
- Type hints en todas las funciones públicas
- Docstrings en funciones de comandos (se usan como help text)
- Click decorators para argumentos y opciones
- Rich o click.echo para output formateado
- No usar print() directamente — usar click.echo() o rich.print()

## Comandos de desarrollo
- Instalar: `pip install -e ".[dev]"`
- Ejecutar CLI: `python -m src.cli [command]`
- Tests: `pytest`
- Tests verbose: `pytest -v`
- Lint: `ruff check src/`
- Format: `ruff format src/`

## Reglas
- Cada comando en su propio archivo dentro de src/commands/
- Todos los comandos se registran en src/cli.py
- NO mezclar lógica de negocio con lógica de CLI (Click)
- Siempre incluir --help con descripción clara
- Manejo de errores con click.ClickException, no sys.exit()
- Ejecutar tests después de implementar cada comando

CLAUDE.md para TypeScript (Commander):

# [Nombre de tu CLI]

CLI para [descripción en 1 frase]. Herramienta de línea de comandos
construida con TypeScript y Commander.

## Stack
- Node.js 20, TypeScript 5.4
- Commander 12 (framework CLI)
- Vitest 1.6 (testing)
- ESLint + Prettier

## Estructura
src/
├── index.ts             → Entry point, programa Commander
├── commands/            → Un archivo por comando
│   ├── commandOne.ts    → Primer comando
│   ├── commandTwo.ts    → Segundo comando
│   └── commandThree.ts  → Tercer comando
└── utils.ts             → Funciones helper compartidas

tests/
├── commandOne.test.ts
├── commandTwo.test.ts
└── commandThree.test.ts

## Convenciones
- camelCase para variables y funciones, PascalCase para tipos
- Archivos de comandos en camelCase: commandOne.ts
- Type annotations en todas las funciones exportadas
- Commander .description() para help text
- chalk para output con colores
- No usar console.log para output de usuario — usar helper formateado
- process.exit() solo en el entry point, nunca en comandos

## Comandos de desarrollo
- Instalar: `npm install`
- Build: `npm run build`
- Ejecutar CLI: `npx ts-node src/index.ts [command]`
- Tests: `npm test`
- Lint: `npm run lint`
- Format: `npm run format`

## Reglas
- Cada comando en su propio archivo dentro de src/commands/
- Todos los comandos se registran en src/index.ts
- NO mezclar lógica de negocio con lógica de CLI (Commander)
- Siempre incluir .description() en cada comando
- Manejo de errores con throw, no process.exit() dentro de comandos
- Ejecutar tests después de implementar cada comando

Notas sobre el CLAUDE.md

  • Personaliza el nombre y la descripción — reemplaza [Nombre de tu CLI] y [descripción en 1 frase]
  • Ajusta los nombres de comandos — reemplaza command_one, command_two, command_three con los nombres reales de tus comandos
  • < 200 líneas — el ejemplo tiene ~50 líneas. Tienes espacio para agregar más detalle si lo necesitas
  • Referencia Módulo 03 si necesitas recordar las mejores prácticas de CLAUDE.md

Paso 4: Crear Skills

Los skills son archivos markdown en .claude/skills/ que Claude Code lee cuando los invocas con un slash command. Crearás 2 skills específicos para tu proyecto CLI.

Skill 1: /create-command

Este skill le dice a Claude cómo crear un nuevo comando CLI siguiendo las convenciones del proyecto.

Para Python — .claude/skills/create-command/SKILL.md:

# Crear comando CLI

Crea un nuevo comando para la CLI siguiendo la estructura y convenciones
del proyecto.

## Instrucciones

1. Crea el archivo src/commands/[command_name].py
2. Importa click y las utilidades necesarias
3. Crea la función del comando con decorador @click.command()
4. Agrega argumentos y opciones con @click.argument() y @click.option()
5. Implementa la lógica del comando
6. Registra el comando en src/cli.py importándolo y agregándolo al grupo
7. Verifica que el comando aparece en --help

## Template del comando

```python
"""Comando [nombre]: [descripción breve]."""

import click

from src.utils import format_output


@click.command()
@click.argument("input_value")
@click.option("--verbose", "-v", is_flag=True, help="Output detallado")
def command_name(input_value: str, verbose: bool) -> None:
    """[Descripción del comando que aparece en --help]."""
    try:
        result = process(input_value)
        if verbose:
            format_output(result, detail=True)
        else:
            format_output(result)
    except Exception as e:
        raise click.ClickException(str(e))

Registro en cli.py

Después de crear el comando, agrégalo al grupo en src/cli.py:

from src.commands.command_name import command_name
cli.add_command(command_name)

Reglas

  • Un archivo = un comando
  • Docstring en la función = help text del comando
  • Errores con click.ClickException, no sys.exit()
  • Type hints en todos los parámetros
  • Output con click.echo() o rich, no print()

**Para TypeScript — `.claude/skills/create-command/SKILL.md`:**

```markdown
# Crear comando CLI

Crea un nuevo comando para la CLI siguiendo la estructura y convenciones
del proyecto.

## Instrucciones

1. Crea el archivo src/commands/[commandName].ts
2. Importa las utilidades necesarias
3. Crea una función exportada que reciba el Command de Commander
4. Define argumentos, opciones, y description
5. Implementa la lógica del comando
6. Registra el comando en src/index.ts
7. Verifica que el comando aparece en --help

## Template del comando

```typescript
import { Command } from "commander";
import { formatOutput } from "../utils";

export function registerCommandName(program: Command): void {
  program
    .command("name")
    .description("[Descripción del comando]")
    .argument("<input>", "descripción del argumento")
    .option("-v, --verbose", "Output detallado")
    .action(async (input: string, options: { verbose?: boolean }) => {
      try {
        const result = await process(input);
        formatOutput(result, options.verbose);
      } catch (error) {
        console.error(`Error: ${(error as Error).message}`);
        process.exitCode = 1;
      }
    });
}

Registro en index.ts

Después de crear el comando, regístralo en src/index.ts:

import { registerCommandName } from "./commands/commandName";
registerCommandName(program);

Reglas

  • Un archivo = un comando
  • .description() obligatorio para help text
  • Errores con throw, no process.exit() dentro del action
  • Type annotations en todos los parámetros
  • Output con helper formateado, no console.log directo

#### Skill 2: `/add-tests`

Este skill le dice a Claude cómo generar tests para un comando específico.

**Para Python — `.claude/skills/add-tests/SKILL.md`:**

```markdown
# Generar tests para comando

Crea tests para un comando CLI específico usando pytest.

## Instrucciones

1. Identifica el comando a testear en src/commands/
2. Analiza los argumentos, opciones, y lógica del comando
3. Crea el archivo tests/test_[command_name].py
4. Usa CliRunner de Click para testear comandos
5. Incluye tests de:
   - Happy path (uso normal con argumentos válidos)
   - Argumentos faltantes o inválidos
   - Opciones/flags (--verbose, etc.)
   - Edge cases específicos del comando
6. Ejecuta los tests: pytest tests/test_[command_name].py -v

## Template de tests

```python
"""Tests para el comando [nombre]."""

from click.testing import CliRunner

from src.cli import cli


class TestCommandName:
    """Tests para el comando [nombre]."""

    def setup_method(self) -> None:
        """Setup para cada test."""
        self.runner = CliRunner()

    def test_basic_usage(self) -> None:
        """Test de uso básico con argumentos válidos."""
        result = self.runner.invoke(cli, ["nombre", "argumento"])
        assert result.exit_code == 0
        assert "expected output" in result.output

    def test_missing_argument(self) -> None:
        """Test de argumento faltante."""
        result = self.runner.invoke(cli, ["nombre"])
        assert result.exit_code != 0

    def test_verbose_flag(self) -> None:
        """Test del flag --verbose."""
        result = self.runner.invoke(cli, ["nombre", "argumento", "--verbose"])
        assert result.exit_code == 0

    def test_edge_case(self) -> None:
        """Test de edge case específico."""
        result = self.runner.invoke(cli, ["nombre", ""])
        assert result.exit_code != 0

Reglas

  • Mínimo 3 tests por comando
  • Usar CliRunner para invocar comandos (no subprocess)
  • Nombres descriptivos: test_[acción]_[escenario]
  • Cada test verifica exit_code Y output
  • Si el comando modifica archivos, usar tmp_path fixture

**Para TypeScript — `.claude/skills/add-tests/SKILL.md`:**

```markdown
# Generar tests para comando

Crea tests para un comando CLI específico usando vitest.

## Instrucciones

1. Identifica el comando a testear en src/commands/
2. Analiza los argumentos, opciones, y lógica del comando
3. Crea el archivo tests/[commandName].test.ts
4. Testea la lógica del comando directamente (no subprocess)
5. Incluye tests de:
   - Happy path (uso normal con argumentos válidos)
   - Argumentos faltantes o inválidos
   - Opciones/flags (--verbose, etc.)
   - Edge cases específicos del comando
6. Ejecuta los tests: npm test -- tests/[commandName].test.ts

## Template de tests

```typescript
import { describe, it, expect, beforeEach } from "vitest";

describe("nombre command", () => {
  it("should handle basic usage correctly", () => {
    const result = processCommand("valid-input");
    expect(result).toBeDefined();
    expect(result.status).toBe("success");
  });

  it("should throw on missing argument", () => {
    expect(() => processCommand("")).toThrow();
  });

  it("should handle verbose flag", () => {
    const result = processCommand("input", { verbose: true });
    expect(result.details).toBeDefined();
  });

  it("should handle edge case", () => {
    const result = processCommand("edge-case-input");
    expect(result.status).toBe("expected");
  });
});

Reglas

  • Mínimo 3 tests por comando
  • Testear la lógica, no el CLI wrapper
  • Nombres descriptivos en inglés
  • Usar describe/it pattern
  • Si el comando modifica archivos, usar fs temporal con cleanup

---

### Paso 5: Configurar Hooks

Los hooks se configuran en `.claude/settings.json`. Ejecutan scripts automáticamente en puntos específicos del ciclo de vida de Claude Code.

Crearás 2 hooks:

1. **PostToolUse (Write):** Ejecuta el linter después de cada archivo escrito
2. **PreToolUse (Execute → git commit):** Ejecuta tests antes de cada commit

#### Crear `.claude/settings.json`

**Para Python:**

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "command": "ruff check --fix $CLAUDE_FILE_PATH 2>/dev/null || true"
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Execute",
        "command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -q 'git commit'; then pytest --tb=short -q 2>/dev/null; fi"
      }
    ]
  }
}

Para TypeScript:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "command": "npx eslint --fix $CLAUDE_FILE_PATH 2>/dev/null || true"
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Execute",
        "command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -q 'git commit'; then npm test -- --run 2>/dev/null; fi"
      }
    ]
  }
}

Qué hace cada hook

Hook 1 — PostToolUse (Write) → Linter:

Claude escribe un archivo
    │
    ▼
Hook se dispara (matcher: "Write")
    │
    ▼
Ejecuta ruff check / eslint en el archivo
    │
    ▼
Si hay errores de estilo → se corrigen automáticamente (--fix)
Si hay errores de lógica → Claude ve el output y los corrige

Hook 2 — PreToolUse (Execute → git commit) → Tests:

Claude intenta ejecutar "git commit"
    │
    ▼
Hook se dispara (matcher: "Execute", input contiene "git commit")
    │
    ▼
Ejecuta pytest / npm test
    │
    ▼
Tests pasan → commit procede
Tests fallan → Claude ve el error, corrige, y reintenta

Crear settings.local.json (opcional)

Para settings personales que no quieres compartir con el equipo:

{
  "permissions": {
    "allow": [
      "Write",
      "Read",
      "Glob",
      "Grep"
    ]
  }
}

Este archivo va en .gitignore — no se versiona.


Paso 6: Configurar el proyecto base

Antes de pasar a la siguiente cápsula, necesitas el proyecto base configurado para poder instalar dependencias.

Para Python — crea pyproject.toml:

[build-system]
requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.backends._legacy:_Backend"

[project]
name = "my-cli"
version = "0.1.0"
description = "CLI tool built with Claude Code"
requires-python = ">=3.8"
dependencies = [
    "click>=8.1",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0",
    "ruff>=0.4",
]

[project.scripts]
my-cli = "src.cli:cli"

[tool.ruff]
target-version = "py312"
line-length = 88

[tool.pytest.ini_options]
testpaths = ["tests"]

Para TypeScript — crea package.json:

{
  "name": "my-cli",
  "version": "0.1.0",
  "description": "CLI tool built with Claude Code",
  "type": "module",
  "main": "src/index.ts",
  "scripts": {
    "build": "tsc",
    "start": "npx ts-node src/index.ts",
    "test": "vitest",
    "lint": "eslint src/",
    "format": "prettier --write src/"
  },
  "dependencies": {
    "commander": "^12.0.0"
  },
  "devDependencies": {
    "typescript": "^5.4.0",
    "ts-node": "^10.9.0",
    "vitest": "^1.6.0",
    "eslint": "^9.0.0",
    "prettier": "^3.2.0",
    "@types/node": "^20.0.0"
  }
}

Crea también tsconfig.json si elegiste TypeScript:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "tests"]
}

Paso 7: Crear el entry point base

No vas a implementar los comandos todavía — eso viene en la Cápsula 04. Pero necesitas el entry point para que la CLI se pueda ejecutar.

Para Python — crea src/cli.py:

"""Entry point de la CLI."""

import click


@click.group()
@click.version_option(version="0.1.0")
def cli() -> None:
    """[Nombre de tu CLI] - [descripción breve]."""
    pass


if __name__ == "__main__":
    cli()

Para TypeScript — crea src/index.ts:

import { Command } from "commander";

const program = new Command();

program
  .name("my-cli")
  .description("[Descripción breve de tu CLI]")
  .version("0.1.0");

program.parse();

Paso 8: Instalar dependencias

Para Python:

pip install -e ".[dev]"

Para TypeScript:

npm install

Verifica la instalación:

Python:

python -m src.cli --help

TypeScript:

npx ts-node src/index.ts --help

Deberías ver el help text de tu CLI (vacío por ahora, sin comandos).


Paso 9: Crear .gitignore

# Python
__pycache__/
*.pyc
*.egg-info/
dist/
.venv/

# Node
node_modules/
dist/

# Claude Code
.claude/settings.local.json

# OS
.DS_Store

Nota que .claude/settings.local.json está en .gitignore pero .claude/settings.json y .claude/skills/ no — esos se versionan con git.


Paso 10: Verificar el setup completo

Antes de pasar a la siguiente cápsula, verifica que todo está en su lugar.

Checklist de archivos

my-cli-project/
├── CLAUDE.md                          ✅
├── .claude/
│   ├── skills/
│   │   ├── create-command.md          ✅
│   │   └── add-tests.md              ✅
│   └── settings.json                  ✅
├── src/
│   ├── __init__.py (Python)           ✅
│   └── cli.py / index.ts             ✅
├── tests/
│   └── __init__.py (Python)           ✅
├── pyproject.toml / package.json      ✅
├── .gitignore                         ✅
└── tsconfig.json (TypeScript)         ✅

Verifica con:

find . -not -path './node_modules/*' -not -path './.git/*' -not -path './__pycache__/*' | sort

Verificar Claude Code

Abre Claude Code en el directorio del proyecto:

claude

Claude debería leer tu CLAUDE.md automáticamente. Verifica con un prompt simple:

> ¿Qué proyecto es este? ¿Qué stack usa?

Claude debería responder con la información de tu CLAUDE.md. Si no, verifica que el archivo se llama exactamente CLAUDE.md (mayúsculas) y está en la raíz del proyecto.

Verificar Skills

Prueba invocar un skill:

> /create-command

Claude debería reconocer el skill y pedir el nombre del comando. No implementes nada todavía — solo verifica que el skill se carga. Sal de la sesión con /exit o Ctrl+C.

Verificar Hooks

Para verificar que los hooks están configurados, puedes revisar el archivo directamente:

cat .claude/settings.json

Los hooks se verificarán en acción durante la Cápsula 04 cuando Claude empiece a escribir archivos y hacer commits.


Código completo comentado

Resumen de archivos creados

ArchivoPropósitoLíneas
CLAUDE.mdContexto persistente para Claude Code~50
.claude/skills/create-command/SKILL.mdSkill para crear comandos CLI~40
.claude/skills/add-tests/SKILL.mdSkill para generar tests~40
.claude/settings.jsonHooks de linter y tests~15
pyproject.toml / package.jsonConfiguración del proyecto~25
src/cli.py / src/index.tsEntry point de la CLI~12
.gitignoreArchivos a ignorar en git~15

Total: ~200 líneas de configuración. Suena a mucho, pero cada archivo tiene un propósito claro y lo configuraste una sola vez.


Troubleshooting

"Claude no lee mi CLAUDE.md"

Causa: El archivo no se llama exactamente CLAUDE.md (en mayúsculas) o no está en la raíz del proyecto.

Solución:

ls -la CLAUDE.md

Si ves claude.md, Claude.md, o similar, renómbralo:

mv claude.md CLAUDE.md

"El skill no se reconoce"

Causa: El archivo no está en .claude/skills/ o tiene extensión incorrecta.

Solución:

ls -la .claude/skills/

Verifica que los archivos terminan en .md y están en el directorio correcto.

"Los hooks no se disparan"

Causa: El archivo settings.json no está en .claude/ o tiene un error de sintaxis JSON.

Solución:

cat .claude/settings.json | python -m json.tool

Si hay un error de JSON, la salida te dirá dónde está. Los errores más comunes:

  • Coma después del último elemento de un array
  • Comillas dobles faltantes
  • Llaves sin cerrar

"pip install falla"

Causa: No tienes pyproject.toml configurado correctamente o falta setuptools.

Solución:

pip install --upgrade pip setuptools wheel
pip install -e ".[dev]"

"npm install falla"

Causa: Versión de Node.js incompatible o package.json con errores.

Solución:

node --version  # Debe ser 18+
npm cache clean --force
npm install

"El linter no está instalado"

Causa: No instalaste las dependencias de desarrollo.

Para Python:

pip install ruff

Para TypeScript:

npm install --save-dev eslint

"ruff / eslint no encuentra el archivo en el hook"

Causa: La variable $CLAUDE_FILE_PATH no resuelve correctamente.

Solución temporal: Cambia el hook para ejecutar el linter en todo el directorio src:

Python:

{
  "matcher": "Write",
  "command": "ruff check --fix src/ 2>/dev/null || true"
}

TypeScript:

{
  "matcher": "Write",
  "command": "npx eslint --fix src/ 2>/dev/null || true"
}

Checklist de completitud

Antes de avanzar a la Cápsula 03, verifica:

  • Directorio del proyecto creado con git init
  • CLAUDE.md profesional con las 6 secciones
  • Skill /create-command creado en .claude/skills/
  • Skill /add-tests creado en .claude/skills/
  • Hooks configurados en .claude/settings.json
  • Entry point de la CLI creado (cli.py / index.ts)
  • Dependencias instaladas (pip install / npm install)
  • CLI ejecutable con --help (sin comandos todavía)
  • .gitignore configurado
  • Claude Code lee CLAUDE.md correctamente

Si todo está marcado, estás listo para la Fase 2.


Resumen

Lo que hiciste en esta cápsula:

  1. Creaste el proyecto con la estructura de directorios correcta
  2. Escribiste CLAUDE.md con las 6 secciones profesionales adaptadas a tu CLI
  3. Creaste 2 skills: /create-command (crear comandos nuevos) y /add-tests (generar tests)
  4. Configuraste 2 hooks: linter automático después de Write, tests antes de commit
  5. Configuraste el proyecto base con dependencias y entry point

Todo esto antes de escribir una sola línea de código funcional. Esa es la diferencia entre un usuario casual y un profesional: el profesional configura su entorno antes de implementar.

Siguiente cápsula: 03 - Explorar y planificar — donde abrirás Claude Code y usarás Explore y Plan para diseñar tu CLI antes de implementarla.