writing/tutorial/2026/07
TutorialJul 6, 2026·30 min read

Build a Long-Running Code Review Agent with Claude Fable 5 and TypeScript

Claude Fable 5 can work for days at a time — planning across stages, chaining tool calls, and self-checking its output. In this tutorial you build a full TypeScript code-review agent that leverages Fable 5's 1M-token context, extended thinking, and 90% prompt-caching discounts.

Claude Fable 5 is Anthropic's most capable model for autonomous, long-horizon work. Released June 9 2026 — and fully restored after a brief export-control suspension on July 1 2026 — it brings a 1-million-token context window, up to 128k output tokens per response, and native extended thinking to the Messages API. With usage-based pricing switching over on July 7 2026, there has never been a better moment to start building with it.

In this tutorial you will build a TypeScript CLI agent that performs a deep code review of any project directory. The agent reads files with custom tools, applies extended thinking to reason about security and architecture, and uses prompt caching to cut API costs by up to 90%. This is the "whole-job delegation" pattern Fable 5 was designed for: you hand it a complex task, set a few guardrails, and let it work.

Prerequisites

  • Node.js 20+ — verify with node --version
  • TypeScript familiarity and async/await comfort
  • An Anthropic API key — generate one at console.anthropic.com
  • A code editor (VS Code recommended)

No GPU hardware is needed. All inference runs on Anthropic's hosted API.

What You'll Build

By the end of this tutorial you will have a CLI command:

npx tsx review-agent.ts ./my-project

It launches a Fable 5 agent that autonomously explores your codebase using file-system tools, applies extended thinking to spot security vulnerabilities, and streams a structured report with Critical, High, Medium, and Recommendation sections.

The complete agent is roughly 250 lines of TypeScript — small enough to fully understand, real enough to use in production.

Understanding Claude Fable 5

A short model overview before writing any code.

What makes Fable 5 different from Sonnet 5

Claude Sonnet 5Claude Fable 5
Context window200k tokens1M tokens
Max output64k tokens128k tokens
Best forFast, iterative responsesLong, multi-step autonomous tasks
Input pricing$3 / MTok$10 / MTok
Output pricing$15 / MTok$50 / MTok
Cached input$0.30 / MTok$1 / MTok

Use Sonnet 5 for interactive features. Use Fable 5 when you need an agent that plans across many steps, delegates sub-tasks, checks its own work, and produces a coherent result over dozens of tool calls.

Pricing note

Fable 5 API calls use usage-based billing starting July 7 2026 — no subscription credit required. At $10 per million input tokens with prompt caching giving 90% off cached reads, a thorough audit of a 50-file TypeScript project typically costs between $0.20 and $0.80 depending on how many turns the agent takes.

Step 1: Project Setup

mkdir fable5-reviewer && cd fable5-reviewer
npm init -y
npm install @anthropic-ai/sdk dotenv
npm install -D typescript tsx @types/node

Create tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true
  }
}

Create .env:

ANTHROPIC_API_KEY=sk-ant-...

Never commit .env — add it to .gitignore immediately.

Step 2: Verify Streaming Works with Fable 5

Non-streaming calls to Fable 5 can take 60 seconds or more on complex prompts. Always stream. Here is a minimal test:

// stream-test.ts
import Anthropic from '@anthropic-ai/sdk';
import 'dotenv/config';
 
const client = new Anthropic();
 
async function main(): Promise<void> {
  const stream = client.messages.stream({
    model: 'claude-fable-5',
    max_tokens: 512,
    messages: [
      {
        role: 'user',
        content: 'List 5 things to check in a TypeScript security audit. Be brief.',
      },
    ],
  });
 
  for await (const chunk of stream) {
    if (
      chunk.type === 'content_block_delta' &&
      chunk.delta.type === 'text_delta'
    ) {
      process.stdout.write(chunk.delta.text);
    }
  }
 
  const final = await stream.finalMessage();
  console.log(
    `\n\nTokens: ${final.usage.input_tokens} in / ${final.usage.output_tokens} out`
  );
}
 
