← All posts
·14 min read

Claude Code Context Management: Stay Sharp Longer

Claude CodeContextWorkflowCLAUDE.md
Claude Code context management: keeping sessions sharp

Why Claude Code quality degrades over a long session

Claude Code has a finite context window. Every file it reads, every code block it writes, every back-and-forth exchange consumes tokens from that window. When the window fills, the model either compacts the conversation (replacing earlier messages with a summary) or truncates the oldest content. Both outcomes have the same effect from a developer's perspective: things Claude knew clearly at the start of the session become fuzzy or disappear entirely.

The degradation does not feel like a hard wall. It feels like a series of small failures: Claude suggests a function it already wrote earlier in the session, generates an import for a module you already decided to remove, or misremembers a constraint you stated thirty minutes ago. The session appears functional. The quality is quietly dropping.

This guide covers the techniques that prevent that drop: structuring CLAUDE.md as a durable anchor, writing session handoffs that survive compaction, budgeting context across parallel agents, and keeping file-read discipline so Claude is not spending tokens re-reading the same content repeatedly. If you are new to CLAUDE.md and want to understand how it fits into the overall configuration system, Claude Code memory setup covers the full memory hierarchy from CLAUDE.md through to per-project and per-session state.

The context window in real numbers

Claude Sonnet and Haiku run with a 200K token context window. Claude Opus 4 offers 1M tokens. What does that mean for a working session?

A rough token budget for a moderately complex feature session:

Content Approximate tokens
System prompt + CLAUDE.md 2,000 to 8,000
Reading a 300-line TypeScript file 2,000 to 3,000
A 10-message conversation about a feature 1,500 to 3,000
Generating a 100-line code block 800 to 1,200
Reading a package.json + tsconfig 500 to 1,000

A session that reads 20 source files, has 40 conversational exchanges, and generates 500 lines of code will consume between 60,000 and 120,000 tokens on Sonnet. That leaves plenty of headroom on a 200K window for a single focused task. The problems start when sessions run across multiple tasks, or when Claude repeatedly re-reads large files to refresh its understanding.

The 1M Opus window is genuinely useful for codebase-wide tasks. But larger windows do not eliminate context management concerns. They delay the degradation point, they do not remove it.

CLAUDE.md as a durable anchor

The most important context management technique is also the simplest: CLAUDE.md. This file is read at the start of every session, which means its contents are always near the top of the context window where they exert the strongest influence on Claude's behaviour. Information in CLAUDE.md does not degrade during a session. It is always present.

Use CLAUDE.md for anything that must remain true regardless of how long the session runs:

# Project: API Gateway

## Architecture (always-true constraints)
- Runtime: Node.js 20.x, TypeScript 5.x strict mode
- Framework: Fastify 4.x (NOT Express, NOT Hapi)
- Database: PostgreSQL 16 via Drizzle ORM (NOT Prisma, NOT raw SQL in app code)
- Auth: JWT validation at the gateway layer, not per-route
- Error format: { code: string, message: string, statusCode: number }

## File structure
- src/routes/         - Fastify route handlers (one file per domain)
- src/services/       - Business logic (no direct DB access, uses repositories)
- src/repositories/   - Drizzle queries (no business logic here)
- src/lib/            - Shared utilities, singleton clients
- src/types/          - Shared TypeScript interfaces

## Hard rules
- NEVER write raw SQL in route handlers or services, use repositories only
- NEVER throw untyped errors, always wrap in AppError from src/lib/errors.ts
- NEVER import directly between services, use the service layer pattern
- ALL async functions must handle errors explicitly (no unhandled promise rejections)
- Environment variables read via src/lib/config.ts, NEVER process.env directly

## Current task context
- Working on: user authentication service
- Completed: route scaffolding, Drizzle schema, JWT middleware
- Next: rate limiting, refresh token rotation

The "Current task context" section is worth maintaining manually at the start of each session. It takes 30 seconds to write and ensures Claude has the project's current state without needing to piece it together from reading multiple files.

A well-structured CLAUDE.md essentially acts as a persistent memory that costs almost no tokens relative to the information it provides. A 3,000-token CLAUDE.md that encodes your entire architectural model is far more valuable than 30,000 tokens of conversational context establishing the same knowledge from scratch.

