How I use Claude to navigate a 200K-line monorepo without getting lost
← Back
April 4, 2026Claude7 min read

How I use Claude to navigate a 200K-line monorepo without getting lost

Published April 4, 20267 min read

When I joined a project with a 200,000-line TypeScript monorepo, I spent the first two weeks just trying to understand how things connected. I eventually built a three-layer Claude workflow that let me answer "how does feature X work?" in minutes instead of hours. Here is the exact strategy.

The three-layer approach

Trying to give Claude the entire codebase does not work at any scale above trivial. The trick is building a hierarchy: a high-level map you keep in Claude's context permanently, service summaries you load on demand, and individual file contents when you need the specifics.

Layer 1: The codebase map

Generate a concise, structured map of your entire repo. This goes in the Claude Projects system context and stays there permanently.

python
#!/usr/bin/env python3
# generate-codebase-map.py

import os
import anthropic
from pathlib import Path

client = anthropic.Anthropic()


def get_package_structure(root: str) -> str:
    """Get the top-level packages/apps in a monorepo."""
    lines = []
    for item in sorted(os.listdir(root)):
        item_path = os.path.join(root, item)
        if os.path.isdir(item_path) and not item.startswith('.'):
            package_json = os.path.join(item_path, 'package.json')
            if os.path.exists(package_json):
                import json
                try:
                    pkg = json.loads(Path(package_json).read_text())
                    name = pkg.get('name', item)
                    desc = pkg.get('description', '')
                    lines.append(f"  {name}: {desc}" if desc else f"  {name}")
                except Exception:
                    lines.append(f"  {item}")
    return "
".join(lines)


def summarize_package(package_path: str) -> str:
    """Generate a summary of a single package."""
    
    # Get key files
    key_files = []
    for pattern in ['src/index.ts', 'src/main.ts', 'README.md', 'src/routes']:
        full = os.path.join(package_path, pattern)
        if os.path.exists(full):
            if os.path.isfile(full):
                content = Path(full).read_text()[:3000]
                key_files.append(f"=== {pattern} ===
{content}")
    
    if not key_files:
        return "No key files found"
    
    combined = "

".join(key_files)
    
    response = client.messages.create(
        model="claude-haiku-4-5",  # Fast and cheap for summaries
        max_tokens=300,
        messages=[{
            "role": "user",
            "content": f"Write a 3-5 sentence technical summary of this package:

{combined}"
        }]
    )
    return response.content[0].text


# Generate the map
root = "."
structure = get_package_structure(root)
print(f"# Monorepo Map

## Packages
{structure}

## Package Summaries
")

for item in os.listdir(root):
    if os.path.isdir(item) and not item.startswith('.'):
        if os.path.exists(os.path.join(item, 'package.json')):
            summary = summarize_package(item)
            print(f"### {item}
{summary}
")

Layer 2: Service-level summaries on demand

When you need to understand a specific service, generate a deeper summary and paste it into Claude:

python
def get_service_context(service_path: str) -> str:
    """Generate deep context for a specific service."""
    
    parts = []
    
    # Routes / API surface
    routes_dir = os.path.join(service_path, 'src', 'routes')
    if os.path.isdir(routes_dir):
        for route_file in Path(routes_dir).glob('**/*.ts'):
            parts.append(f"=== {route_file} ===
{route_file.read_text()[:2000]}")
    
    # Database models / schemas
    for schema_pattern in ['src/models', 'src/schemas', 'prisma/schema.prisma']:
        schema_path = os.path.join(service_path, schema_pattern)
        if os.path.isdir(schema_path):
            for f in Path(schema_path).glob('**/*.ts'):
                parts.append(f"=== {f} ===
{f.read_text()[:1000]}")
        elif os.path.exists(schema_path):
            parts.append(f"=== {schema_pattern} ===
{Path(schema_path).read_text()[:3000]}")
    
    context = "

".join(parts[:10])  # Limit to 10 key files
    
    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=800,
        messages=[{
            "role": "user",
            "content": f"Summarize the architecture and API surface of this service:

{context}"
        }]
    )
    return response.content[0].text

Layer 3: Targeted file context

For specific questions, only load the relevant files. A shell alias makes this fast:

bash
# ~/.zshrc

# Ask Claude about specific files
ask-code() {
  local question="$1"
  shift
  local files=("$@")
  
  context=""
  for file in "${files[@]}"; do
    context+="=== $file ===
$(cat $file)

"
  done
  
  echo -e "$context" | claude --print     --system "You are a senior engineer familiar with this codebase. Answer specifically."     "$question"
}

# Usage:
# ask-code "How does auth work?" src/middleware/auth.ts src/services/jwt.ts
# ask-code "What does this query return?" src/queries/user.ts src/models/user.ts

The workflow in practice

When I need to add a feature to an unfamiliar service:

  1. Start a Claude Project with the codebase map in the system context
  2. Ask "which service handles X?" — Claude answers from the map
  3. Run get_service_context('services/payments') and paste into the conversation
  4. Ask "show me the payment flow" — Claude gives a specific answer
  5. Load just the relevant files: ask-code "how does charge retry work?" src/services/retry.ts src/jobs/payment.ts

This three-layer approach means I never burn context on files that are not relevant to the current question. The map layer is tiny (5-10KB), the service layer is medium (10-30KB), and the file layer is exactly what the question needs.

Share this
← All Posts7 min read