main();

Run it:

npx tsx stream-test.ts

You should see the list stream in token by token. If you see model_not_available, check that access has been re-enabled on your account at console.anthropic.com.

Step 3: Define Custom File-System Tools

Tools are how Fable 5 reads your project. Define three: list_directory, read_file, and search_pattern.

// tools.ts
import { readFileSync, readdirSync, statSync } from 'fs';
import { join, extname } from 'path';
import type Anthropic from '@anthropic-ai/sdk';
 
const ALLOWED_EXTENSIONS = new Set([
  '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
  '.json', '.md', '.yaml', '.yml', '.toml', '.env.example',
]);
 
export const toolDefinitions: Anthropic.Tool[] = [
  {
    name: 'list_directory',
    description: 'List files and directories at a given path.',
    input_schema: {
      type: 'object' as const,
      properties: {
        path: { type: 'string', description: 'Directory path to list' },
        recursive: { type: 'boolean', description: 'Recurse into subdirectories' },
      },
      required: ['path'],
    },
  },
  {
    name: 'read_file',
    description: 'Read the text content of a source file.',
    input_schema: {
      type: 'object' as const,
      properties: {
        path: { type: 'string', description: 'Absolute or relative file path' },
      },
      required: ['path'],
    },
  },
  {
    name: 'search_pattern',
    description: 'Search for a regex pattern across all source files in a directory. Returns matching lines with file and line number.',
    input_schema: {
      type: 'object' as const,
      properties: {
        pattern: { type: 'string', description: 'Regex pattern to search for' },
        directory: { type: 'string', description: 'Root directory to search' },
        maxResults: {
          type: 'number',
          description: 'Maximum lines to return (default 50)',
        },
      },
      required: ['pattern', 'directory'],
    },
  },
];
 
function walkDir(dir: string, recursive: boolean): string[] {
  const results: string[] = [];
  for (const name of readdirSync(dir)) {
    if (name.startsWith('.') || name === 'node_modules' || name === 'dist') continue;
    const full = join(dir, name);
    const stat = statSync(full);
    if (stat.isDirectory()) {
      results.push(`${full}/`);
      if (recursive) results.push(...walkDir(full, true));
    } else {
      results.push(full);
    }
  }
  return results;
}
 
function grepFiles(pattern: string, dir: string, maxResults: number): string[] {
  const regex = new RegExp(pattern, 'gi');
  const matches: string[] = [];
  const files = walkDir(dir, true).filter(
    (f) => !f.endsWith('/') && ALLOWED_EXTENSIONS.has(extname(f))
  );
  for (const file of files) {
    if (matches.length >= maxResults) break;
    try {
      const lines = readFileSync(file, 'utf-8').split('\n');
      for (let i = 0; i < lines.length; i++) {
        if (regex.test(lines[i])) {
          matches.push(`${file}:${i + 1}: ${lines[i].trim()}`);
          if (matches.length >= maxResults) break;
        }
      }
    } catch {
      // skip unreadable files
    }
  }
  return matches;
}
 
export function executeTool(
  name: string,
  input: Record<string, unknown>
): string {
  try {
    if (name === 'list_directory') {
      const path = input.path as string;
      const recursive = (input.recursive as boolean) ?? false;
      const entries = walkDir(path, recursive);
      return entries.slice(0, 300).join('\n') || '(empty directory)';
    }
 
    if (name === 'read_file') {
      const path = input.path as string;
      if (!ALLOWED_EXTENSIONS.has(extname(path))) {
        return `Skipped: extension ${extname(path)} is not in the allow-list.`;
      }
      const content = readFileSync(path, 'utf-8');
      // Truncate files larger than 50k chars to protect the context budget
      return content.length > 50_000
        ? content.slice(0, 50_000) + '\n\n[file truncated — over 50 000 characters]'
        : content;
    }
 
    if (name === 'search_pattern') {
      const pattern = input.pattern as string;
      const directory = input.directory as string;
      const maxResults = (input.maxResults as number) ?? 50;
      const hits = grepFiles(pattern, directory, maxResults);
      return hits.length > 0 ? hits.join('\n') : 'No matches found.';
    }
 
    return `Unknown tool: ${name}`;
  } catch (err) {
    return `Tool error: ${(err as Error).message}`;
  }
}

