Claude Code en monorepos: dale solo la rebanada que necesita
Un cliente me pasó su monorepo el mes pasado. Nueve paquetes, pnpm workspaces, Turborepo por encima. Le pedí a Claude Code algo ridículamente pequeño: cambiar el tipo de una prop en packages/ui.
Tres respuestas después me estaba proponiendo tocar el cliente HTTP del backend.
No era un modelo tonto. Era yo. Había arrancado la sesión desde la raíz del repo, y trabajar con Claude Code en monorepos desde la raíz significa una cosa muy concreta: le has dado nueve paquetes de superficie para una tarea que vive en uno.
Esto no es el problema del que ya escribí en Context Drift. Aquel es temporal: la sesión se alarga, el historial se pudre, el agente se olvida de la instrucción de la iteración 3. Este es espacial. Se degrada en el minuto uno, con la ventana medio vacía, porque el repo es grande y nadie le ha dicho qué parte del repo importa.
La tesis del post es esta: en un monorepo, la decisión más importante que tomas no es qué prompt escribes. Es desde qué directorio arrancas el agente.
Smart context slicing es la práctica de arrancar el agente en el subárbol mínimo del monorepo que la tarea necesita, calculado a partir del grafo de dependencias en vez de a ojo. Son tres decisiones concretas: desde qué directorio lanzas claude, qué paquetes vecinos añades con --add-dir y qué rutas bloqueas con reglas de denegación. Las tres, en ese orden, son el resto del post.
Claude Code en monorepos: un CLAUDE.md en la raíz no escala
La documentación de Anthropic recomienda mantener cada CLAUDE.md por debajo de 200 líneas, y lo justifica: los archivos largos consumen más contexto y reducen la adherencia a las instrucciones.
Ahora divide. Nueve paquetes, 200 líneas: 22 líneas por paquete para explicar su stack, sus convenciones y sus trampas.
Así que solo hay dos finales, y he visto los dos.
O el CLAUDE.md crece hasta las 600 líneas y el agente ignora la mitad — incluidas las reglas que importaban. O se queda genérico ("usa TypeScript estricto", "escribe tests"), que es una forma elegante de no decir nada.
Si todavía estás montando el tuyo, el punto de partida lo dejé en CLAUDE.md: el system prompt de tu proyecto. Aquí doy por hecho que ya lo tienes y se te ha quedado pequeño.
La solución es partirlo: raíz para lo global, un archivo por paquete para lo local.
monorepo/
CLAUDE.md # reglas globales: commits, estilo, "corre los scripts desde el paquete"
packages/
ui/CLAUDE.md # convenciones de componentes, tokens de diseño
api/CLAUDE.md # Knex, migraciones, .env obligatorio
web/CLAUDE.md # rutas, data fetching
Pero partirlo no sirve de nada si no entiendes cuándo se carga cada trozo.
La regla de carga que casi nadie ha leído
Claude Code no trata igual a los CLAUDE.md que están por encima de ti y a los que están por debajo.
| Dónde vive el CLAUDE.md | Cuándo entra en contexto |
|---|---|
| Tu directorio de trabajo y todos sus ancestros | Al arrancar la sesión, siempre |
| Subdirectorios por debajo de ti | Bajo demanda, solo cuando el agente lee un archivo de esa carpeta |
Si arrancas desde la raíz, cargas solo el CLAUDE.md raíz — y vas acumulando el de cada paquete que el agente toque. Toca muchos, porque no sabe dónde está el límite.
Si arrancas con cd packages/ui && claude, cargas raíz + packages/ui de golpe, y los de api y web no existen para esa sesión mientras no los pises. Además, solo puede leer y editar dentro de ese subárbol hasta que le concedas más.
Eso es una rebanada. Y te ha costado un cd.
Compruébalo: lanza /context y mira la lista de Memory files. Ahí está lo que se cargó de verdad.
El slice no lo decides tú: lo decide el grafo de dependencias
"Trabaja desde el paquete" está bien hasta que la tarea toca de verdad a los vecinos. Cambiar un tipo exportado de ui puede romper a quien lo consume, y si el agente no ve a esos consumidores, te entrega algo que compila en su rebanada y revienta en CI.
La pregunta correcta no es qué paquetes te apetece abrir, sino qué paquetes toca esta tarea de verdad. Y esa respuesta ya está en tu repo: en el grafo de dependencias.
Monté un workspace de cinco paquetes para verlo, con pnpm 11.1.3 y Turborepo 2.10.12. @acme/api y @acme/web dependen de @acme/ui; @acme/ui depende de @acme/config; @acme/jobs va por libre.
Inventario primero:
pnpm ls -r --depth -1
Ahora el blast radius hacia arriba — qué se rompe si toco @acme/ui. En la sintaxis de filtros de pnpm, los tres puntos delante del nombre significan "y todo lo que depende de él":
pnpm --filter "...@acme/ui" ls --depth -1
# (salida recortada al nombre de cada paquete)
# @acme/ui
# @acme/api
# @acme/web
Y hacia abajo, con los puntos detrás, "y todo aquello de lo que depende":
pnpm --filter "@acme/ui..." ls --depth -1
# (salida recortada)
# @acme/ui
# @acme/config
Si quieres el cierre completo en los dos sentidos, pones los puntos a ambos lados: "...@acme/ui...". Y si te sobra el propio paquete, el circunflejo lo excluye: "...^@acme/ui" devuelve solo api y web.
Turborepo lo da con un matiz. --dry enseña el plan sin ejecutar nada:
turbo run build --filter="...@acme/ui" --dry
• Packages in scope: @acme/api, @acme/ui, @acme/web
• Running build in 3 packages
El detalle que solo ves ejecutándolo: "Packages in scope" son 3, pero si sacas el JSON aparecen 4 tareas:
turbo run build --filter="...@acme/ui" --dry=json | jq -r 39;.tasks[].directory39; | sort -u
# packages/api
# packages/config
# packages/ui
# packages/web
@acme/config no está en el scope de edición, pero entra en el grafo de build porque ui lo necesita compilado. Son dos rebanadas distintas y conviene no confundirlas:
| Rebanada | Paquetes | % del repo |
|---|---|---|
| Repo completo | 5 | 100% |
Slice de edición (ui + dependientes) |
3 | 60% |
Slice de build (añade config) |
4 | 80% |
Nunca entra (@acme/jobs) |
1 | 20% |
En un repo de cinco paquetes, dejar fuera un paquete suena a poco. En el del cliente, con nueve, el slice real de la tarea eran tres paquetes: dos tercios del repo que no tenían por qué abrirse nunca.
Con esa lista en la mano, el arranque deja de ser una corazonada:
cd packages/ui
claude --add-dir ../api --add-dir ../web
Si el equipo entero trabaja así, lo fijas en packages/ui/.claude/settings.json:
{
"permissions": {
"additionalDirectories": ["../api", "../web"]
}
}
Ojo con una diferencia que muerde: additionalDirectories da acceso a los ficheros pero no carga nunca el CLAUDE.md ni las skills de esos directorios. Con --add-dir sí cargan las skills, y el CLAUDE.md solo si arrancas con CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1. Si escribiste un CLAUDE.md en packages/api y no sale en /context, es por esto.
Y como el slice te dice qué se puede romper, también sabes qué verificar antes de dar la tarea por buena: los tests de api y web, no los de ui. Convertir "parece que funciona" en un veredicto ejecutable lo desarrollé entero en el ebook gratuito Revisión por Contrato, sobre cómo revisar lo que te entrega un agente sin leértelo línea a línea.
Lo que no debe entrar en la ventana bajo ningún concepto
Las búsquedas de contenido de Claude Code respetan tu .gitignore por defecto, así que node_modules/, dist/ y build/ ya están fuera de los resultados de un grep.
El problema es lo que sí está commiteado: código generado, un SDK vendorizado, snapshots enormes. Para eso hay reglas de denegación:
{
"permissions": {
"deny": [
"Read(./**/dist/**)",
"Read(./**/*.generated.*)",
"Read(./vendor/**)"
]
}
}
Un detalle que rompe esto sin avisar: los patrones relativos anclan en el directorio desde el que arrancas la sesión, no en la raíz del repo. Si guardas estas reglas en la raíz pero lanzas la sesión desde packages/ui, Read(./vendor/**) está apuntando a packages/ui/vendor/. Para que apliquen en todo el repo las escribes absolutas, con doble barra: Read(//ruta/absoluta/al/repo/vendor/**).
Y si arrancando desde la raíz se te cuelan los CLAUDE.md de equipos con los que no trabajas, existe claudeMdExcludes, en el .claude/settings.local.json de la raíz. Los patrones se comparan contra rutas absolutas, así que empiezan por **/ para que casen en cualquier punto del árbol:
{
"claudeMdExcludes": ["**/packages/legacy-*/**"]
}
Con un aviso honesto: esa lista es estática, no un interruptor por tarea. Para alternar de paquete cada día la herramienta sigue siendo el cd.
Cuando de verdad no sabes dónde está, delega la búsqueda
Todo lo anterior asume que sabes qué paquete tocar. A veces no lo sabes, y ahí es donde la gente destroza la sesión: "busca en el repo dónde se genera el token de refresco". El agente lee doscientos archivos y te devuelve una frase. Los doscientos archivos se quedan en tu ventana. La frase también, pero ya da igual.
Delégalo a un subagente. Corre en su propia ventana de contexto y te devuelve el resumen, no los archivos:
Usa un subagente para localizar en qué paquetes se genera y se valida
el token de refresco. Devuélveme solo la lista de rutas y una línea
por cada una. No propongas cambios todavía.
El resultado es una lista de paquetes. Cierras la sesión, haces cd al correcto y empiezas la tarea real con la ventana limpia. La exploración se paga una vez y se tira.
Es el mismo principio que conté en Context Engineering: lo caro no es el token, es el token irrelevante que se queda mirándote el resto de la sesión.
Lo que puedes hacer hoy en tu monorepo
Una sola cosa, y es gratis: deja de arrancar el agente desde la raíz del monorepo.
Antes de la próxima tarea, corre pnpm --filter "...<tu-paquete>" ls --depth -1, mira los tres o cuatro nombres que salen, y arranca así:
cd packages/<tu-paquete>
claude --add-dir ../<vecino>
No hace falta que escribas ni un CLAUDE.md nuevo para notar la diferencia. Eso viene después, cuando ya sepas qué reglas son globales y cuáles de un paquete — y eso solo se ve claro tras unos días trabajando por rebanadas.
Si quieres el flujo completo, de la idea al producto con estas decisiones tomadas antes de escribir código, es lo que montamos en el curso Construye con IA.
Preguntas frecuentes
¿Es mejor arrancar Claude Code desde la raíz del monorepo o desde el paquete?
Desde el paquete, salvo que la tarea cruce varios subsistemas de verdad. Arrancando desde packages/ui cargas el CLAUDE.md raíz más el de ui, y el agente solo puede leer y editar ese subárbol. Desde la raíz tienes acceso a todo: útil para refactors transversales, caro para cualquier otra cosa. Si necesitas un vecino puntual, --add-dir te lo añade sin romper el aislamiento.
¿Los CLAUDE.md de los subdirectorios se cargan siempre?
No, y esta es la confusión más habitual. Los de tu directorio de trabajo y de todos sus ancestros se cargan al arrancar la sesión. Los de subdirectorios por debajo de ti se cargan bajo demanda, solo cuando el agente lee un archivo de esa carpeta. Para ver qué se cargó de verdad en una sesión, lanza /context.
¿Qué hago si la tarea toca varios paquetes a la vez?
Dásela entera en una sola sesión, con el slice completo delante. Partirla en una sesión por paquete es peor: cada sesión redecide el diseño desde cero y acabas con tres criterios distintos. Calcula el slice con el filtro de dependientes, añade esos directorios y trabaja en plan mode antes de editar: el plan se escribe a un archivo que Claude Code reinyecta tras cada compactación.
¿Esto sirve si uso Nx o si mi repo es un solo árbol grande sin paquetes?
Sí. En Nx el equivalente es nx graph para ver el grafo y nx show projects --affected para saber qué proyectos toca un cambio: cambia el comando, no la idea. Y en un repo de un solo árbol sustituyes "paquete" por "subsistema" — src/billing/, src/auth/, lib/core/. Un CLAUDE.md por subsistema y un cd hacen el mismo trabajo.
¿No basta con el .gitignore para que el agente no lea dist?
Para las búsquedas de contenido sí: Claude Code respeta el .gitignore por defecto, así que dist/, build/ y node_modules/ no aparecen cuando busca texto. Lo que no cubre es lo commiteado — código generado, SDKs vendorizados, fixtures gigantes. Para eso necesitas reglas Read(...) en permissions.deny. Con un límite: cubren las herramientas de fichero y los comandos de Bash que Claude Code reconoce, pero un grep -r sobre una carpeta con ficheros denegados sigue sacándolos por pantalla.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
¿Te resultó útil este artículo?
Compártelo con tu comunidad y ayuda a otros desarrolladores.
