How I use Claude to navigate a 200K-line monorepo without getting lost
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.
#!/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:
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:
# ~/.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:
- Start a Claude Project with the codebase map in the system context
- Ask "which service handles X?" — Claude answers from the map
- Run
get_service_context('services/payments')and paste into the conversation - Ask "show me the payment flow" — Claude gives a specific answer
- 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.