Module 6: Advanced Hooks and Headless SDK

5. Headless SDK — TypeScript: Claude Code in the Node.js Ecosystem

5. Headless SDK — TypeScript: Claude Code in the Node.js Ecosystem

Description

If your main stack is JavaScript or TypeScript, you don't need to switch to Python to automate Claude Code. Everything you learned in the previous capsule — headless mode with -p, JSON output, --allowedTools — works the same from Node.js. But the TypeScript ecosystem adds advantages: static typing to parse Claude's responses, direct integration with build tools like esbuild and Vite, and the @anthropic-ai/claude-code package that offers a native API without going through subprocess.

This capsule covers two approaches: subprocess (using Node.js's child_process, similar to Python) and the SDK package (@anthropic-ai/claude-code). The first approach works in any environment where the Claude CLI is installed. The second offers a cleaner, typed API, but requires installing the package.

By the end you'll know how to run Claude Code from TypeScript scripts, integrate the automation with your Node.js tooling (build scripts, deploy scripts, monitoring), and choose between Python and TypeScript based on the use case.


Approach 1: subprocess with child_process

The basic pattern

Node.js has child_process as the equivalent of Python's subprocess:

import { execSync } from "child_process";

const result = execSync(
  'claude -p "How many TypeScript files are in src/?" --output-format json --allowedTools "Read,Glob,Grep"',
  { encoding: "utf-8", timeout: 60000 }
);

const output = JSON.parse(result);
console.log(output.result);
console.log(`Cost: $${output.cost_usd?.toFixed(4)}`);

execSync vs exec vs spawn

MethodTypeWhen to use it
execSyncSynchronous, blocksSimple scripts, short tasks
execAsync with callbackMedium-duration tasks
spawnAsync with streamsLong executions, streaming

Reusable function with typing

import { execSync } from "child_process";

interface ClaudeResult {
  type: string;
  subtype: string;
  is_error: boolean;
  result: string;
  session_id: string;
  cost_usd: number;
  duration_ms: number;
  num_turns: number;
}

interface ClaudeError {
  is_error: true;
  error: string;
  error_type: "timeout" | "not_found" | "parse_error" | "exit_code";
}

type ClaudeOutput = ClaudeResult | ClaudeError;

function runClaude(
  prompt: string,
  tools: string[] = [],
  timeoutMs: number = 300000
): ClaudeOutput {
  const toolsArg = tools.length > 0
    ? `--allowedTools "${tools.join(",")}"`
    : "";

  const cmd = `claude -p "${prompt.replace(/"/g, '\\"')}" --output-format json ${toolsArg}`;

  try {
    const result = execSync(cmd, {
      encoding: "utf-8",
      timeout: timeoutMs,
      maxBuffer: 10 * 1024 * 1024,
    });

    return JSON.parse(result) as ClaudeResult;
  } catch (error: any) {
    if (error.killed) {
      return { is_error: true, error: "Timeout", error_type: "timeout" };
    }
    if (error.code === "ENOENT") {
      return { is_error: true, error: "claude not found", error_type: "not_found" };
    }
    return {
      is_error: true,
      error: error.message || "Unknown error",
      error_type: "exit_code",
    };
  }
}

const output = runClaude(
  "List the exported functions in src/api/",
  ["Read", "Grep", "Glob"]
);

if (output.is_error) {
  console.error(`Error: ${(output as ClaudeError).error}`);
} else {
  const result = output as ClaudeResult;
  console.log(result.result);
  console.log(`Cost: $${result.cost_usd.toFixed(4)}`);
}

Async version with exec

For scripts that need to run multiple invocations without blocking:

import { exec } from "child_process";
import { promisify } from "util";

const execAsync = promisify(exec);

async function runClaudeAsync(
  prompt: string,
  tools: string[] = [],
  timeoutMs: number = 300000
): Promise<ClaudeResult | ClaudeError> {
  const toolsArg = tools.length > 0
    ? `--allowedTools "${tools.join(",")}"`
    : "";

  const cmd = `claude -p "${prompt.replace(/"/g, '\\"')}" --output-format json ${toolsArg}`;

  try {
    const { stdout } = await execAsync(cmd, {
      encoding: "utf-8",
      timeout: timeoutMs,
      maxBuffer: 10 * 1024 * 1024,
    });

    return JSON.parse(stdout) as ClaudeResult;
  } catch (error: any) {
    return {
      is_error: true,
      error: error.message || "Unknown error",
      error_type: "exit_code",
    };
  }
}

async function main() {
  const result = await runClaudeAsync(
    "Analyze src/components/ and list all the React components",
    ["Read", "Glob", "Grep"]
  );

  if (!result.is_error) {
    console.log((result as ClaudeResult).result);
  }
}

main();

Approach 2: The @anthropic-ai/claude-code Package

Installation

npm install @anthropic-ai/claude-code

Basic API

The package offers a cleaner API than subprocess:

import { claude } from "@anthropic-ai/claude-code";

async function main() {
  const result = await claude({
    prompt: "Analyze src/ and report the project structure",
    allowedTools: ["Read", "Glob", "Grep"],
    options: {
      maxTurns: 10,
    },
  });

  console.log(result.text);
  console.log(`Cost: $${result.costUsd.toFixed(4)}`);
}

main();

Native typing

The package's main advantage is the typing:

import { claude, ClaudeResult } from "@anthropic-ai/claude-code";

async function analyzeModule(path: string): Promise<ClaudeResult> {
  return claude({
    prompt: `Analyze ${path} and report: structure, dependencies, issues`,
    allowedTools: ["Read", "Glob", "Grep"],
    options: {
      maxTurns: 15,
    },
  });
}

async function main() {
  const result = await analyzeModule("src/api/");

  if (result.isError) {
    console.error("Analysis failed:", result.text);
    return;
  }

  console.log("Analysis:", result.text);
  console.log("Turns used:", result.numTurns);
  console.log("Duration:", result.durationMs, "ms");
}

main();

Streaming with the package

import { claude } from "@anthropic-ai/claude-code";

async function main() {
  const stream = claude.stream({
    prompt: "Generate a code quality report for src/",
    allowedTools: ["Read", "Glob", "Grep"],
  });

  for await (const event of stream) {
    if (event.type === "text") {
      process.stdout.write(event.content);
    }
  }

  const result = await stream.finalResult();
  console.log(`\n\nCost: $${result.costUsd.toFixed(4)}`);
}

main();

Automation Scripts in TypeScript

Script 1: Component Documentation Generator

#!/usr/bin/env npx tsx
/**
 * Generates automatic documentation for React components.
 * Usage: npx tsx scripts/gen-component-docs.ts
 */

import { execSync } from "child_process";
import * as fs from "fs";
import * as path from "path";

interface ClaudeResult {
  result: string;
  is_error: boolean;
  cost_usd: number;
}

function runClaude(prompt: string, tools: string[]): ClaudeResult | null {
  try {
    const toolsStr = tools.join(",");
    const result = execSync(
      `claude -p "${prompt.replace(/"/g, '\\"')}" --output-format json --allowedTools "${toolsStr}"`,
      { encoding: "utf-8", timeout: 120000 }
    );
    return JSON.parse(result);
  } catch {
    return null;
  }
}

function getComponentFiles(): string[] {
  const result = execSync(
    'find src/components -name "*.tsx" -not -name "*.test.*" -not -name "*.spec.*"',
    { encoding: "utf-8" }
  );
  return result.trim().split("\n").filter(Boolean);
}

function main() {
  const components = getComponentFiles();
  console.log(`Found ${components.length} components\n`);

  const docs: string[] = ["# Component Documentation\n"];
  let totalCost = 0;

  for (const file of components) {
    console.log(`Documenting: ${file}`);

    const output = runClaude(
      `Read ${file} and generate documentation for the React component it contains.
Include: name, props (with types), description, usage example.
Markdown format. Be concise.`,
      ["Read"]
    );

    if (output && !output.is_error) {
      docs.push(output.result);
      docs.push("\n---\n");
      totalCost += output.cost_usd || 0;
    }
  }

  const outputPath = "docs/components.md";
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
  fs.writeFileSync(outputPath, docs.join("\n"));

  console.log(`\nDocs written to ${outputPath}`);
  console.log(`Total cost: $${totalCost.toFixed(4)}`);
}

main();

Script 2: Build Validator

#!/usr/bin/env npx tsx
/**
 * Validates the build and asks Claude to fix errors.
 * Usage: npx tsx scripts/build-validator.ts
 */

import { execSync } from "child_process";

interface ClaudeResult {
  result: string;
  is_error: boolean;
  cost_usd: number;
}

function runBuild(): { success: boolean; output: string } {
  try {
    const output = execSync("npm run build 2>&1", { encoding: "utf-8" });
    return { success: true, output };
  } catch (error: any) {
    return { success: false, output: error.stdout || error.message };
  }
}

function fixWithClaude(buildErrors: string): ClaudeResult | null {
  const prompt = `The TypeScript build is failing with these errors:

${buildErrors}

Read the files mentioned in the errors and fix them.
Only fix TypeScript errors (types, imports, syntax).
Don't change the business logic.`;

  try {
    const result = execSync(
      `claude -p "${prompt.replace(/"/g, '\\"').replace(/\n/g, "\\n")}" --output-format json --allowedTools "Read,Write,Edit,Grep,Glob"`,
      { encoding: "utf-8", timeout: 300000 }
    );
    return JSON.parse(result);
  } catch {
    return null;
  }
}

function main() {
  const maxAttempts = 3;
  let totalCost = 0;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    console.log(`\n--- Build attempt ${attempt}/${maxAttempts} ---`);

    const { success, output } = runBuild();

    if (success) {
      console.log("Build successful!");
      console.log(`Total fix cost: $${totalCost.toFixed(4)}`);
      return;
    }

    console.log("Build failed. Asking Claude to fix...");

    const fix = fixWithClaude(output.slice(0, 3000));

    if (fix && !fix.is_error) {
      totalCost += fix.cost_usd || 0;
      console.log(`Fix applied ($${(fix.cost_usd || 0).toFixed(4)})`);
    } else {
      console.log("Claude fix failed");
    }
  }

  console.log(`Build still failing after ${maxAttempts} attempts`);
  console.log(`Total cost: $${totalCost.toFixed(4)}`);
  process.exit(1);
}