The 50k character truncation per file is a deliberate safeguard. Even with Fable 5's 1M token context, a single enormous generated file could consume tens of thousands of tokens. Trim aggressively; the agent will ask for more context if needed.

Step 4: The Core Agent Loop

The loop drives Fable 5 until it returns a final answer. On each turn: call the API, dispatch any tool calls, feed results back, repeat.

// agent-loop.ts
import Anthropic from '@anthropic-ai/sdk';
import { toolDefinitions, executeTool } from './tools.js';
 
const client = new Anthropic();
 
const SYSTEM = `You are a senior software engineer performing a thorough, systematic code review.
 
Workflow:
1. Call list_directory on the project root (recursive: true) to understand the structure.
2. Read key config files first: package.json, tsconfig.json, .eslintrc, next.config.js.
3. Read each major source file in turn.
4. Use search_pattern to look for common security issues: eval, dangerouslySetInnerHTML, SQL string building, hardcoded secrets, process.env without validation.
5. After reading all relevant files, write a structured report.
 
Report format:
## Critical Issues
## High Priority
## Medium Priority  
## Recommendations`;
 
export async function runReviewAgent(projectPath: string): Promise<void> {
  const messages: Anthropic.MessageParam[] = [
    {
      role: 'user',
      content: `Perform a comprehensive code review of the project at: ${projectPath}
 
Review everything — security, correctness, TypeScript quality, error handling, and architecture. Start by exploring the project structure.`,
    },
  ];
 
  let iteration = 0;
  const maxIterations = 40;
 
  console.log(`\nFable 5 code review — ${projectPath}\n${'─'.repeat(60)}\n`);
 
  while (iteration < maxIterations) {
    iteration++;
 
    const response = await client.messages.create({
      model: 'claude-fable-5',
      max_tokens: 8192,
      system: SYSTEM,
      tools: toolDefinitions,
      messages,
    });
 
    // Add the assistant turn before processing
    messages.push({ role: 'assistant', content: response.content });
 
    const toolResults: Anthropic.ToolResultBlockParam[] = [];
 
    for (const block of response.content) {
      if (block.type === 'text' && block.text) {
        process.stdout.write(block.text);
      } else if (block.type === 'tool_use') {
        const input = block.input as Record<string, unknown>;
        console.log(`\n  [${block.name}] ${JSON.stringify(input).slice(0, 80)}`);
        const result = executeTool(block.name, input);
        toolResults.push({
          type: 'tool_result',
          tool_use_id: block.id,
          content: result,
        });
      }
    }
 
    if (toolResults.length > 0) {
      messages.push({ role: 'user', content: toolResults });
      continue;
    }
 
    if (response.stop_reason === 'end_turn') {
      console.log(`\n${'─'.repeat(60)}\nDone — ${iteration} iterations`);
      break;
    }
 
    console.warn(`\nUnexpected stop_reason: ${response.stop_reason}`);
    break;
  }
 
  if (iteration >= maxIterations) {
    console.warn('Max iterations reached — review may be incomplete.');
  }
}

Fable 5 typically finishes a medium TypeScript project review in 8–18 iterations. The 40-iteration cap is a safety backstop for very large monorepos.

Step 5: Extended Thinking for Deep Security Analysis

Extended thinking lets Fable 5 reason privately before writing its final response. For security analysis this matters: vulnerability chains require more careful reasoning than surface-level pattern matching. Enable it with the thinking parameter:

// deep-review.ts
import Anthropic from '@anthropic-ai/sdk';
import { toolDefinitions, executeTool } from './tools.js';
 
const client = new Anthropic();
 
