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_threecon 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
| Archivo | Propósito | Líneas |
|---|---|---|
CLAUDE.md | Contexto persistente para Claude Code | ~50 |
.claude/skills/create-command/SKILL.md | Skill para crear comandos CLI | ~40 |
.claude/skills/add-tests/SKILL.md | Skill para generar tests | ~40 |
.claude/settings.json | Hooks de linter y tests | ~15 |
pyproject.toml / package.json | Configuración del proyecto | ~25 |
src/cli.py / src/index.ts | Entry point de la CLI | ~12 |
.gitignore | Archivos 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-commandcreado en.claude/skills/ - Skill
/add-testscreado 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) -
.gitignoreconfigurado - 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:
- Creaste el proyecto con la estructura de directorios correcta
- Escribiste CLAUDE.md con las 6 secciones profesionales adaptadas a tu CLI
- Creaste 2 skills:
/create-command(crear comandos nuevos) y/add-tests(generar tests) - Configuraste 2 hooks: linter automático después de Write, tests antes de commit
- 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.