main();

Script 3: Project Monitoring and Report

#!/usr/bin/env npx tsx
/**
 * Generates a project health report.
 * Usage: npx tsx scripts/project-health.ts
 */

import { execSync } from "child_process";
import * as fs from "fs";

interface ClaudeResult {
  result: string;
  is_error: boolean;
  cost_usd: number;
  duration_ms: number;
}

function runClaude(prompt: string, tools: string[]): ClaudeResult | null {
  try {
    const result = execSync(
      `claude -p "${prompt.replace(/"/g, '\\"')}" --output-format json --allowedTools "${tools.join(",")}"`,
      { encoding: "utf-8", timeout: 300000, maxBuffer: 10 * 1024 * 1024 }
    );
    return JSON.parse(result);
  } catch {
    return null;
  }
}

interface HealthCheck {
  name: string;
  prompt: string;
  tools: string[];
}

const checks: HealthCheck[] = [
  {
    name: "Code Quality",
    prompt: "Analyze src/ for code quality: complexity, duplication, naming. Score 1-10 with a brief justification.",
    tools: ["Read", "Grep", "Glob"],
  },
  {
    name: "Security",
    prompt: "Look in src/ for security problems: secrets, injection, eval, unsafe patterns. List findings with severity.",
    tools: ["Read", "Grep", "Glob"],
  },
  {
    name: "Dependencies",
    prompt: "Analyze package.json and look for: outdated deps, unused deps, known vulnerabilities. Report findings.",
    tools: ["Read", "Grep", "Glob"],
  },
];