The session handoff pattern

When a session ends, the context window resets. The next session starts cold. The session handoff pattern solves this by writing a structured summary to disk at the end of each session that the next session can read instead of reconstructing context from scratch.

A handoff file follows a consistent structure:

# Session Handoff - 2026-06-11

## What was done
- Implemented JWT refresh token rotation in src/services/auth.service.ts
- Added rate limiting middleware at src/middleware/rate-limit.ts (10 req/min per IP)
- Fixed the token expiry bug: refresh tokens now correctly reject after 7 days
- Updated Drizzle schema: added refresh_tokens table with user_id FK + expires_at

## Files touched
- src/services/auth.service.ts (modified: refreshToken() method, added rotateToken())
- src/middleware/rate-limit.ts (created)
- src/lib/drizzle/schema.ts (modified: refresh_tokens table)
- src/lib/drizzle/migrations/0012_add_refresh_tokens.sql (created)
- src/routes/auth.ts (modified: POST /auth/refresh route added)

## Current state
- Auth service: complete and tested
- Rate limiting: deployed to middleware stack, active on all auth routes
- Migrations: applied to dev DB, not yet applied to staging

## Blockers / open questions
- Staging migration needs to run before QA can test; confirm with team
- Redis rate limit store vs in-memory: currently in-memory (fine for single instance, needs Redis for multi-instance deploy)

## Next steps
- Run migration on staging: pnpm drizzle-kit migrate --config drizzle.staging.config.ts
- Write integration tests for token rotation flow
- Replace in-memory rate limit store with Redis if horizontal scaling is needed

At the start of the next session, you paste the handoff file path into the conversation or include a CLAUDE.md rule that points to it:

## Session continuity
- At the start of each session, check for a handoff file at docs/session-handoff.md
- Read it before any other task; it contains the current project state
- After completing tasks, update docs/session-handoff.md with what changed

The handoff file should stay under 200 lines. It is not a log. It is a context anchor. If it grows past 200 lines, trim the "what was done" section to just the most recent session.

Compaction: what happens and how to prepare for it

Claude Code automatically compacts the conversation when the context window fills. Compaction replaces the full conversation history with a compressed summary generated by the model. The CLAUDE.md file and the most recent messages survive compaction intact. Earlier messages are summarised.

You can also trigger compaction manually with the /compact command. This is useful when you know you are about to start a new phase of work that does not depend on the details of the current phase.

Preparing for compaction before it happens:

Write a session handoff before running /compact. The auto-compaction summary is generated from whatever is in the context at the time. A deliberately written handoff with specific file paths and state is far more useful than an auto-summary.

Update CLAUDE.md with any decisions made in the current session. If you decided during the session to use Redis instead of in-memory rate limiting, update the CLAUDE.md rule to reflect that. The decision will survive compaction because CLAUDE.md is always re-read.

Finish the current task before the window fills. This sounds obvious but matters practically. A compaction mid-task means the resumed session will re-establish context before continuing. If you can see the context is getting heavy, finish the current output cleanly before it compacts rather than leaving it mid-way through a code block.

A CLAUDE.md rule that reinforces this pattern:

## Compaction and session management
- Before any /compact, write a session summary to docs/session-handoff.md
- After compaction, read docs/session-handoff.md + src/lib/ before continuing
- After any significant decision (architecture, library choice, schema change), update CLAUDE.md
- The session-handoff.md is the single recovery document; keep it current

Controlling what Claude reads

Context is consumed every time Claude reads a file. A session that reads the same large file three times to answer three different questions is spending three times as many tokens as necessary. Disciplined file-read practices significantly extend effective context.

Three techniques that reduce unnecessary reads:

Reference files by path in CLAUDE.md instead of asking Claude to read them. If Claude knows that the Drizzle schema is at src/lib/drizzle/schema.ts, it can work with that information without reading the file on every reference. It will read it when it needs the exact content, but not for every passing mention.

Keep reference files short. A schema file with 200 tables costs far more to read than the same file with 20 tables. If the schema is large, create a schema-summary.md that describes the tables in natural language. Claude reads the summary for context and the actual schema file only when it needs to write a specific query.

