Why run Claude from the command line inside VS Code?

The VS Code extension ecosystem is great, but there’s a special kind of speed you get from the terminal:

  • You can pipe any file or output directly to Claude and get structured responses back.
  • You can script repeatable AI workflows (tests, refactors, doc drafts) with one command or keybinding.
  • You can keep everything in version control: prompts, outputs, diffs, and logs.

This guide shows you how to build a minimal Claude CLI, wire it into VS Code, and use it to accelerate everyday work. You’ll learn how to:

  • Create a tiny, reliable CLI script to call the Claude API.
  • Pipe files and prompts from VS Code into the CLI.
  • Save outputs into your repo (docs, patches, summaries).
  • Build repeatable tasks and keybindings for common AI workflows.
  • Use secure environment variables and troubleshoot issues.

The approach is lightweight, reproducible, and editor-agnostic—perfect for teams who want portable AI workflows.


Prerequisites

Before you start:

  • A Claude API key (set as ANTHROPIC_API_KEY)
  • Node.js 18+ (for the example CLI below)
  • VS Code (any recent version)

Set your API key as an environment variable:

  • macOS/Linux (bash/zsh):
    • Add to ~/.zshrc or ~/.bashrc:
      • export ANTHROPIC_API_KEY="your_api_key_here"
    • Reload: source ~/.zshrc
  • Windows (PowerShell, current session):
    • $env:ANTHROPIC_API_KEY="your_api_key_here"
  • Windows (persist via cmd):
    • setx ANTHROPIC_API_KEY "your_api_key_here"

Tip: Never commit API keys. If you prefer .env files, ensure they’re in .gitignore and only loaded in local dev.


Build a minimal Claude CLI (Node.js)

We’ll create a small Node.js script that:

  • Reads input from stdin or files passed as arguments.
  • Accepts optional flags like --model and --system.
  • Calls Claude and prints the response to stdout.

Create a file at scripts/claude.mjs:

mkdir -p scripts
#!/usr/bin/env node
// scripts/claude.mjs
import fs from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import Anthropic from "@anthropic-ai/sdk";

const HELP = `
Usage:
  claude [--model MODEL] [--system "SYSTEM PROMPT"] [--max-tokens N] [FILE...]
  If FILEs are provided, they're read and combined. Otherwise, reads from stdin.

Examples:
  echo "Explain this code:" | claude --model claude-3-5-sonnet-latest
  claude --system "You are a senior code reviewer" src/**/*.ts

Environment:
  ANTHROPIC_API_KEY must be set.
`.trim();

function parseArgs(argv) {
  const args = { files: [], model: "claude-3-5-sonnet-latest", system: "", maxTokens: 1024 };
  for (let i = 2; i < argv.length; i++) {
    const a = argv[i];
    if (a === "-h" || a === "--help") return { help: true };
    if (a === "--model") { args.model = argv[++i]; continue; }
    if (a.startsWith("--model=")) { args.model = a.split("=")[1]; continue; }
    if (a === "--system") { args.system = argv[++i]; continue; }
    if (a.startsWith("--system=")) { args.system = a.split("=").slice(1).join("="); continue; }
    if (a === "--max-tokens") { args.maxTokens = Number(argv[++i]); continue; }
    if (a.startsWith("--max-tokens=")) { args.maxTokens = Number(a.split("=")[1]); continue; }
    args.files.push(a);
  }
  return args;
}

async function readStdin() {
  const chunks = [];
  for await (const chunk of process.stdin) chunks.push(chunk);
  return Buffer.concat(chunks).toString("utf8");
}

async function readFiles(files) {
  const contents = [];
  for (const f of files) {
    const matched = f.includes("*") ? await globExpand(f) : [f];
    for (const file of matched) {
      try {
        const abs = path.resolve(file);
        const text = await fs.readFile(abs, "utf8");
        contents.push(`\n===== FILE: ${file} =====\n${text}`);
      } catch (err) {
        console.error(`Skipping unreadable file: ${file} (${err.message})`);
      }
    }
  }
  return contents.join("\n");
}