async function main() {
  console.log("Project Health Report\n");

  const results: { name: string; result: string; cost: number }[] = [];
  let totalCost = 0;

  for (const check of checks) {
    console.log(`Running: ${check.name}...`);
    const output = runClaude(check.prompt, check.tools);

    if (output && !output.is_error) {
      results.push({
        name: check.name,
        result: output.result,
        cost: output.cost_usd || 0,
      });
      totalCost += output.cost_usd || 0;
    } else {
      results.push({
        name: check.name,
        result: "Check failed",
        cost: 0,
      });
    }
  }

  const report = [
    `# Project Health Report — ${new Date().toISOString().split("T")[0]}`,
    "",
    ...results.map((r) => [
      `## ${r.name}`,
      "",
      r.result,
      "",
      `*Cost: $${r.cost.toFixed(4)}*`,
      "",
      "---",
      "",
    ]).flat(),
    `**Total cost: $${totalCost.toFixed(4)}**`,
  ].join("\n");

  fs.mkdirSync("reports", { recursive: true });
  const reportPath = `reports/health-${new Date().toISOString().split("T")[0]}.md`;
  fs.writeFileSync(reportPath, report);

  console.log(`\nReport saved: ${reportPath}`);
  console.log(`Total cost: $${totalCost.toFixed(4)}`);
}

