Creando Skills Potentes de Claude Code con npx bun
Actualización (15 de enero de 2026): Después de más pruebas, he descubierto que la detección de
node_modulesde Bun puede romper la instalación automática en situaciones inesperadas. Ahora recomiendo Deno para este caso de uso. El enfoque que se describe a continuación sigue funcionando para scripts independientes, pero consulta Por qué cambié de Bun a Deno para Skills de Claude Code para la explicación completa.
Las skills de Claude Code te permiten extender la CLI de codificación
agéntica de Anthropic con instrucciones personalizadas y scripts ejecutables.
Pero, ¿qué pasa si tu script necesita paquetes npm de terceros como lodash,
zod o csv-parse? Sin un paso de compilación ni carpeta node_modules,
las importaciones fallan. Esta guía muestra cómo usar npx bun para escribir
skills TypeScript autocontenidas que autoinstalan dependencias en tiempo de
ejecución—sin necesidad de package.json.
El problema: las
skills de Claude Code
no tienen paso de compilación (build step). No puedes simplemente hacer
import lodash from 'lodash' y esperar que funcione. El script se ejecuta en un
entorno limpio sin carpeta node_modules.
Lo que quería: uv run para JavaScript
Python resuelve esto elegantemente con uv. Declaras las dependencias en línea (inline) usando metadatos PEP 723:
#!/usr/bin/env -S uv run# /// script# dependencies = ["requests", "rich"]# requires-python = ">=3.10"# ///import requestsfrom rich import printprint(requests.get("https://example.com"))
Ejecútalo con uv run script.py o simplemente ./script.py. Las dependencias
se instalan automáticamente en un entorno aislado. Sin requirements.txt, sin
gestión de entornos virtuales, sin paso de compilación. El script es
autocontenido.
Python fue mi primer lenguaje de programación profesional, y todavía admiro cómo
ha evolucionado su ecosistema. Pero mi equipo en
Buffer trabaja en TypeScript. Necesitaba algo que
fuera fácil de adoptar para mis compañeros—paquetes npm familiares, sintaxis
familiar—pero tan flexible y potente como uv run.
La Restricción
Una skill típica de Claude Code se ve así:
my-skill/├── SKILL.md└── scripts/└── process.js
Claude lee SKILL.md para obtener las instrucciones y ejecuta los scripts
cuando es relevante. Pero si process.js importa algún paquete npm, falla. Sin
package.json, sin node_modules, sin dependencias.
Las soluciones obvias—incluir node_modules en el commit o ejecutar
npm install en tiempo de ejecución—son feas. La primera infla la carpeta de tu
skill. La segunda añade latencia cada vez.
Exploré varios enfoques.
Enfoque 1: Pre-empaquetado con esbuild
Empaqueta tu script en un solo archivo con todas las dependencias incluidas:
esbuild script.ts --bundle --platform=node --outfile=script.mjs
La salida es autocontenida. Sin dependencias en tiempo de ejecución. Envíalo con tu skill y Claude lo ejecuta directamente.
Esto funciona, pero requiere un paso de compilación. Debes reconstruir después de cada cambio. Depurar código empaquetado es más difícil. Es lo opuesto a lo que quería.
Enfoque 2: Importaciones dinámicas con esm.sh
esm.sh sirve paquetes npm como módulos ES a través de HTTPS:
import _ from "https://esm.sh/lodash @4.17.21";import { z } from "https://esm.sh/zod @3.23.0";
No requiere instalación. El entorno de ejecución (runtime) obtiene el módulo en el primer uso. La versión fijada vive en la URL.
El problema: Node.js no soporta nativamente importaciones HTTPS sin flags experimentales o loaders personalizados. Algunos paquetes no funcionan bien como ESM puro. La latencia de red en cada arranque en frío se acumula.
Enfoque 3: Google zx con --install
zx es la herramienta de Google para escribir
scripts de shell en JavaScript. Envuelve child_process y añade comodidades
como el literal de plantilla $ para ejecutar comandos.
El flag --install auto-instala las dependencias faltantes:
#!/usr/bin/env zximport _ from "lodash"; // @^4.17import { parse } from "yaml"; // @^2.0await $`echo "Dependencies auto-installed"`;
Ejecútalo con npx zx --install script.mjs. En la primera ejecución, zx
detecta las importaciones, instala los paquetes y los almacena en caché.
Esto se acerca más a lo que quería. Pero fijar versiones a través de comentarios se siente como un parche. Y no hay soporte nativo de TypeScript—necesitarías tsx o similar.
Enfoque 4: Bun
Bun toma un enfoque diferente. La auto-instalación está integrada en el runtime. Escribe importaciones normales y Bun maneja el resto:
#!/usr/bin/env bunimport _ from "lodash";import { z } from "zod@^3.20";import chalk from "chalk@^5.0.0";console.log(chalk.green("Dependencies just work"));
La fijación de versiones ocurre directamente en la ruta de importación—más limpio que la sintaxis de comentarios de zx. TypeScript se ejecuta nativamente. El arranque es rápido.
La trampa: Puede que Bun no esté instalado en todos los entornos. Los entornos de Claude Code tienen Node.js y npm, pero no necesariamente Bun.
El Descubrimiento: npx bun
Entonces me di cuenta: No necesito Bun instalado globalmente. Solo necesito npm.
npx -y bun script.ts
El flag -y salta el aviso de confirmación, lo cual importa para ejecuciones no
interactivas. Esto funciona porque bun está publicado como un paquete npm.
Cuando ejecutas npx bun, npm descarga el binario de Bun y ejecuta tu script.
Obtienes la auto-instalación de Bun, soporte de TypeScript y velocidad—todo a
través de la cadena de herramientas de npm/Node.js que ya está en todas partes.
Probé esto en un entorno limpio:
import chalk from "chalk @^5.0.0";import { z } from "zod @3.23.0";import _ from "lodash @^4.17.0";console.log(chalk.green("✓ chalk loaded"));const schema = z.object({ name: z.string() });console.log(chalk.blue(`✓ zod loaded - validation works`));const grouped = _.groupBy(["one", "two", "three"], "length");console.log(chalk.yellow(`✓ lodash loaded`));
Salida:
✓ chalk loaded✓ zod loaded - validation works✓ lodash loaded
Sin package.json. Sin node_modules. Sin paso de compilación. La primera
ejecución instala dependencias en la caché global de Bun. Las ejecuciones
subsiguientes son instantáneas.
Este es el equivalente en JavaScript de uv run. La misma experiencia de
desarrollador, los mismos scripts autocontenidos, ecosistema npm familiar.
Haciendo Scripts Directamente Ejecutables
Mejora aún más. Al igual que con #!/usr/bin/env -S uv run de Python, puedes
usar un shebang para hacer los scripts directamente ejecutables:
#!/usr/bin/env -S npx -y bunimport chalk from "chalk@^5.0.0";console.log(chalk.green("Hello!"));
El flag -S le dice a env que divida la cadena en argumentos separados. Haz
el script ejecutable y ejecútalo directamente:
chmod +x script.ts./script.ts
Ahora tienes scripts TypeScript autocontenidos—sin invocación explícita necesaria.
Usando Esto en Skills de Claude Code
Estructura tu skill así:
my-skill/├── SKILL.md└── scripts/└── process.ts
En SKILL.md:
---name: data-processordescription: Process and transform data files using advanced librariesallowed-tools: [Bash, Read, Write]---# Data ProcessorRun the processing script:```bash./scripts/process.ts <input-file>```
En scripts/process.ts:
import { parse } from "csv-parse/sync @^5.0";import * as XLSX from "xlsx @^0.20";const [, , inputPath] = Bun.argv;const file = Bun.file(inputPath);const content = await file.text();const rows = parse(content, { columns: true });console.log(JSON.stringify(rows, null, 2));
Claude ejecuta la skill, el script se ejecuta con acceso completo a paquetes
npm, y nunca tocas un package.json.
Comparación
| Enfoque | Paso de Compilación | TypeScript | Fijación de Versión | Velocidad Primera Ejecución |
|---|---|---|---|---|
| esbuild bundle | Sí | Vía compilación | En código fuente | Rápida |
| esm.sh | No | No | En URL | Limitada por red |
| npx zx --install | No | Vía tsx | Comentarios | Moderada |
| npx -y bun | No | Nativo | En ruta de importación | Rápida tras caché |
Advertencias
Este enfoque no es perfecto. Algunas cosas a considerar:
Auto-install requiere que no exista un directorio node_modules. La función
de auto-instalación de Bun solo funciona cuando no se encuentra ningún
directorio node_modules en el directorio de trabajo ni en ningún directorio
padre.1 Cuando existe una carpeta node_modules—común en monorepos o
proyectos existentes—Bun cambia a la resolución de módulos estilo Node.js en
lugar de su algoritmo de auto-instalación. Incluso el flag --install=force no
resuelve esto completamente: los especificadores de versión en imports (como
import { z } from "[email protected]") arrojarán un error
VersionSpecifierNotAllowedHere cuando node_modules está presente. Esto
significa que el enfoque funciona mejor para scripts independientes fuera de
proyectos existentes. Para skills de Claude Code almacenadas en
~/.claude/skills/, esto típicamente no es un problema. Pero si estás
escribiendo scripts dentro de un directorio de proyecto con node_modules,
necesitarás usar un package.json tradicional o mover el script fuera del árbol
del proyecto.
Bun no es totalmente compatible con Node.js. La mayoría de paquetes npm
funcionan bien, pero algunas APIs de Node.js se comportan diferente o no están
implementadas aún. Si tu script depende de comportamientos extremos
(edge-case) de Node.js—ciertas operaciones de fs, opciones específicas de
child_process, addons nativos—podrías encontrar problemas inesperados. Revisa
la
documentación de compatibilidad de Bun con Node.js
antes de comprometerte con este enfoque.
La latencia de la primera ejecución existe. La primera ejecución descarga Bun vía npx (~100MB dependiendo de la arquitectura) e instala dependencias. En una conexión lenta o en un entorno de arranque en frío, esto añade un tiempo notable. Las ejecuciones subsiguientes son rápidas, pero ese impacto inicial importa si tu skill se ejecuta en entornos efímeros que no preservan la caché de Bun.
La fijación de versiones en importaciones no es estándar. La sintaxis
import x from "pkg @^1.0" es específica de Bun. Tu IDE no la entenderá para
autocompletado o chequeo de tipos. Para scripts rápidos, puedes añadir
// @ts-ignore sobre las importaciones problemáticas. Para un desarrollo más
serio, mantén un package.json con versiones adecuadas y solo usa la sintaxis
en línea (inline) en la skill desplegada.
Cuándo usar zx en su lugar. Si necesitas compatibilidad garantizada con Node.js—porque estás usando un paquete que depende de internos específicos de Node, o tu equipo tiene requisitos estrictos de runtime—zx con --install es la opción más segura. Se ejecuta en Node.js directamente, así que la compatibilidad nunca es una duda. El compromiso es no tener TypeScript nativo y la fijación de versiones basada en comentarios.
Para la mayoría de skills que usan paquetes comunes como lodash, zod o csv-parse, Bun funciona bien. Pero ten en cuenta que existe esa vía de escape.
Conclusión
npx -y bun combina las mejores propiedades: sin paso de compilación,
TypeScript nativo, fijación de versiones limpia y disponibilidad donde sea que
npm se ejecute. Para skills de Claude Code que necesitan paquetes de terceros,
es el camino más simple hacia scripts potentes—mientras te mantengas dentro de
los límites de compatibilidad de Bun.
Si has usado uv de Python y deseabas que JavaScript tuviera algo similar, es esto. Misma filosofía, mismo flujo de trabajo, herramientas familiares. Y cuando llegues a los límites de Bun, zx está ahí como respaldo.
Referencias
- Documentación de Skills de Claude Code
- Bun — El runtime de JavaScript con auto-instalación integrada
- Resolución de Módulos de Bun — Entendiendo cómo Bun resuelve módulos
- Google zx — Una herramienta para escribir mejores scripts
- esm.sh — Paquetes npm como módulos ES a través de CDN
- uv — Gestor de paquetes de Python con
dependencias de script en línea (
uv run) - PEP 723 — Especificación de metadatos de script en línea para Python
- esbuild — Empaquetador de JavaScript rápido
Footnotes
-
Documentación de Auto-Install de Bun — "If no node_modules directory is found in the working directory or higher, Bun will abandon Node.js-style module resolution in favor of the Bun module resolution algorithm." ↩