Claude Code for CLI Tools: Building Command-Line Apps in Node.js, Python, and Go — Claude Skills 360 Blog
Blog / Development / Claude Code for CLI Tools: Building Command-Line Apps in Node.js, Python, and Go
Development

Claude Code for CLI Tools: Building Command-Line Apps in Node.js, Python, and Go

Published: May 12, 2026
Read time: 9 min read
By: Claude Skills 360

CLI tools are some of the most satisfying things to build — a single command that does something useful. But building them well involves a set of patterns that aren’t obvious: argument parsing libraries, interactive prompts, progress indicators, config file management, error output conventions, and packaging for distribution. Claude Code knows these patterns for Node.js, Python, and Go.

This guide covers building CLI tools with Claude Code: argument parsing, interactive UX, config management, and publishing.

Setting Up Claude Code for CLI Development

# CLI Tool Project Context

## Stack
- Node.js 20 + TypeScript
- CLI framework: commander.js (not yargs)
- Interactive prompts: @inquirer/prompts (not inquirer v8)
- Spinner/progress: ora + cli-progress
- Config: cosmiconfig for rc files + env var override
- Output: chalk for color, boxen for boxes

## Conventions
- Exit codes: 0 = success, 1 = general error, 2 = misuse
- Error output to stderr, progress/info to stdout
- Never silence errors — always show what failed
- Config hierarchy: CLI flags > env vars > config file > defaults

## Distribution
- npm package with bin entry in package.json
- Cross-platform (Windows compatibility required)

See the CLAUDE.md setup guide for complete configuration.

Building CLI Tools with Node.js

Command Structure with Commander

Build a CLI tool called "deploy-tool" with three subcommands:
- deploy <environment> [--dry-run] [--version <tag>]
- rollback <environment> [--to <version>]
- status [<environment>]
#!/usr/bin/env node
import { Command } from 'commander';
import { deploy } from './commands/deploy.js';
import { rollback } from './commands/rollback.js';
import { status } from './commands/status.js';
import { version } from '../package.json';

const program = new Command();

program
  .name('deploy-tool')
  .description('Deployment automation CLI')
  .version(version);

program
  .command('deploy <environment>')
  .description('Deploy to the specified environment')
  .option('-d, --dry-run', 'Preview changes without deploying')
  .option('-v, --version <tag>', 'Deploy a specific version tag')
  .action(async (environment: string, options: { dryRun?: boolean; version?: string }) => {
    try {
      await deploy({ environment, ...options });
    } catch (error) {
      console.error(chalk.red(`Deploy failed: ${error.message}`));
      process.exit(1);
    }
  });

program
  .command('rollback <environment>')
  .description('Roll back the specified environment')
  .option('--to <version>', 'Roll back to a specific version')
  .action(async (environment: string, options: { to?: string }) => {
    try {
      await rollback({ environment, targetVersion: options.to });
    } catch (error) {
      console.error(chalk.red(`Rollback failed: ${error.message}`));
      process.exit(1);
    }
  });

program
  .command('status [environment]')
  .description('Check deployment status (all envs if none specified)')
  .action(async (environment?: string) => {
    await status({ environment });
  });

program.parseAsync(process.argv);

Commander’s .command('deploy <environment>') syntax defines required arguments. Optional arguments use [brackets]. Claude generates TypeScript types for the options objects, handles the error → stderr → exit(1) pattern, and structures commands in separate files.

Interactive Prompts

Before deploying to production, require interactive confirmation.
Show what will be deployed and ask for explicit "yes".
import { input, confirm, select } from '@inquirer/prompts';
import chalk from 'chalk';

export async function confirmProductionDeploy(options: DeployOptions) {
  console.log(chalk.yellow('\n⚠  Production deployment'));
  console.log('  Environment:', chalk.bold('production'));
  console.log('  Version:    ', chalk.bold(options.version ?? 'latest'));
  console.log('  Service:    ', chalk.bold(options.service ?? 'all services'));
  console.log();
  
  const confirmed = await confirm({
    message: 'Deploy to production?',
    default: false,  // Defaults to No — must explicitly say Yes
  });
  
  if (!confirmed) {
    console.log('Deploy cancelled.');
    process.exit(0);
  }
  
  // Extra safeguard: type environment name
  const typed = await input({
    message: 'Type "production" to confirm:',
  });
  
  if (typed !== 'production') {
    console.error(chalk.red('Confirmation failed. Aborting.'));
    process.exit(1);
  }
}