main();

Parallel Execution in TypeScript

Promise.all for concurrent analysis

import { execSync } from "child_process";

interface ClaudeResult {
  result: string;
  is_error: boolean;
  cost_usd: number;
}

async function analyzeModuleAsync(modulePath: string): Promise<{
  path: string;
  analysis: ClaudeResult | null;
}> {
  return new Promise((resolve) => {
    try {
      const output = execSync(
        `claude -p "Analyze ${modulePath}: structure, exports, issues. Concise." --output-format json --allowedTools "Read,Grep,Glob"`,
        { encoding: "utf-8", timeout: 120000 }
      );
      resolve({ path: modulePath, analysis: JSON.parse(output) });
    } catch {
      resolve({ path: modulePath, analysis: null });
    }
  });
}

async function main() {
  const modules = ["src/api/", "src/components/", "src/services/"];

  console.log(`Analyzing ${modules.length} modules in parallel...\n`);

  const results = await Promise.all(
    modules.map((m) => analyzeModuleAsync(m))
  );

  let totalCost = 0;

  for (const { path, analysis } of results) {
    console.log(`\n## ${path}`);
    if (analysis && !analysis.is_error) {
      console.log(analysis.result.slice(0, 300));
      totalCost += analysis.cost_usd || 0;
    } else {
      console.log("Analysis failed");
    }
  }

  console.log(`\nTotal cost: $${totalCost.toFixed(4)}`);
}

main();

Comparison: Python SDK vs TypeScript SDK

When to use Python

ScenarioWhy Python
CI/CD scriptsPython is the standard in DevOps and CI pipelines
Data processingPandas, numpy to analyze results
ML/AI workflowsIntegration with the ML ecosystem
Backend automationMigration, seeding, maintenance scripts
Quick scriptsLess boilerplate for one-off scripts

When to use TypeScript

ScenarioWhy TypeScript
Build toolsIntegration with esbuild, Vite, webpack
Frontend toolingScripts that interact with the frontend codebase
Type safetyStatic typing to parse Claude's responses
Node.js ecosystemnpm scripts, JS development tools
Full-stack JS projectsLanguage consistency across the whole stack

Direct comparison

