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
- Add to ~/.zshrc or ~/.bashrc:
- 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:
- Select text in the editor.
- Copy it.
- 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.