Batch related reads. When starting a new task, give Claude a list of all the files it will need at once. Reading 5 files in one batch is more context-efficient than reading them one at a time as each becomes relevant, because sequential reads accumulate the conversational overhead of each request.

Example of efficient task kickoff:

I need to add email verification to the auth flow. The relevant files are:
- src/services/auth.service.ts (current auth logic)
- src/lib/drizzle/schema.ts (database schema)
- src/lib/resend.ts (email client)
- src/routes/auth.ts (existing auth routes)

Read all four, then let's design the implementation.

Claude reads all four files in one pass. The subsequent design conversation does not require re-reading any of them unless the content changes.

Per-agent context budgeting

Claude Code supports spawning subagents (via the --agent flag or programmatically through the API) that each get their own fresh context window. This is useful for long tasks that decompose cleanly into parallel subtasks.

A codebase migration that touches 50 files can be structured as:

  • Agent 1: migrate files in src/routes/ (15 files)
  • Agent 2: migrate files in src/services/ (20 files)
  • Agent 3: migrate files in src/repositories/ and src/lib/ (15 files)

Each agent works with a fresh 200K context window, focused on its own file set. The orchestrating session collects the results and resolves any conflicts.

For this pattern to work, each subagent needs to be fully self-contained in its instructions. It cannot rely on context from the orchestrating session. The agent prompt should include:

You are migrating the service layer from Prisma to Drizzle ORM.

Context:
- Project uses TypeScript 5.x strict mode
- Drizzle client singleton is at src/lib/db.ts (import { db } from '@/lib/db')
- Schema types are at src/lib/drizzle/schema.ts
- All queries use the Drizzle query builder, no raw SQL
- Each service file exports an object with query methods, no classes

Files to migrate (read each, rewrite using Drizzle, output the full updated file):
- src/services/user.service.ts
- src/services/product.service.ts
[...]

Rules:
- Preserve all function signatures exactly (callers must not need to change)
- Return the same types as the Prisma version (may need Drizzle select inference)
- Do not change business logic, only the data access layer

This kind of self-contained agent prompt is essentially a CLAUDE.md for a single bounded task. The subagent gets the architecture context it needs without loading the full project context.

The re-read problem

One of the most context-expensive patterns is asking Claude to re-read a file it read earlier in the session. This happens when the conversation moves away from a file and then returns to it. Claude re-reads because it has partially forgotten the earlier content or wants to confirm its memory is accurate.

You can see this pattern when Claude says things like "Let me check that file again to be sure" or "I should re-read the schema to confirm the column names." Each re-read is expensive: you pay the token cost of reading the file again, plus the conversational overhead.

Two ways to reduce re-reads:

Keep working within a single domain until complete. Finishing all the changes to user.service.ts before switching to auth.service.ts means you spend one file-read budget per service file rather than multiple re-reads as you bounce between them.

Summarise long files after reading them. After Claude reads a 300-line file, ask it to write a 10-line summary of the key exports, types, and constraints. The summary costs 200 tokens. Re-reading the original file costs 2,000. When you need to reference the file later in the session, reference the summary instead.

After reading src/lib/db.ts, summarise the exports and their types in 10 lines.

Claude's summary:

Exports:
- db: DrizzleClient (singleton, env-validated at startup)
- transaction(): wraps a callback in a Drizzle transaction
- healthCheck(): returns boolean, runs SELECT 1

Types used: PostgresJsDatabase from drizzle-orm/postgres-js
Config: reads DATABASE_URL from process.env (checked at module load)
No query functions here - db is passed to repositories

For the rest of the session, you reference this summary rather than asking Claude to re-read the file. The file is re-read only when you need to actually modify it.

Prioritising context for code generation

When the context window is under pressure, Claude's code generation quality degrades before its conversational quality. The model will give coherent-sounding explanations while writing code that contradicts them. To keep code generation sharp, prioritise context for the specific files and types being modified.

Before asking Claude to generate or modify code, give it the types it will use:

// types/api.ts - share these before asking for code generation
export interface ApiResponse<T> {
  data: T;
  error: null;
  statusCode: 200 | 201;
}