AspectPythonTypeScript
Invocationsubprocess.run()execSync() / exec()
Asyncasyncio / ThreadPoolExecutorPromise.all() / async/await
JSON parsingjson.loads()JSON.parse()
TypingOptional (type hints)Native and strict
SDK packagesubprocess approach@anthropic-ai/claude-code
BoilerplateLessMore (types, interfaces)
Error handlingtry/excepttry/catch
StreamingPopen + line iterationspawn + event listeners
CI ecosystemMore commonLess common
Script runnerpython script.pynpx tsx script.ts

The decision rule

Is your project JS/TS?  ──→ TypeScript
Is it a CI/CD script?   ──→ Python
Do you need strong typing for the results? ──→ TypeScript
Is it a quick one-off script? ──→ Python
Does it integrate with JS build tools? ──→ TypeScript
Does it integrate with data pipelines? ──→ Python

If you're not sure: use the language you use most day-to-day. Both work equally well for invoking Claude Code in headless mode.


Integration with npm scripts

package.json

{
  "scripts": {
    "review": "npx tsx scripts/code-review.ts",
    "docs:generate": "npx tsx scripts/gen-component-docs.ts",
    "health": "npx tsx scripts/project-health.ts",
    "build:fix": "npx tsx scripts/build-validator.ts",
    "changelog": "npx tsx scripts/changelog.ts"
  },
  "devDependencies": {
    "tsx": "^4.0.0",
    "typescript": "^5.0.0"
  }
}
npm run review
npm run docs:generate
npm run health
npm run build:fix

This integrates the automation scripts directly into your usual npm workflow.


Troubleshooting

"Cannot find module '@anthropic-ai/claude-code'"

Cause: The package isn't installed.

Solution:

npm install @anthropic-ai/claude-code

If the package isn't available in your version of npm or doesn't yet exist as a public package, use the subprocess approach, which works with any Claude CLI installation.

"execSync: command not found: claude"

Cause: The Claude CLI isn't in the PATH when Node.js runs the subprocess.

Solution:

import { execSync } from "child_process";

const claudePath = execSync("which claude", { encoding: "utf-8" }).trim();

const result = execSync(
  `${claudePath} -p "prompt" --output-format json`,
  { encoding: "utf-8" }
);

"MaxBuffer exceeded"

Cause: Claude's output exceeds execSync's default buffer (1MB).

Solution:

const result = execSync(cmd, {
  encoding: "utf-8",
  maxBuffer: 10 * 1024 * 1024, // 10MB
});

"SyntaxError: Unexpected token in JSON"

Cause: The output includes non-JSON text (warnings, mixed stderr).

Solution:

const raw = execSync(cmd, { encoding: "utf-8" });
const jsonStart = raw.indexOf("{");
const jsonEnd = raw.lastIndexOf("}") + 1;
if (jsonStart >= 0 && jsonEnd > jsonStart) {
  const output = JSON.parse(raw.slice(jsonStart, jsonEnd));
}

"Timeout with npx tsx"

Cause: npx tsx adds startup overhead. The execSync timeout may not be enough.

Solution: Increase the timeout or compile the script to JavaScript first:

npx tsc scripts/review.ts --outDir dist/
node dist/review.js

Exercises

Exercise 1: Headless "Hello World" in TypeScript (Easy)

Write a TypeScript script that runs Claude in headless mode to count .ts files in src/, parses the JSON, and prints the result and cost.

See solution
#!/usr/bin/env npx tsx
import { execSync } from "child_process";

const result = execSync(
  'claude -p "How many .ts files are in src/?" --output-format json --allowedTools "Glob"',
  { encoding: "utf-8", timeout: 60000 }
);

const output = JSON.parse(result);
console.log(`Result: ${output.result}`);
console.log(`Cost: $${(output.cost_usd || 0).toFixed(4)}`);

Exercise 2: Reusable typed function (Easy)

Create an askClaude function with TypeScript types for input and output that encapsulates the headless invocation with complete error handling.

