Why I Switched from Bun to Deno for Claude Code Skills
Bun's auto-install feature breaks when any node_modules directory exists in
parent paths. This makes Bun unreliable for portable Claude Code skills that
run from project directories or monorepos. After testing
my previous npx bun approach
in real environments, I switched to Deno. Here is why Deno's npm: specifier is
the better choice for self-contained TypeScript skills.
Bun's auto-install only works when no node_modules directory exists in the
working directory or any parent directory. When node_modules is present
anywhere up the tree, Bun switches to standard Node.js module resolution.
Version specifiers in imports—the core feature that made the approach
useful—throw VersionSpecifierNotAllowedHere errors:
$ cd ~/my-project # has node_modules/$ cat skill.ts#!/usr/bin/env -S npx -y bunimport chalk from "chalk@^5.0.0"console.log(chalk.green("Hello"))$ ./skill.tserror: VersionSpecifierNotAllowedHereimport chalk from "chalk@^5.0.0"^
This breaks in practical scenarios. Run a skill from within a project directory?
Broken. Work in a monorepo where some ancestor has node_modules? Broken. Your
home directory happens to have an old node_modules from a forgotten
experiment? Broken.
For portable Claude Code skills that might run from anywhere, this is a footgun.
The script works when you test it in ~/.claude/skills/, then fails
mysteriously when Claude invokes it from a different directory. The error
message obscures the problem—diagnosing it requires understanding Bun's internal
resolution logic.
Credit for the solution goes to J Edward Wynia, who pointed me toward Deno in response to that article. I forget why I skipped Deno initially—probably because Bun's syntax looked cleaner—but the suggestion was right.
Why Deno Solves This
Deno's npm: specifier works regardless of whether node_modules exists.
Dependencies always go to Deno's global cache at ~/.cache/deno. Local
node_modules directories don't affect resolution—though you need the
--node-modules-dir=false flag to ensure this behavior when running from
directories that already have a node_modules folder. Consistent behavior
everywhere.
The same npx distribution trick works. Just like npx -y bun, you can use
npx -y deno to run Deno without installing it globally. Any environment with
npm can execute Deno scripts.
One caveat: if Deno is already installed on your system, npx -y deno still
downloads a separate copy to npm's cache (~40MB, comparable to Bun's ~100MB
first-download cost). For systems with Deno pre-installed, use deno run
directly. The npx approach targets portability—scripts that work on any
machine with npm, regardless of what's pre-installed.
The Deno Approach
Here's what a Deno-based skill looks like:
#!/usr/bin/env -S npx -y deno run --node-modules-dir=false --allow-read --allow-writeimport { parse } from "npm:csv-parse@^5.0/sync";import chalk from "npm:chalk@^5.0.0";import { z } from "npm:zod@^3.23";const inputPath = Deno.args[0];const content = await Deno.readTextFile(inputPath);const rows = parse(content, { columns: true });console.log(chalk.green(`Parsed ${rows.length} rows`));
The npm: prefix is more verbose than Bun's bare imports, but it clarifies
package origins. TypeScript works natively. Version pinning lives in the import
path, same as with Bun. No deno.json or import map required—dependencies
resolve directly from the specifiers.
Deno requires permission flags—--allow-read, --allow-write, --allow-net,
etc. More verbose than Bun, but you declare exactly what the script does. For
skills running through Claude Code, explicit permissions document what the
script can access. For trusted environments, --allow-all (or -A) skips the
ceremony.
Trade-offs
| Aspect | Bun | Deno |
|---|---|---|
| Import syntax | import x from "[email protected]" | import x from "npm:[email protected]" |
| node_modules safe | No | Yes |
| Raw performance | ~20-30% faster | Slightly slower |
| TypeScript | Native | Native |
| Permissions model | Permissive by default | Explicit flags required |
Bun is faster. Startup time, runtime performance, HTTP serving—Bun consistently beats Deno in benchmarks. If you're building a production API or a performance-critical CLI tool, that matters.
For Claude Code skills, it doesn't.
Why Performance Doesn't Matter Here
The agent's thinking time dwarfs script execution time. Claude takes two to five seconds to decide what to do next. A skill that runs in 50 milliseconds versus 80 milliseconds is effectively the same—both are instant compared to the agent's decision loop.
Reliability matters more. A skill that works from any directory is more valuable than a skill that's 30% faster but breaks in monorepos.
Practical Example for Skills
The structure follows the same pattern from the
original article—a
SKILL.md pointing to executable scripts. The only changes are the shebang and
Deno-specific APIs:
#!/usr/bin/env -S npx -y deno run --node-modules-dir=false --allow-read --allow-writeimport { parse } from "npm:csv-parse@^5.0/sync";import * as XLSX from "npm:xlsx@^0.20";const inputPath = Deno.args[0];const content = await Deno.readTextFile(inputPath);const rows = parse(content, { columns: true });console.log(JSON.stringify(rows, null, 2));
Claude runs the skill, the script accesses npm packages, and everything works regardless of directory.
Conclusion
The npm: prefix is more verbose. Permission flags add ceremony. Bun's import
syntax is cleaner and faster. But Deno's reliability across different directory
structures makes it the better choice for Claude Code skills.
You don't have to debug why a skill works in one directory and fails in another. You don't have to document "this only works outside of projects with node_modules." The script just works.
If Bun adds a flag to force auto-install regardless of node_modules presence,
I'd reconsider. Until then, Deno's consistency wins.
References
- Writing Powerful Claude Code Skills with npx bun — The original exploration of this approach
- Deno — A modern runtime for JavaScript and TypeScript
- Deno npm compatibility
— How the
npm:specifier works - Bun Auto-Install Documentation — Understanding when auto-install activates
- Claude Code Skills Documentation