// Minimal globber for **/*.ext patterns (not exhaustive)
async function globExpand(pattern) {
  // naive: only handle "**/*" or "*" at filename level; for real use, depend on "fast-glob"
  if (!pattern.includes("*")) return [pattern];
  const dir = pattern.includes("/") ? pattern.slice(0, pattern.lastIndexOf("/")) : ".";
  const base = pattern.slice(pattern.lastIndexOf("/") + 1);
  const regex = new RegExp("^" + base.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$");
  async function walk(d) {
    const results = [];
    for (const dirent of await fs.readdir(d, { withFileTypes: true })) {
      const p = path.join(d, dirent.name);
      if (dirent.isDirectory()) {
        results.push(...await walk(p));
      } else if (regex.test(dirent.name)) {
        results.push(p);
      }
    }
    return results;
  }
  const root = dir || ".";
  return await walk(root);
}

async function main() {
  const parsed = parseArgs(process.argv);
  if (parsed.help) {
    console.log(HELP);
    process.exit(0);
  }
  const apiKey = process.env.ANTHROPIC_API_KEY;
  if (!apiKey) {
    console.error("Error: ANTHROPIC_API_KEY not set.");
    process.exit(1);
  }

  let input = "";
  if (parsed.files.length > 0) {
    input = await readFiles(parsed.files);
  } else {
    // Read from stdin if connected, else show help
    if (process.stdin.isTTY) {
      console.log(HELP);
      process.exit(0);
    }
    input = await readStdin();
  }

  const client = new Anthropic({ apiKey });
  try {
    const response = await client.messages.create({
      model: parsed.model,
      max_tokens: parsed.maxTokens,
      system: parsed.system || undefined,
      messages: [{ role: "user", content: input }]
    });

    const text = (response.content || [])
      .map(block => block.type === "text" ? block.text : JSON.stringify(block))
      .join("\n");

    process.stdout.write(text.trim() + "\n");
  } catch (err) {
    console.error("Claude error:", err?.message || err);
    process.exit(1);
  }
}

main();

Make it executable:

chmod +x scripts/claude.mjs

Install the SDK:

npm install @anthropic-ai/sdk

You now have a minimal, reliable Claude CLI you can run anywhere in VS Code’s integrated terminal.


Quick smoke test

Open the VS Code terminal (View > Terminal) and try:

echo "Give me three ideas for onboarding UI microcopy." | ./scripts/claude.mjs

Specify a system prompt and model:

echo "Refactor this into a pure function:\n\n$(cat src/util.js)" \
| ./scripts/claude.mjs --system "You are a pragmatic senior JavaScript engineer." --model claude-3-5-sonnet-latest

Read files directly:

./scripts/claude.mjs --system "Summarize each file succinctly." README.md src/**/*.ts

If you see coherent text output, you’re ready to wire this into VS Code workflows.


VS Code integration patterns that feel native

You can use the built-in terminal and Tasks to make the CLI feel first-class in your editor.

1) A task to “Ask Claude about the current file”

Create .vscode/tasks.json:

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "Claude: Summarize current file",
      "type": "shell",
      "command": "node",
      "args": [
        "${workspaceFolder}/scripts/claude.mjs",
        "--system",
        "You are a technical writer. Summarize the file, listing functions and responsibilities.",
        "${file}"
      ],
      "problemMatcher": [],
      "presentation": {
        "reveal": "always",
        "panel": "dedicated",
        "clear": true
      }
    }
  ]
}

Run it via Terminal > Run Task … > “Claude: Summarize current file”. The output appears in a dedicated panel so it doesn’t mix with your build logs.

Tip: Add a keybinding for faster use. In keybindings.json:

[
  {
    "key": "cmd+shift+s",
    "command": "workbench.action.tasks.runTask",
    "args": "Claude: Summarize current file",
    "when": "editorTextFocus && resourceExtname =~ /(js|ts|py|md)/"
  }
]

Change the keys to your liking (Ctrl on Windows/Linux).

2) Save responses into your repo

Instead of printing to the terminal, redirect to a file in your docs or logs folder:

{
  "label": "Claude: Summarize -> docs/claude/${fileBasenameNoExtension}.md",
  "type": "shell",
  "command": "bash",
  "args": [
    "-lc",
    "mkdir -p docs/claude && node ${workspaceFolder}/scripts/claude.mjs --system 'Write a concise overview, include code blocks if helpful.' ${file} > docs/claude/${fileBasenameNoExtension}.md"
  ],
  "problemMatcher": [],
  "presentation": { "reveal": "always", "panel": "dedicated" }
}

Now every run produces a markdown artifact you can commit.

Windows PowerShell equivalent:

{
  "label": "Claude: Summarize (Windows)",
  "type": "shell",
  "command": "powershell",
  "args": [
    "-NoProfile",
    "-Command",
    "New-Item -ItemType Directory -Force docs/claude | Out-Null; node ${workspaceFolder}/scripts/claude.mjs --system 'Write a concise overview, include code blocks if helpful.' ${file} | Out-File -Encoding utf8 docs/claude/${fileBasenameNoExtension}.md"
  ]
}

3) Use prompt templates for repeatable tasks

Create a prompts directory:

prompts/
  code_review.system.txt
  unit_tests.system.txt
  doc_summary.system.txt

Example prompts/unit_tests.system.txt:

You are a meticulous test engineer.
- Generate unit tests with clear arrange-act-assert structure.
- Prefer table-driven cases for edge conditions.
- Target 80%+ coverage, but do not overfit.
Return only code blocks for the test file(s).

Add tasks that reuse these prompts:

{
  "label": "Claude: Generate unit tests for current file",
  "type": "shell",
  "command": "bash",
  "args": [
    "-lc",
    "node ${workspaceFolder}/scripts/claude.mjs --system \"$(cat prompts/unit_tests.system.txt)\" ${file} > ${fileDirname}/${fileBasenameNoExtension}.test.suggested.md"
  ],
  "problemMatcher": [],
  "presentation": { "reveal": "always", "panel": "dedicated" }
}

Now you can generate tests for any file with a keystroke.


Practical workflows you’ll actually use

Quick explain or “TL;DR” on code

./scripts/claude.mjs --system "Explain as if to a mid-level engineer. Include pitfalls and examples." ${file}

Save to docs:

./scripts/claude.mjs --system "Summarize the module's responsibilities and public API." ${file} > docs/claude/${fileBasenameNoExtension}.md

Code review checklist with suggestions

Create prompts/code_review.system.txt:

You are a senior reviewer. For each file:
- Identify clear issues (correctness, performance, security).
- Propose specific diffs (unified diff format) with minimal changes.
- Explain trade-offs.
Return first: a one-paragraph summary, then diffs per file.

Run against changes (git):

git diff --name-only | xargs ./scripts/claude.mjs --system "$(cat prompts/code_review.system.txt)" > review.md

Open review.md in VS Code and apply diffs manually. You can also instruct Claude to output only unified diffs and apply them with git apply, but review carefully.

Generate a patch you can apply

Prompt Claude to return a unified diff:

System: You return only a valid unified diff (git patch) for the code changes requested—no commentary.
User: Update fetchData to use async/await, add error handling, and adjust callers accordingly.

Files:
===== FILE: src/api.js =====
...file content...
===== FILE: src/view.js =====
...file content...

Apply the patch:

./scripts/claude.mjs --system "Return only a unified diff that applies cleanly to HEAD." src/api.js src/view.js > changes.patch
git apply --reject --whitespace=fix changes.patch

If there are rejects, inspect *.rej files and apply manually.

Summarize a PR or issue thread

If you have a local text export:

./scripts/claude.mjs --system "Summarize the conversation by decisions, blockers, and next steps." docs/pr-1234-thread.txt > docs/claude/pr-1234-summary.md

Documentation scaffolding

Bootstrap a README from code:

./scripts/claude.mjs --system "Draft a README with overview, setup, usage examples, and API reference." src/**/*.ts > docs/README.draft.md

Then edit to final.

SQL or data transformation helper

echo "I have a table 'orders(id, user_id, amount, created_at)'. Write a query for monthly revenue last 6 months with cumulative sum." \
| ./scripts/claude.mjs --system "You are a SQL expert. Return only SQL in a fenced code block."