See solution
import { execSync } from "child_process";

interface ClaudeInput {
  prompt: string;
  tools?: string[];
  timeoutMs?: number;
}

interface ClaudeSuccess {
  is_error: false;
  result: string;
  cost_usd: number;
  duration_ms: number;
}

interface ClaudeFailure {
  is_error: true;
  error: string;
}

type ClaudeOutput = ClaudeSuccess | ClaudeFailure;

function askClaude(input: ClaudeInput): ClaudeOutput {
  const { prompt, tools = [], timeoutMs = 300000 } = input;
  const toolsArg = tools.length ? `--allowedTools "${tools.join(",")}"` : "";
  const cmd = `claude -p "${prompt.replace(/"/g, '\\"')}" --output-format json ${toolsArg}`;

  try {
    const raw = execSync(cmd, {
      encoding: "utf-8",
      timeout: timeoutMs,
      maxBuffer: 10 * 1024 * 1024,
    });
    const parsed = JSON.parse(raw);
    return { is_error: false, result: parsed.result, cost_usd: parsed.cost_usd || 0, duration_ms: parsed.duration_ms || 0 };
  } catch (error: any) {
    return { is_error: true, error: error.message || "Unknown error" };
  }
}

const output = askClaude({ prompt: "List exports of src/api/", tools: ["Read", "Grep"] });
if (!output.is_error) {
  console.log(output.result);
} else {
  console.error(output.error);
}

Exercise 3: Component docs generator (Medium)

Write a script that reads all the .tsx files in src/components/, asks Claude to document each one, and generates a docs/components.md file.

See solution
#!/usr/bin/env npx tsx
import { execSync } from "child_process";
import * as fs from "fs";

function runClaude(prompt: string, tools: string[]): string | null {
  try {
    const result = execSync(
      `claude -p "${prompt.replace(/"/g, '\\"')}" --output-format json --allowedTools "${tools.join(",")}"`,
      { encoding: "utf-8", timeout: 120000, maxBuffer: 5 * 1024 * 1024 }
    );
    const parsed = JSON.parse(result);
    return parsed.is_error ? null : parsed.result;
  } catch {
    return null;
  }
}

const files = execSync('find src/components -name "*.tsx" -not -name "*.test.*"', { encoding: "utf-8" })
  .trim()
  .split("\n")
  .filter(Boolean);

const docs: string[] = ["# Components\n"];

for (const file of files) {
  console.log(`Documenting: ${file}`);
  const doc = runClaude(`Read ${file}. Document: name, props, description, example. Concise Markdown.`, ["Read"]);
  if (doc) {
    docs.push(doc, "\n---\n");
  }
}

fs.mkdirSync("docs", { recursive: true });
fs.writeFileSync("docs/components.md", docs.join("\n"));
console.log("Done: docs/components.md");

Exercise 4: Build fixer with retry (Medium)

Write a TypeScript script that: (1) runs npm run build, (2) if it fails, passes the errors to Claude to fix, (3) repeats up to 3 times, (4) reports whether it succeeded and the total cost.

See solution
#!/usr/bin/env npx tsx
import { execSync } from "child_process";

function build(): { ok: boolean; output: string } {
  try {
    return { ok: true, output: execSync("npm run build 2>&1", { encoding: "utf-8" }) };
  } catch (e: any) {
    return { ok: false, output: e.stdout || e.message };
  }
}

