Knip: código muerto cuando los agentes escriben más rápido
Los agentes de IA aceleran cuánto código generamos. Knip ayuda a detectar archivos, exports y dependencias que ya nadie usa, y a convertir esa higiene en una regla automática del proyecto.
A lo largo de los años me he encontrado muchas veces con bases de código que acumulan funciones cuya utilidad nadie recuerda con claridad, variables abandonadas, exports que siguen existiendo aunque nadie los importe y archivos que permanecen en el proyecto simplemente porque nunca hubo una razón concreta para borrarlos.
No siempre es descuido. Es el resultado natural de iterar: se reemplaza una abstracción, se mueve una responsabilidad, se deja un util “por si acaso”. Con agentes de IA eso se acelera: el código crece más rápido, y el que nadie limpia también. Con el tiempo, el repositorio mezcla lo que la aplicación usa con lo que alguna vez usó.
TypeScript y los IDEs modernos ayudan con una parte de esto. Es sencillo detectar visualmente variables, imports o ciertas funciones que no se están usando. Los linters también permiten señalar código no utilizado dentro de un archivo.
Aun así, siempre tuve la sensación de que faltaba una respuesta para una pregunta más amplia:
¿Qué partes de mi proyecto siguen siendo realmente necesarias?
Una cosa es saber que una variable no se utiliza dentro de un archivo. Otra distinta es analizar las relaciones entre archivos, exports, entradas del proyecto y dependencias para determinar qué elementos forman parte del código utilizado.
Aquí es donde conocí Knip
Knip aborda precisamente esa segunda parte del problema.
Según su documentación, construye un grafo del proyecto a partir de los archivos de entrada y sigue las relaciones entre imports, exports y archivos. A partir de ese grafo puede identificar archivos que nadie alcanza, exports que ningún otro archivo importa y dependencias que no están siendo utilizadas.
Eso cambia la forma de pensar el problema. Ya no preguntamos únicamente “¿esta variable se utiliza?”, sino “¿este elemento sigue formando parte de algo que realmente utiliza mi aplicación?”.
Esa diferencia importa. Knip no reemplaza a ESLint, TypeScript u otras herramientas. De hecho, su documentación deja claro que no está orientado a detectar variables e imports no utilizados dentro de un mismo archivo; para eso recomienda herramientas como ESLint, Biome u oxlint. Knip opera a otra escala: la del proyecto completo.
Cuando lo integré en un proyecto propio, esa distinción se volvió concreta. TypeScript me avisaba de lo local. Knip me mostraba lo estructural: archivos que nadie alcanzaba desde las entradas, exports que nadie importaba y dependencias que ya no tenían consumidores.
También me obligó a leer bien los falsos positivos. Si Knip marca algo como no usado, casi siempre está diciendo: “desde las entradas que conozco, nadie llega hasta aquí”. A veces el hallazgo es real; a veces falta enseñarle el límite correcto del grafo —entradas, plugins o excepciones específicas, como exports de una librería de UI que todavía no se consumen todos—.
Código muerto y exports sin utilizar
Conviene no simplificar de más el impacto de un export sin uso.
No es correcto afirmar, sin matices, que “un export de más hace que la aplicación sea más lenta”. El efecto depende de cómo se compile, se empaquete y se ejecute el código.
La documentación de Knip señala que los exports no utilizados pueden aumentar el tamaño del bundle cuando no se eliminan correctamente mediante tree-shaking, además de generar ruido en la base de código, dificultar el mantenimiento y la navegación, e incrementar el trabajo de herramientas que analizan el código.
Para razonarlo con más precisión, ayuda separar capas:
- Código en el repositorio. Existe, se versiona, se lee en PRs y forma parte de la comprensión del sistema.
- Código que entra al proceso de compilación. TypeScript, el bundler y otras herramientas pueden tener que parsearlo, tiparlo o resolverlo aunque nadie lo ejecute.
- Código que llega al bundle. Solo una parte del grafo de producción termina empaquetada.
- Código que el tree-shaking puede eliminar. Un bundler moderno intenta descartar exports no referenciados, pero es conservador: si no puede probar que algo es seguro de eliminar, suele conservarlo.
- Código que afecta al runtime. Lo que finalmente se descarga y se ejecuta en el cliente o en el servidor.
Tree-shaking ayuda, pero no convierte el código muerto en un problema irrelevante. Knip lo deja claro en su FAQ: tree-shaking es una optimización de build; Knip es un linter de proyecto. El primero puede reducir el bundle. El segundo ataca el costo de mantener y auditar código que el repositorio ya no necesita.
Lo que más me interesa: convertirlo en una regla
Lo que más me interesó de Knip no fue solo encontrar código muerto, sino poder convertirlo en una condición automática: comandos locales, Git hooks, CI/CD o GitHub Actions.
La documentación de Knip recomienda usarlo en CI precisamente para evitar que regresen con el tiempo dependencias, exports o archivos que debieron eliminarse. Además, puede finalizar con código de salida 1 cuando encuentra problemas.
Esto conecta con una idea que ya desarrollé en Git Auditor: hooks nativos con shell para auditoría de equipo: las buenas prácticas duran más cuando dejan de ser recordatorios y se convierten en reglas automáticas. Un hook o un job de CI no reemplaza el criterio humano; reduce la probabilidad de que una limpieza quede “para después”.
Knip también ofrece auto-fix (--fix, y --allow-remove-files cuando corresponde). Eso acelera la limpieza, pero no elimina la necesidad de revisar. Su propia documentación advierte contra ejecutar auto-fix antes de tener una configuración sólida: si el grafo está incompleto, puedes borrar código que todavía se usa.
El contexto de los agentes de IA
Hoy usamos cada vez más agentes de inteligencia artificial para escribir, modificar y refactorizar código. Eso cambia la velocidad a la que una base de código puede crecer.
Un agente puede crear una abstracción, reemplazarla después por otra, modificar una arquitectura o introducir funciones nuevas sin que necesariamente todo el código anterior se elimine. El resultado es una base de código que evoluciona más rápido, y que también puede acumular código innecesario más rápido.
Si delegamos más escritura de código a agentes, también necesitamos más mecanismos automáticos para auditarlo. No se trata de confiar a ciegas ni de revisar cada línea a mano: los agentes pueden ir rápido, y otras herramientas verifican que el resultado siga cumpliendo reglas de calidad.
Automatizar antes que confiar en la memoria
Mantener una base de código limpia no debería depender únicamente de que alguien recuerde hacer limpieza.
Durante años añadimos linters, formateadores, tests y distintos mecanismos de validación a nuestros proyectos precisamente para convertir buenas prácticas en reglas automáticas. Creo que el código muerto debería tratarse de la misma manera.
Y eso se vuelve todavía más relevante cuando trabajamos con agentes de IA.
Si los agentes nos permiten escribir código más rápido, también necesitamos herramientas que nos permitan detectar automáticamente el código que ya no necesitamos.
La velocidad de generación de código no debería traducirse en una velocidad equivalente de acumulación de deuda técnica.
El objetivo no es solamente escribir más código.
Es poder escribir, modificar y eliminar código con la misma confianza.
Cómo integrarlo
La parte práctica puede quedar al final: instalar, configurar y enchufar Knip al flujo del proyecto.
Instalación y configuración básica
La forma más directa de empezar, según la documentación oficial, es inicializar Knip con el generador de configuración:
pnpm create @knip/config
# o: npm init @knip/configTambién puedes instalarlo a mano como dependencia de desarrollo y añadir un script:
pnpm add -D knip typescript @types/node{
"scripts": {
"knip": "knip"
}
}La configuración suele vivir en knip.json, knip.config.ts o campos equivalentes. Lo esencial es declarar las entradas del grafo (entry), el alcance del proyecto (project) y, cuando haga falta, excepciones puntuales:
import type { KnipConfig } from 'knip';
const config: KnipConfig = {
entry: ['src/lib/i18n/request.ts'],
project: ['src/**/*.{js,ts,jsx,tsx}'],
ignoreDependencies: ['lint-staged', 'tailwindcss'],
ignoreIssues: {
'./src/components/ui/**': ['files', 'exports', 'types'],
},
};
export default config;ignoreIssues es útil cuando quieres seguir analizando un directorio, pero silenciar tipos concretos de hallazgos —por ejemplo, exports de una librería de componentes que todavía no se consumen todos—. No es lo mismo que sacar esos archivos del grafo por completo.
Después de una primera pasada manual, conviene convertir Knip en un comando de auditoría (pnpm knip) y, cuando el reporte sea estable, en una condición de hooks o CI. Knip sale con código 1 si encuentra problemas, así que encaja bien como gate.
Knip en Edge Functions con Deno y Supabase
La mayoría de ejemplos de Knip asumen Node.js o Bun. En un backend de Edge Functions con Deno —como los de Supabase— el encaje es menos obvio. Knip todavía no tiene soporte nativo para Deno; aparece como una capacidad pedida en su FAQ. Aun así, se puede usar con un puente práctico.
El patrón que me funcionó en un backend Deno/Supabase se apoya en tres piezas:
- Correr Knip vía npm desde Deno, por ejemplo con un task deno run -A npm:knip.
- Mantener un package.json mínimo solo porque Knip espera encontrarlo. En un proyecto Deno real, ese archivo es un señuelo: no debería acumular dependencias ni scripts; la fuente de verdad sigue siendo deno.json.
- Declarar las Edge Functions como entradas del grafo, normalmente los index.ts de cada función, y mapear los alias compartidos (@shared/*) en la config de Knip.
Una configuración típica se ve así:
{
"entry": ["supabase/functions/**/index.ts"],
"project": ["supabase/functions/**/*.ts"],
"include": ["files", "dependencies", "exports", "unresolved"],
"paths": {
"@shared/*": ["./supabase/functions/_shared/*"]
}
}Y en deno.json, además del import map de dependencias npm (npm:zod, npm:@supabase/supabase-js, etc.), el task queda explícito:
{
"imports": {
"@shared/": "./supabase/functions/_shared/",
"npm:knip": "npm:knip"
},
"tasks": {
"knip": "deno run -A npm:knip"
}
}Eso permite auditar la capa de Edge Functions con la misma idea de siempre: partir de las entradas reales (**/index.ts), seguir imports hacia _shared y detectar archivos, exports o referencias que ya nadie alcanza. No es soporte Deno de primera clase, pero convierte Knip en una regla también en ese entorno —incluyendo hooks de pre-push o CI— sin mezclar el runtime Deno con un ecosistema Node inventado.
Un matiz: Knip analiza bien el grafo TypeScript y los exports no usados, pero las dependencias npm: de Deno no siempre se comportan como en un package.json convencional. Conviene acotar el reporte (include) y revisar los primeros falsos positivos antes de endurecer la regla.
También hay integración para agentes mediante MCP: su documentación recomienda el MCP Server para que los agentes puedan usar Knip y trabajar con su configuración.
Si te sirve, también tengo Knip en Resources, junto con otras herramientas que uso en el día a día.