Make it feel seamless in VS Code

Create a dedicated terminal profile with environment

If your shell profile isn’t loading in VS Code, set terminal-specific env:

  • VS Code Settings (JSON): add
"terminal.integrated.env.osx": {
  "ANTHROPIC_API_KEY": "YOUR_KEY_OR_BETTER_USE_SHELL_PROFILE"
}

Prefer storing the key in your shell profile. For teams, avoid committing this to settings unless you’re using secrets injection.

Run selected text straight to Claude

Workflow:

  1. Select text in the editor.
  2. Copy it.
  3. Paste into terminal and pipe:
pbpaste | ./scripts/claude.mjs --system "Explain as comments, then show revised code."

Cross-platform clipboard commands:

  • macOS: pbpaste / pbcopy
  • Linux: xclip -selection clipboard or xsel --clipboard
  • Windows: Get-Clipboard and Set-Clipboard in PowerShell

Example (Windows PowerShell):

Get-Clipboard | node .\scripts\claude.mjs --system "Convert to idiomatic C# with comments."

Tip: Map editor.action.clipboardCopyAction to a keybinding and keep a terminal open for rapid cycles.

Use Code Runner or NPM scripts as a bridge

If you already use npm scripts:

{
  "scripts": {
    "ask": "node scripts/claude.mjs --model claude-3-5-sonnet-latest",
    "review:file": "node scripts/claude.mjs --system \"$(cat prompts/code_review.system.txt)\""
  }
}

Then in VS Code, use the NPM Scripts panel to run them with a click.


Managing context size and performance

LLMs have token limits and cost considerations. A few tips:

  • Be selective with files. Ask Claude about the module and 1–2 key dependencies, not the whole repo.
  • Summarize first, then dive deeper. Use a two-step approach: summary → targeted follow-ups.
  • Use “index prompts”: small prompts that instruct Claude to list the most relevant files to inspect next.
  • Reduce noise:
    • Exclude node_modules, build outputs, and large binaries.
    • Add a “context filter” script to strip comments or minify when you only need structure.

Example filter: only function signatures (JS/TS):

sed -n -E 's/^[[:space:]]*\/\/.*$//;/function|class|=>/p' src/**/*.ts \
| ./scripts/claude.mjs --system "Identify main entry points and responsibilities."

Prompt engineering patterns that work

  • Start with a clear role and output format:
    • “You are a senior reviewer. Return a prioritized list and a patch.”
    • “You are a test engineer. Return only Jest code blocks.”
  • Use constraints to reduce noise:
    • “Return only a unified diff.”
    • “Do not restate the prompt. Keep answers under 200 lines.”
  • Provide context as labeled sections:
    • “Requirements: …”
    • “Code: …”
    • “Constraints: …”
    • “Output format: …”
  • Iterate. When the first answer isn’t perfect, refine your prompt and rerun. Keep both prompt and output in your repo to build a library of proven patterns.