function fixWithClaude(errors: string): number {
  try {
    const prompt = errors.slice(0, 3000).replace(/"/g, '\\"').replace(/\n/g, "\\n");
    const result = execSync(
      `claude -p "Fix these TypeScript build errors:\\n${prompt}" --output-format json --allowedTools "Read,Write,Edit,Grep,Glob"`,
      { encoding: "utf-8", timeout: 300000 }
    );
    return JSON.parse(result).cost_usd || 0;
  } catch {
    return 0;
  }
}

let totalCost = 0;
for (let i = 1; i <= 3; i++) {
  console.log(`\nAttempt ${i}/3`);
  const { ok, output } = build();
  if (ok) {
    console.log(`Build passed! Cost: $${totalCost.toFixed(4)}`);
    process.exit(0);
  }
  totalCost += fixWithClaude(output);
}

console.log(`Build still failing. Cost: $${totalCost.toFixed(4)}`);
process.exit(1);

Exercise 5: Parallel analysis with Promise.all (Hard)

Write a script that analyzes 4 directories in parallel, each with its own headless invocation, using Promise.all. Consolidate the results into a Markdown report.

See solution
#!/usr/bin/env npx tsx
import { exec } from "child_process";
import { promisify } from "util";
import * as fs from "fs";

const execAsync = promisify(exec);

async function analyze(dir: string): Promise<{ dir: string; result: string; cost: number }> {
  try {
    const { stdout } = await execAsync(
      `claude -p "Analyze ${dir}: files, exports, issues. Concise." --output-format json --allowedTools "Read,Grep,Glob"`,
      { encoding: "utf-8", timeout: 120000, maxBuffer: 5 * 1024 * 1024 }
    );
    const parsed = JSON.parse(stdout);
    return { dir, result: parsed.result || "No result", cost: parsed.cost_usd || 0 };
  } catch (e: any) {
    return { dir, result: `Error: ${e.message}`, cost: 0 };
  }
}

async function main() {
  const dirs = ["src/api/", "src/components/", "src/services/", "src/utils/"];
  console.log(`Analyzing ${dirs.length} modules in parallel...\n`);

  const results = await Promise.all(dirs.map(analyze));

  const totalCost = results.reduce((sum, r) => sum + r.cost, 0);

  const report = [
    `# Module Analysis — ${new Date().toISOString().split("T")[0]}`,
    "",
    ...results.flatMap((r) => [`## ${r.dir}`, "", r.result, "", "---", ""]),
    `**Total cost: $${totalCost.toFixed(4)}**`,
  ].join("\n");

  fs.mkdirSync("reports", { recursive: true });
  fs.writeFileSync("reports/analysis.md", report);
  console.log(`Report: reports/analysis.md ($${totalCost.toFixed(4)})`);
}

main();

Summary

  • TypeScript offers two approaches for the headless SDK: child_process (subprocess) and @anthropic-ai/claude-code (native package)
  • execSync for simple scripts, exec (promisified) for async, spawn for streaming
  • TypeScript's static typing gives safety when parsing Claude's JSON responses
  • Scripts are run with npx tsx for direct TypeScript support without compilation
  • Promise.all enables parallel analysis of multiple modules
  • Direct integration with npm scripts (npm run review, npm run docs:generate)
  • Python vs TypeScript: Python for CI/CD and data, TypeScript for build tools and JS projects
  • The SDK package (@anthropic-ai/claude-code) offers a cleaner API but requires installation — subprocess always works
  • maxBuffer must be increased (10MB+) for large Claude outputs

Additional Resources

  1. Claude Code CLI Reference — The -p, --output-format, --allowedTools flags
  2. Node.js child_process — Official documentation of exec, execSync, spawn
  3. tsx (TypeScript Execute) — TypeScript runner for scripts
  4. TypeScript Handbook — Reference for types and interfaces
  5. Claude Code Best Practices — Automation best practices
  6. npm Scripts — Integration with npm
  7. Claude Code Hooks — Hooks that complement the SDK
  8. Claude Code Overview — General context of Claude Code

Next capsule: In capsule 06 (the project) you integrate everything: hooks for all the events + an SDK script that orchestrates the complete pipeline. SessionStart configures, PreToolUse validates, PostToolUse lints, SubagentStop reports, and a Python script triggers and processes the whole flow. The project closes the module by demonstrating that hooks + SDK turn Claude Code into an automated development system.