export interface ApiError {
  data: null;
  error: { code: string; message: string };
  statusCode: 400 | 401 | 403 | 404 | 500;
}

export type ApiResult<T> = ApiResponse<T> | ApiError;

With these types in the current context, Claude generates code that matches them correctly. Without them, under context pressure, Claude will invent a compatible-looking but non-standard type signature that breaks downstream consumers.

The same principle applies to configuration constants, environment variable names, and API endpoint paths. The more specific information Claude has immediately before a code generation request, the more accurately the output matches your actual codebase.

Monitoring context health

Claude Code does not expose a token counter in the interface, but you can infer context health from response quality. Signs that context is degrading:

  • Claude suggests an approach you explicitly ruled out earlier in the session
  • Code generation imports modules that do not exist in the project
  • Claude asks you to confirm something it knew confidently 20 messages ago
  • Responses become more hedged with "I think" and "if I recall correctly"

When you notice these signs, the most effective response is a brief context refresh before continuing:

Quick refresh before we continue:
- We are using Fastify 4, NOT Express
- The DB client is at src/lib/db.ts, import { db } from '@/lib/db'
- Error handling uses AppError from src/lib/errors.ts
- Current task: adding the rate limiting middleware

This 100-token refresh is much cheaper than the errors it prevents. Add a CLAUDE.md instruction to prompt Claude to request a context refresh when it notices its own uncertainty:

## Context refresh protocol
- If you are uncertain about a project constraint you believe was established earlier,
  ask me for a context refresh before generating code
- Do not silently assume; request confirmation for any architectural detail you are not certain of
- When starting a new sub-task within a session, state what you understand the current
  state to be so I can correct it before you begin

Token-efficient CLAUDE.md structure

A CLAUDE.md that grows without discipline becomes expensive. Every token in CLAUDE.md is spent on every session. Structure it so that high-value, frequently-referenced rules come first, and low-frequency reference content comes last or lives in separate files.

# Project: API Gateway

## Stack (read first, always-true)
[10 lines of core tech choices]

## Hard rules (read second, enforcement patterns)
[15 lines of do/don't rules]

## File structure (reference as needed)
[10 lines of directory map]

## Patterns (reference when generating)
[20 lines of common code patterns]

## Domain-specific context (read only for relevant tasks)
### Auth
[10 lines about the auth subsystem]
### Payments
[10 lines about the payments subsystem, ref: [Claude Code with Stripe](/blog/claude-code-stripe)]
### Email
[10 lines about email, ref: [Claude Code with Resend](/blog/claude-code-resend)]

Domain-specific sections that are rarely needed can reference external files:

## Database schema reference
Full schema: src/lib/drizzle/schema.ts
Schema summary: docs/schema-summary.md (human-readable, updated with migrations)
Read the summary unless you need the exact Drizzle column definitions

This keeps the CLAUDE.md compact while making larger reference material available without forcing Claude to load it on every session.

Building sessions that stay sharp

Context management in Claude Code is not a single technique. It is a combination of: CLAUDE.md as a durable anchor for always-true constraints, session handoff files that survive the gap between sessions, compaction preparation that preserves decisions rather than losing them, disciplined file-read practices that stop the same content from consuming tokens multiple times, per-agent context budgeting for tasks that exceed a single window, and brief context refreshes when degradation shows.

The underlying principle is that context is a resource with a cost, and the most expensive context is context that has to be re-established from scratch. Every architectural decision written to CLAUDE.md is a decision that costs zero tokens next session. Every session handoff file is a condensed restoration of everything that would otherwise take 10,000 tokens of back-and-forth to re-establish. Spending 20 minutes at the end of a session writing a good handoff is the highest-return context investment you can make.

For a complete guide to how CLAUDE.md fits into the broader memory system alongside per-project state files and session-specific memory, see Claude Code memory setup. For understanding the token costs that drive context management decisions, Claude Code token usage covers the cost model in detail.

Get Claudify. Pre-built CLAUDE.md templates, session handoff patterns, and context management rules for every major framework, ready to use from day one.

More like this

Ready to upgrade your Claude Code setup?

Get Claudify
Featured on Dofollow.Tools AI Toolz Dir Claudify - Featured on Startup Fame