Security, secrets, and team workflows

  • Never embed API keys in tasks.json or committed scripts. Use environment variables or secret stores.
  • Add patterns to .gitignore:
    • .env
    • docs/claude/*.md (if you don’t want to commit drafts)
    • *.patch (if sensitive)
  • For team-wide usage:
    • Provide a shared prompts/ folder with your best system prompts.
    • Wrap your CLI in Make/NPM scripts with consistent flags and defaults.
    • Document costs and usage guidelines.

Example Makefile:

ask:
	echo "$(q)" | node scripts/claude.mjs --model $(model)
review:
	node scripts/claude.mjs --system "$$(cat prompts/code_review.system.txt)" $(files) > review.md

Run: make ask q="How does X work?" model=claude-3-5-sonnet-latest


Troubleshooting

  • “Error: ANTHROPIC_API_KEY not set.”

    • Ensure the variable is available to VS Code’s terminal. Echo it: echo $ANTHROPIC_API_KEY (macOS/Linux) or $env:ANTHROPIC_API_KEY (Windows).
    • If your shell profile isn’t loading in VS Code, set terminal.integrated.defaultProfile and Profiles correctly, or export in the shell config used by VS Code.
  • “Permission denied” on scripts/claude.mjs

    • chmod +x scripts/claude.mjs or run with node scripts/claude.mjs
  • “Module not found: @anthropic-ai/sdk”

    • npm install @anthropic-ai/sdk at the workspace root. Ensure Node 18+.
  • CLI returns truncated or incomplete answers

    • Increase --max-tokens (e.g., --max-tokens 4000), but note cost and limits.
    • Narrow input context. Summarize or split the task.
  • Network or rate limit errors

    • Implement retries around your calls (wrap the API call in a small retry function).
    • Add short delays between batch tasks. Keep track of usage.
  • Diff won’t apply

    • Ask Claude to base against HEAD and include only modified hunks.
    • Use git apply --reject and inspect *.rej diffs.

Extend your CLI when you’re ready

Once the basics work, consider:

  • Streaming output
    • The SDK supports streaming; you can render tokens as they arrive for faster feedback.
  • JSON modes
    • Instruct Claude to return strict JSON and parse it for automated workflows (e.g., issue summaries, changelog entries).
  • Tooling “agents”
    • Add subcommands like claude refactor, claude test, each with a dedicated system prompt.
  • Incremental context
    • Cache summaries of large files and only pull full content when needed.

Example: add a --json flag to always return a JSON object and parse response.content, then format or save accordingly.


How this compares to a dedicated VS Code extension

If you prefer an in-editor chat UI with context awareness, consider using a Claude-centric VS Code extension for a turnkey experience. The CLI approach shines when you want:

  • Repeatable, shareable scripts in your repo.
  • Tight integration with your Git/CI tooling.
  • Keyboard-driven flows with precise outputs (diffs, code blocks, JSON).
  • Portability across editors and environments.

Many teams use both: an extension for ad‑hoc conversation and a CLI for scripted tasks.


A few ready-to-use recipes

  • “Explain function and add JSDoc” for the current file:
{
  "label": "Claude: JSDoc current file",
  "type": "shell",
  "command": "bash",
  "args": [
    "-lc",
    "node scripts/claude.mjs --system 'Explain each function briefly and produce JSDoc for each public function. Return only the updated code in a single fenced block.' ${file} > ${fileDirname}/${fileBasenameNoExtension}.jsdoc.md"
  ],
  "presentation": { "reveal": "always", "panel": "dedicated" }
}
  • “Write unit tests” from a prompt template:
node scripts/claude.mjs --system "$(cat prompts/unit_tests.system.txt)" src/service/*.ts > tests/suggested/service.tests.md
  • “Summarize a directory”:
find src -name "*.py" -maxdepth 1 -type f -print0 \
| xargs -0 node scripts/claude.mjs --system "Summarize each file's role and public API." \
> docs/claude/python-overview.md
  • “Generate a changelog entry from commit diff”:
git log -1 -p | node scripts/claude.mjs --system "Write a semantic, human-readable changelog entry with scope, rationale, and migration notes." > CHANGELOG_ENTRY.md

Best practices recap

  • Keep prompts modular and versioned (prompts/ directory).
  • Save meaningful outputs (docs/claude/). Discard noise.
  • Prefer small, focused inputs over dumping entire repos.
  • Use a few great tasks with keybindings to speed daily work.
  • Treat diffs as suggestions—review before applying.
  • Start simple; add streaming, JSON, or subcommands as needs grow.

Final thoughts

You don’t need a heavyweight setup to get serious value from Claude inside VS Code. A tiny CLI script, a handful of prompt templates, and a few tasks give you a powerful, repeatable AI toolkit:

  • Ask questions about code without leaving your editor.
  • Generate tests, docs, and patches you can commit.
  • Capture and evolve your best AI workflows as code.

Start with the minimal CLI in this guide, wire up one or two tasks you’ll use daily, and iterate. Within a day, you’ll have a fast, reliable AI assistant that feels native to your editor and adapts to your team’s way of working.

Share this article
Last updated: Oct 01, 2025

Need AI Expert Help?

Get professional consultation for your AI integration project. Our AI experts are ready to help you build intelligent, scalable solutions.