export async function runDeepSecurityAudit(projectPath: string): Promise<void> {
  const messages: Anthropic.MessageParam[] = [
    {
      role: 'user',
      content: `Perform a deep security audit of ${projectPath}.
 
Focus on:
- Injection vulnerabilities (SQL, command, path traversal)
- Authentication and authorization flaws
- Sensitive data exposure and secrets in code
- Insecure deserialization
- Vulnerable dependencies in package.json
 
Examine all source files carefully, then provide a detailed security report.`,
    },
  ];
 
  let iteration = 0;
 
  while (iteration < 30) {
    iteration++;
 
    const response = await client.messages.create({
      model: 'claude-fable-5',
      max_tokens: 16000,
      tools: toolDefinitions,
      messages,
      thinking: {
        type: 'enabled',
        budget_tokens: 8000, // Allow up to 8k tokens of private reasoning per turn
      },
    });
 
    messages.push({ role: 'assistant', content: response.content });
    const toolResults: Anthropic.ToolResultBlockParam[] = [];
 
    for (const block of response.content) {
      if (block.type === 'thinking') {
        // You can display or omit thinking blocks — they are never billed at output rates
        console.log(`\n  [thinking — ${block.thinking.length} chars]`);
      } else if (block.type === 'text' && block.text) {
        process.stdout.write(block.text);
      } else if (block.type === 'tool_use') {
        console.log(`\n  [${block.name}]`);
        const result = executeTool(
          block.name,
          block.input as Record<string, unknown>
        );
        toolResults.push({
          type: 'tool_result',
          tool_use_id: block.id,
          content: result,
        });
      }
    }
 
    if (toolResults.length > 0) {
      messages.push({ role: 'user', content: toolResults });
      continue;
    }
    if (response.stop_reason === 'end_turn') break;
  }
}

A budget_tokens value of 8000 is a good starting point. For security audits, values between 4000 and 16 000 are practical — larger budgets enable deeper reasoning chains but add to token count. Thinking tokens are billed at input rates, not output rates.

Step 6: Prompt Caching for 90% Cost Savings

At $10 per million input tokens, a 40-turn agent run on a large project can accumulate significant input costs — the full conversation history is sent on every turn. Prompt caching stores static blocks on Anthropic's servers for 5 minutes, billing those reads at just $1 per million tokens.

Cache your system prompt and any large reference file you read once and reference repeatedly:

// cached-agent.ts
import Anthropic from '@anthropic-ai/sdk';
import { readFileSync } from 'fs';
import { toolDefinitions, executeTool } from './tools.js';
 
const client = new Anthropic();
 
export async function runCachedReview(projectPath: string): Promise<void> {
  // Read package.json once — it will be cached across all subsequent turns
  let packageJson = '(no package.json)';
  try {
    packageJson = readFileSync(`${projectPath}/package.json`, 'utf-8');
  } catch { /* not found */ }
 
  // System prompt with a cacheable block for the large reference content
  const systemContent: Anthropic.TextBlockParam[] = [
    {
      type: 'text',
      text: `You are a code security and quality reviewer.\n\nproject package.json:\n\`\`\`json\n${packageJson}\n\`\`\`\n\nReference this when analysing dependencies.`,
      cache_control: { type: 'ephemeral' }, // Cache this large block for 5 minutes
    },
    {
      type: 'text',
      text: 'Use the provided tools to read source files. Report issues by severity.',
      // Shorter blocks are not worth caching
    },
  ];
 
  const messages: Anthropic.MessageParam[] = [
    {
      role: 'user',
      content: `Review all TypeScript files in ${projectPath} for security and code quality issues.`,
    },
  ];
 
  let iteration = 0;
  let cachedTokensSaved = 0;
 
  while (iteration < 30) {
    iteration++;
 
    const response = await client.messages.create({
      model: 'claude-fable-5',
      max_tokens: 8192,
      system: systemContent,
      tools: toolDefinitions,
      messages,
    });
 
    const usage = response.usage as Anthropic.Usage & {
      cache_read_input_tokens?: number;
    };
    if (usage.cache_read_input_tokens) {
      cachedTokensSaved += usage.cache_read_input_tokens;
    }
 
    messages.push({ role: 'assistant', content: response.content });
    const toolResults: Anthropic.ToolResultBlockParam[] = [];
 
    for (const block of response.content) {
      if (block.type === 'text') process.stdout.write(block.text);
      else if (block.type === 'tool_use') {
        const result = executeTool(
          block.name,
          block.input as Record<string, unknown>
        );
        toolResults.push({
          type: 'tool_result',
          tool_use_id: block.id,
          content: result,
        });
      }
    }
 
    if (toolResults.length > 0) {
      messages.push({ role: 'user', content: toolResults });
      continue;
    }
 
    if (response.stop_reason === 'end_turn') {
      // 9/10 of cached tokens cost nothing compared to standard input rate
      const savedUsd = ((cachedTokensSaved * 9) / 10 / 1_000_000) * 10;
      console.log(
        `\n\nCache saved ~$${savedUsd.toFixed(4)} (${cachedTokensSaved.toLocaleString()} tokens at 90% discount)`
      );
      break;
    }
  }
}