@inquirer/prompts is the current package — inquirer v8 is deprecated. Claude uses the current API.

Progress Indicators

The deploy step takes 30-90 seconds.
Show a spinner during the upload phase and a progress bar 
during the migration phase.
import ora from 'ora';
import { SingleBar, Presets } from 'cli-progress';

async function runDeploy(options: DeployOptions) {
  // Spinner for indeterminate steps
  const spinner = ora('Uploading artifacts...').start();
  
  try {
    await uploadArtifacts(options);
    spinner.succeed('Artifacts uploaded');
  } catch (error) {
    spinner.fail(`Upload failed: ${error.message}`);
    throw error;
  }
  
  // Progress bar for quantified steps
  const migrations = await getMigrations();
  if (migrations.length > 0) {
    const bar = new SingleBar({
      format: 'Migrations |{bar}| {value}/{total}',
      barCompleteChar: '█',
      barIncompleteChar: '░',
    }, Presets.shades_classic);
    
    bar.start(migrations.length, 0);
    
    for (const migration of migrations) {
      await runMigration(migration);
      bar.increment();
    }
    
    bar.stop();
    console.log(chalk.green(`✓ ${migrations.length} migrations applied`));
  }
  
  const healthSpinner = ora('Verifying health checks...').start();
  await waitForHealthy(options);
  healthSpinner.succeed('All services healthy');
}

Spinner for unknown-duration operations, progress bar for countable steps. ora handles the terminal cursor and line-clearing correctly.

Config Management

Read config from: CLI flags (highest priority), 
environment variables, .deployrc.json, and defaults.
Use cosmiconfig.
import { cosmiconfig } from 'cosmiconfig';

interface DeployConfig {
  registry: string;
  timeout: number;
  slackWebhook?: string;
}

const defaults: DeployConfig = {
  registry: 'https://registry.example.com',
  timeout: 300,
};

export async function loadConfig(cliOverrides: Partial<DeployConfig>): Promise<DeployConfig> {
  const explorer = cosmiconfig('deploy');
  const result = await explorer.search();
  const fileConfig = result?.config ?? {};
  
  // Environment variable overrides
  const envConfig: Partial<DeployConfig> = {};
  if (process.env.DEPLOY_REGISTRY) envConfig.registry = process.env.DEPLOY_REGISTRY;
  if (process.env.DEPLOY_TIMEOUT) envConfig.timeout = parseInt(process.env.DEPLOY_TIMEOUT);
  if (process.env.SLACK_WEBHOOK) envConfig.slackWebhook = process.env.SLACK_WEBHOOK;
  
  // Merge: defaults < file < env < CLI flags
  return { ...defaults, ...fileConfig, ...envConfig, ...cliOverrides };
}

Cosmiconfig looks for .deployrc, .deployrc.json, .deployrc.yaml, or a deploy key in package.json — standard config file discovery. Claude generates the full precedence chain.

Python CLI Tools

Click-Based CLI

Build a Python CLI tool for database backup operations.
Commands: backup, restore, list, delete.
Progress bar for large exports.
#!/usr/bin/env python3
import click
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
from rich.console import Console

console = Console()

@click.group()
@click.option('--db-url', envvar='DATABASE_URL', required=True)
@click.pass_context
def cli(ctx, db_url: str):
    """Database backup management tool."""
    ctx.ensure_object(dict)
    ctx.obj['db_url'] = db_url

