Summary #
tsx (TypeScript Execute) is the recommended solution for distributing TypeScript CLI tools via npm without a build step. With 11.6k GitHub stars and 416k dependents[1], tsx has become the de facto standard for running TypeScript directly in Node.js. It uses esbuild under the hood for fast transpilation and requires zero configuration—no tsconfig.json needed to get started[2].
For distributing a CLI as a Claude Code plugin, you can ship TypeScript source files directly and use the shebang #!/usr/bin/env -S npx tsx to make your entry point executable. This approach eliminates the build step entirely while providing full TypeScript support including features that Node.js native type-stripping doesn't handle (like path aliases and certain tsconfig options)[3].
While Node.js 23.6+ now includes native TypeScript support via type-stripping[4], it still has significant limitations: it ignores tsconfig.json, doesn't support features like paths aliases, and the Node.js team still recommends against production use[5]. tsx provides a more complete and battle-tested solution.
Philosophy & Mental Model #
tsx is a drop-in replacement for node that transparently handles TypeScript. Think of it as node with superpowers:
- Just-in-time transpilation: TypeScript is converted to JavaScript on the fly using esbuild
- Zero config: Works out of the box without tsconfig.json (but respects it if present)
- No type-checking: tsx focuses on execution speed, not correctness—run
tsc --noEmitseparately for type checking - Single binary: No peer dependencies required (TypeScript and esbuild are bundled)
The key mental model: tsx makes TypeScript files behave like JavaScript files. Anywhere you'd use node script.js, you can use tsx script.ts.
Setup #
For a Distributable CLI Package #
1. Initialize your package:
1npm init -y
2. Install tsx as a regular dependency:
1npm install tsx
Important: Use
dependencies, notdevDependencies, since consumers need tsx to run your CLI.
3. Create your CLI entry point (e.g., cli.ts):
1#!/usr/bin/env -S npx tsx
2
3import { Command } from 'commander';
4
5const program = new Command();
6
7program
8 .name('my-cli')
9 .description('My awesome CLI tool')
10 .version('1.0.0');
11
12program
13 .argument('<input>', 'input to process')
14 .action((input) => {
15 console.log(`Processing: ${input}`);
16 });
17
18program.parse();
4. Configure package.json:
1{
2 "name": "my-cli",
3 "version": "1.0.0",
4 "type": "module",
5 "bin": {
6 "my-cli": "./cli.ts"
7 },
8 "files": [
9 "cli.ts",
10 "src/**/*.ts"
11 ],
12 "dependencies": {
13 "tsx": "^4.21.0"
14 }
15}
5. Make executable (for local testing):
1chmod +x cli.ts
6. Test locally:
1# Direct execution
2./cli.ts hello
3
4# Via npx (simulates how users will run it)
5npx . hello
Publishing #
1npm publish
Users can then run your CLI with:
1npx my-cli hello
Core Usage Patterns #
Pattern 1: The Shebang for Distribution #
The magic line that makes your TypeScript file executable:
1#!/usr/bin/env -S npx tsx
Why -S? The -S flag tells env to split the argument string. Without it, npx tsx would be treated as a single command name. This works on macOS and modern Linux (coreutils 8.30+)[6].
Why npx? Using npx tsx instead of just tsx ensures tsx is available even if not globally installed—npx will fetch it if needed.
Pattern 2: Hybrid CLI and Library #
Create a single file that works both as an executable CLI and an importable module[7]:
1#!/usr/bin/env -S npx tsx
2
3// Exported function for library usage
4export function processData(input: string): string {
5 return input.toUpperCase();
6}
7
8// CLI entry point - only runs when executed directly
9if (import.meta.url === `file://${process.argv[1]}`) {
10 const args = process.argv.slice(2);
11 if (args.length === 0) {
12 console.error('Usage: my-cli <input>');
13 process.exit(1);
14 }
15 console.log(processData(args[0]));
16}
Users can:
- Run as CLI:
npx my-cli hello→HELLO - Import as library:
import { processData } from 'my-cli'
Pattern 3: Multi-File Project Structure #
my-cli/
├── cli.ts # Entry point with shebang
├── src/
│ ├── commands/
│ │ ├── generate.ts
│ │ └── process.ts
│ ├── utils/
│ │ └── helpers.ts
│ └── index.ts # Re-exports for library usage
├── package.json
└── tsconfig.json # Optional but recommended
cli.ts:
1#!/usr/bin/env -S npx tsx
2
3import { program } from './src/commands/index.js';
4program.parse();
Note: Use
.jsextensions in imports even for.tsfiles when using ESM. tsx handles the resolution.
Pattern 4: Watch Mode for Development #
During development, use watch mode to auto-reload on changes:
1npx tsx watch ./cli.ts
Or add to package.json:
1{
2 "scripts": {
3 "dev": "tsx watch ./cli.ts",
4 "start": "tsx ./cli.ts"
5 }
6}
Pattern 5: Environment Variables with dotenv #
tsx works seamlessly with dotenv:
1#!/usr/bin/env -S npx tsx
2
3import 'dotenv/config';
4
5console.log(process.env.API_KEY);
Anti-Patterns & Pitfalls #
Don't: Rely on tsx for Type Checking #
1// This will run successfully despite the type error!
2const x: number = "not a number";
3console.log(x);
Why it's wrong: tsx strips types without checking them. Your code will run but may have runtime errors that TypeScript would have caught.
Instead: Run tsc Separately #
1{
2 "scripts": {
3 "typecheck": "tsc --noEmit",
4 "build": "npm run typecheck && npm run start"
5 }
6}
Don't: Use #!/usr/bin/env tsx Without -S npx #
1#!/usr/bin/env tsx // Won't work if tsx isn't globally installed
Why it's wrong: This requires tsx to be globally installed on the user's system, which you can't guarantee.
Instead: Always Use the Full Shebang #
1#!/usr/bin/env -S npx tsx
Don't: Put tsx in devDependencies for CLIs #
1{
2 "devDependencies": {
3 "tsx": "^4.21.0" // Wrong for distributed CLIs!
4 }
5}
Why it's wrong: When users install your package, devDependencies aren't installed. Your CLI will fail because tsx won't be available.
Instead: Use Regular Dependencies #
1{
2 "dependencies": {
3 "tsx": "^4.21.0"
4 }
5}
Don't: Forget the files Field #
1{
2 "bin": {
3 "my-cli": "./cli.ts"
4 }
5 // Missing "files" field!
6}
Why it's wrong: npm might not include your TypeScript source files in the published package.
Instead: Explicitly List Files #
1{
2 "bin": {
3 "my-cli": "./cli.ts"
4 },
5 "files": [
6 "cli.ts",
7 "src/**/*.ts"
8 ]
9}
Don't: Mix CommonJS and ESM Carelessly #
1// In an ESM project (type: "module")
2const fs = require('fs'); // This will fail
Why it's wrong: tsx respects your module system. If you're using ESM, use ESM imports.
Instead: Be Consistent With Your Module System #
1// In ESM (type: "module")
2import fs from 'fs';
3import { readFileSync } from 'fs';
4
5// Or use createRequire if you must use require
6import { createRequire } from 'module';
7const require = createRequire(import.meta.url);
Caveats #
-
~200ms npx overhead: Using
npx tsxadds approximately 200ms startup time compared to running compiled JavaScript[8]. For frequently-run CLIs, consider pre-compiling with tsup or having users install tsx globally. -
Windows compatibility: The shebang approach works on Windows because npm creates wrapper
.cmdfiles[9]. However, direct execution (./cli.ts) only works in Unix-like shells. -
No runtime type safety: tsx removes types at transpilation time. For runtime validation, use libraries like Zod or io-ts.
-
Node.js version requirements: tsx supports all maintained Node.js versions (18+). Check compatibility if targeting older Node versions.
-
tsconfig.json is optional but recommended: While tsx works without it, having a tsconfig.json ensures consistent behavior and enables better IDE support.
-
Large dependency tree: tsx bundles esbuild (~9MB). If package size is critical, consider pre-compiling to JavaScript instead.
References #
[1] tsx GitHub Repository - GitHub stars count and dependents data
[2] tsx Official Documentation - Getting Started - Zero-config setup and installation guide
[3] How to run TypeScript files directly in Node.js in 2025 - Comparison of tsx vs Node.js native TypeScript support
[4] Node.js 23.6 Now Runs TypeScript Natively - InfoQ - Node.js native TypeScript type stripping announcement
[5] Node's new built-in support for TypeScript - Detailed analysis of limitations and production readiness
[6] Stack Overflow: Using NPX command for shell script shebang - Explanation of the -S flag requirement
[7] Creating a Hybrid TypeScript CLI and Library Function with tsx - Pattern for dual CLI/library usage
[8] ts-node GitHub Issue #995 - Discussion of npx overhead and shebang patterns
[9] npm Community Forum: Making a command for a package - npm's cross-platform executable handling
[10] Add CLI Scripts to Your TypeScript/Node Project with TSX - Practical guide for CLI script creation