Prompt caching pays off most on blocks that appear unchanged across many turns — system prompts, large reference documents, and shared configuration files. The 5-minute cache window easily covers a complete agent run.

Step 7: The Entry-Point CLI

Wire everything into a single runnable file:

// review-agent.ts
import 'dotenv/config';
import { runCachedReview } from './cached-agent.js';
 
const projectPath = process.argv[2];
 
if (!projectPath) {
  console.error('Usage: npx tsx review-agent.ts <path>');
  process.exit(1);
}
 
console.log('Claude Fable 5 Code Reviewer');
console.log(`Path   : ${projectPath}`);
console.log('Model  : claude-fable-5');
console.log('Context: 1M tokens  |  Max output: 128k tokens\n');
 
runCachedReview(projectPath).catch((err) => {
  console.error('\nAgent error:', err.message);
  process.exit(1);
});

Run a review:

npx tsx review-agent.ts ./src

Fable 5 will explore the directory structure, read files one by one, search for security patterns, and write the final report — typically within 2–5 minutes for a medium TypeScript project.

Troubleshooting

Streaming timeout — Always use client.messages.stream() when extended thinking is enabled. Non-streaming calls with large budget_tokens values will time out at the HTTP layer before the model finishes.

model_not_available — Fable 5 access was restored on July 1 2026 after the June export-control suspension. If you still see this error, check your account tier at console.anthropic.com — Fable 5 requires a usage-credit-enabled API key.

Context length exceeded — With 1M tokens this is rare, but very large monorepos with many lock files and generated code can hit it. Add '.lock', '.snap', '.d.ts' to the blocked-extension list in tools.ts.

High cost on first run — The first turn of a run does not benefit from cache (there is nothing cached yet). Costs drop by up to 90% from the second turn onward. Profile with a short test run on a small directory before running on a large codebase.

Next Steps

  • Add a file-write tool — let the agent save the report as code-review.md in the project root
  • GitHub Actions integration — run the agent automatically on each pull request and post findings as PR comments
  • Specialist sub-agents — use the Beta Managed Agents API (client.beta.environments.work.worker()) to spin up parallel security, performance, and style reviewers that report back to a Fable 5 orchestrator
  • Batch API for large codebases — use the Messages Batch API for cost-effective analysis of many repos at once; batch pricing is $5 / $25 per million input / output tokens

Conclusion

Claude Fable 5 changes what a single TypeScript agent can do in one uninterrupted run. The 1-million-token context window eliminates the context juggling that makes long-horizon agents brittle. Extended thinking brings careful, multi-step reasoning to security analysis. And prompt caching turns what could be an expensive iterative process into something cost-effective enough to run on every pull request.

The patterns here — streaming first, capping iterations, caching static blocks — are the right foundation for any Fable 5-powered workflow. With usage-based pricing now live, the model is accessible without a subscription tier gate. Start small, measure token usage on real code, then scale up to the tasks that would previously have required a human reviewer sitting at a terminal for hours.