@cli.command()
@click.argument('name')
@click.option('--compress/--no-compress', default=True)
@click.option('--output', '-o', type=click.Path(), default='.')
@click.pass_context
def backup(ctx, name: str, compress: bool, output: str):
    """Create a database backup."""
    with Progress(
        SpinnerColumn(),
        TextColumn("[progress.description]{task.description}"),
        BarColumn(),
        "[progress.percentage]{task.percentage:>3.0f}%",
    ) as progress:
        task = progress.add_task("Backing up...", total=100)
        
        result = run_backup(
            ctx.obj['db_url'], name, compress, output,
            on_progress=lambda pct: progress.update(task, completed=pct)
        )
    
    console.print(f"[green]✓[/green] Backup saved: {result.path} ({result.size_mb:.1f} MB)")

@cli.command()
@click.argument('backup_name')
@click.option('--confirm', is_flag=True, help='Skip confirmation prompt')
@click.pass_context
def restore(ctx, backup_name: str, confirm: bool):
    """Restore from a backup."""
    if not confirm:
        click.confirm(f'Restore from {backup_name}? This will overwrite the database.', abort=True)
    
    with console.status(f"Restoring {backup_name}..."):
        run_restore(ctx.obj['db_url'], backup_name)
    
    console.print(f"[green]✓[/green] Restore complete")

Claude uses click for argument parsing, rich for terminal output (better than tqdm or print with ANSI codes). pass_context threads config through subcommands without global state.

Go CLI Tools

For Go CLIs, Claude generates cobra + viper — the standard Go CLI stack:

var rootCmd = &cobra.Command{
    Use:   "mytool",
    Short: "A brief description of your application",
}

var deployCmd = &cobra.Command{
    Use:   "deploy [environment]",
    Short: "Deploy to the specified environment",
    Args:  cobra.ExactArgs(1),
    RunE: func(cmd *cobra.Command, args []string) error {
        dryRun, _ := cmd.Flags().GetBool("dry-run")
        return runDeploy(args[0], dryRun)
    },
}

func init() {
    rootCmd.AddCommand(deployCmd)
    deployCmd.Flags().BoolP("dry-run", "d", false, "Preview without deploying")
}

cobra.Command.RunE (returns error) vs Run (no return) — Claude uses RunE to propagate errors to cobra’s error handler. The scaffolding connects flag binding to viper for config file + env var support.

Publishing CLI Tools

npm Package

Configure the package.json for publishing as a CLI tool.
The binary should be callable as "deploy-tool" after npm install -g.
{
  "name": "deploy-tool",
  "version": "1.0.0",
  "bin": {
    "deploy-tool": "./dist/index.js"
  },
  "engines": {
    "node": ">=18.0.0"
  },
  "scripts": {
    "build": "tsc",
    "prepublishOnly": "npm run build"
  },
  "files": ["dist/"]
}

The shebang #!/usr/bin/env node at the top of src/index.ts + the bin field in package.json makes it callable globally. The engines field shows npm a minimum Node version.

Cross-Platform Compatibility

Claude flags Windows compatibility issues: path.join() vs / separators, process.env.HOME vs os.homedir(), shell command execution differences. For tools that shell out, it uses execa (not child_process.exec) which handles cross-platform argument quoting correctly.

Testing CLI Tools

Write tests for the deploy command.
Test that: dry-run doesn't call the actual deploy, 
invalid environments reject with exit code 2,
missing config shows a helpful error.

Claude writes tests using the command module’s exported functions (not testing the full CLI process) and uses jest.spyOn(process, 'exit') to test exit codes. For integration tests, it uses execa to run the compiled binary in a subprocess with controlled environment variables.

CLI Tool Development with Claude Code

CLI tools are a strong Claude Code use case because the patterns are consistent but verbose to write from scratch — argument parsing boilerplate, error handling conventions, progress display, config file loading. Once Claude knows your CLI framework (Commander vs Click vs Cobra), it generates tools that behave like well-built CLIs rather than quick scripts.

The testing guide covers general testing patterns. For DevOps CLIs that wrap Docker or kubectl operations, the Docker guide and Kubernetes guide show the patterns that Claude Code handles for those domains. Starting CLI development? The Claude Skills 360 bundle includes CLI scaffolding skills for Node.js, Python, and Go — try the free tier for the Node.js Commander patterns.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free