Claude Code Breaks at 60%: The Five-File Fix

  • SaaS & Tech

10 min read

Claude Code Breaks at 60%: The Five-File Fix

TL;DR

A 9-day FY27 pricing rebuild, originally scoped at 14 days, across 28 Claude Code sessions stayed coherent by treating context degradation, which sets in above roughly 60% of the context window, as a scheduling problem rather than a prompting one. Five files, PLAN.md, PATTERNS.md, BATCH.md, RESULTS.md, SESSION.md, carried institutional knowledge between fresh sessions triggered at the start of every day, after every Snowflake test cycle, and whenever context crossed 60%. The rebuild delivered six new pricing models with 11 regional multipliers across nine independently validated batches, finishing at 0.00% variance per batch and 0.002% overall against a full year of historical invoices.

Key takeaway — We were supposed to take 14 days. We finished in 9. Not because of extra hours. Because we found a way to use Claude Code that doesn’t degrade — a five-file memory system and fresh-start protocol that kept every session coherent from day 1 to day 9.

We were supposed to take 14 days. We finished in 9.

Not because of extra hours. Because we found a way to use Claude Code that doesn’t degrade. That took most of day 3 to figure out, and it changed how we ran every session after that.


February 1 was the hard deadline. A cloud security company needed FY27 pricing live before the fiscal year opened — new tier rates, 11 regional multipliers, a completely restructured discount hierarchy. Six new models, hundreds of thousands of rows each, all validating at 0.00% variance against a full year of historical invoices.

The Finance team was counting on these numbers for Q1 bookings. The engineering team had a clean v1 to migrate from, dbt on Snowflake, and Claude Code. Nine days to make it happen.

The environment added a hard constraint: this codebase ran on Snowflake’s native dbt executor, not dbt Cloud. There’s no local development workflow. The cycle was: write code → commit → push → test in Snowflake UI → record results → start the next session. Every validation step required human eyes in Snowflake. You can’t run queries from the AI session. That structure — code in AI session, validate outside it — is what makes the five-file handoff essential, not optional.

Day 1 and 2 went well. Day 3 is when we hit the problem.


If you’ve done a long session with Claude Code, you know the moment. Above roughly 60% of available context — and based on reports from practitioners using newer large-context models, degradation can start as early as 40–48% — something shifts. The code gets slightly sloppier. You ask it to apply a pattern it established two hours ago and it gives you a different answer. You catch it, correct it, move on. An hour later it happens again.

This isn’t a bug. It’s just how transformer architectures work under load. The model isn’t storing your conversation in memory — it’s reasoning about it, and as the context fills up, earlier decisions get compressed and start to fade.

On a short sprint, you’d push through. Our first instinct was to push through. “We’re almost done with this component.” We were not almost done. We were degrading.

The fix wasn’t a better prompt. It was organizational.

Context degradation curve: two paths diverging past 60% context window fill — the push-through path falls off a cliff, the fresh-start protocol stays above the quality floor


We built five files that lived in the repo alongside the code. Every session started by reading them. Every session ended by updating them.

FilePurpose
PLAN.mdMaster plan, batch status, what’s done and what’s next
PATTERNS.mdProven patterns — JOIN+QUALIFY, temporal lookups, v2 validation strategy
BATCH.mdCurrent batch scope only. Resets at the start of each batch.
RESULTS.mdSnowflake test results for this batch
SESSION.mdWhat this specific session needs to accomplish

Five-file memory system: left zone shows the five files with their persistence categories, right zone shows the daily morning-afternoon-evening session rhythm

The insight was simple: Claude Code doesn’t need to remember your project history. It needs access to your project history in a format it can read in five minutes.

PATTERNS.md — what an entry actually looks like:

This is the most important file. An entry looks like this:

## JOIN + QUALIFY over Correlated Subqueries

Snowflake's native dbt doesn't support correlated subqueries.
Switch all temporal lookups to JOIN + QUALIFY pattern.

BAD (fails at runtime in Snowflake native):
SELECT *, (SELECT rate FROM pricing_rates WHERE effective_date <= billing_date ORDER BY effective_date DESC LIMIT 1)
FROM billing

GOOD:
SELECT billing.*, rates.rate
FROM billing
LEFT JOIN pricing_rates rates
  ON rates.effective_date <= billing.billing_date
QUALIFY ROW_NUMBER() OVER (PARTITION BY billing.billing_id ORDER BY rates.effective_date DESC) = 1

After the discovery in Batch 4, this entry went into PATTERNS.md. Every session that started after Batch 4 read it first. We never hit the correlated subquery issue again across 20+ subsequent sessions.

SESSION.md — what it looks like at the start of a morning session:

## Session: Morning Day 6 — Batch 5 start

Context: Batch 4 complete and committed. JOIN+QUALIFY pattern confirmed working.
Finance review scheduled for this afternoon.

This session:
1. Build fct_revenue_v2 using seed-driven approach
2. Build fct_arr_v2 using same pattern
3. Validate grain: one row per (customer_id, product_line, billing_period)

Do NOT:
- Touch macros/fiscal/ without explicit request
- Use correlated subqueries (see PATTERNS.md)
- Push to prod target

Stop and update RESULTS.md before session ends.

The point of these examples: Claude Code doesn’t need to remember the project. It needs to read five files and have enough institutional knowledge to start useful work immediately.

The five-file system became the foundation for a reusable starter kit we left with the team — see how we built guardrails that outlast the consultant.

Fresh starts weren’t failures. They were the protocol.


We ran 28 sessions over 9 days. Most days had three distinct blocks.

Morning (4–5 hours): Fresh instance. Load context files. Write 2–3 components. Commit and push before the session degrades. No heroics — if it’s not committed, it’s not real.

Afternoon (2–3 hours): No Claude Code. Snowflake UI testing — run the models, check the results, document everything in RESULTS.md. This is human work. The AI cannot run your queries or read your actual data. It can write SQL; you have to verify it. This is also where the Finance team caught edge cases in Batch 6 that automated testing had missed entirely — specific customer configurations our test coverage hadn’t anticipated. That review is not optional. The people who built the business rules know things the data doesn’t say out loud.

Evening (3–4 hours): Fresh instance again. Read RESULTS.md. Fix what broke. Prep tomorrow’s SESSION.md so the morning session knows exactly where to start.

We triggered a fresh start whenever a new day started (always — no exceptions), whenever Snowflake testing finished and we had results to act on, whenever we were moving to a new batch, whenever context usage crossed 60%, and whenever Claude contradicted itself or the code quality noticeably dropped.

The last two are judgment calls. You develop a feel for it after a few days. The responses get slightly longer. The explanations get chattier. The code loses a little precision at the edges. That’s your signal — not a specific token count, just a feeling that the session has started answering questions you didn’t ask.


The work broke into nine batches, each independently validated before we touched the next.

Batch 1 was foundation: seeds, lookup macros. The boring infrastructure that everything else depends on, which is exactly why it came first. Batch 2 was core pricing logic — hybrid floor rules, Observability and External discount structures. The pieces that had to work before regional complexity could even be attempted.

Batch 3 is where the scope started feeling real: 11 regional multipliers plus new tier rates. Not complicated in isolation, complicated in combination.

Batch 4 is where we hit our biggest surprise.

Snowflake’s native dbt adapter doesn’t support correlated subqueries. If you’ve been writing SQL on Postgres or BigQuery for years, you reach for them without thinking. They don’t work in Snowflake native, and the error messages aren’t always clear about why. The fix was JOIN + QUALIFY — every correlated subquery pattern rewritten as a join with a QUALIFY clause for deduplication. The moment we put that in PATTERNS.md, every subsequent session started already knowing it. We never hit it again.

One principle shaped every batch validation: parity first, correctness second. When we discovered the legacy BI system had a fiscal quarter bug — a quarter that spanned 15 months instead of 3 — we replicated the bug intentionally, validated parity against it, then fixed it in a separate PR. This is counterintuitive. The right move was to write the wrong code first. If you correct and validate simultaneously, you can’t tell whether a variance is a migration error or an intentional fix. Separate them. Validate parity. Then improve.

Batches 5 through 7 were the harder work: seed-driven v2 revenue models, Finance feedback on specific customer discount scenarios, priority-based account discount logic that had to resolve in exactly the right order. Batch 7 was the hardest to get right — the kind of logic where the output looks correct until you compare it to a case where two discount rules both apply and one should win.

Batch 8 was blocked. Waiting on data from a major technology partner. We moved to Batch 9 and came back. Zero scrambling — the batching structure made partial progress safe. You can’t do that with a monolithic sprint.

Batch 9 was the finish line: seed-driven ARR models with FY27 discounts applied. Final validation: hundreds of thousands of rows at 0.00% variance.

Nine batches. Nine days. Five days early.


What the validation actually looked like

The 0.00% per-batch figure is real, but the methodology that produced it is the part nobody talks about. The 0.002% overall variance figure gets cited; the query pattern that generated it rarely does.

The core pattern was a FULL OUTER JOIN variance query run at the end of every batch:

with v1 as (
  select period_month, sum(net_revenue) as v1_value
  from fct_revenue_v1
  group by period_month
),
v2 as (
  select period_month, sum(net_revenue) as v2_value
  from fct_revenue_v2
  group by period_month
)
select
  coalesce(v1.period_month, v2.period_month) as period_month,
  v1.v1_value,
  v2.v2_value,
  v2.v2_value - v1.v1_value as absolute_diff,
  case
    when v1.v1_value = 0 then 0
    else round((v2.v2_value - v1.v1_value) / v1.v1_value * 100, 4)
  end as pct_diff
from v1
full outer join v2 on v1.period_month = v2.period_month
order by period_month

Why FULL OUTER JOIN: an INNER JOIN would miss months where one system has data and the other doesn’t. The most common migration error — a missing grain — would be invisible. FULL OUTER JOIN surfaces it immediately as a NULL on one side.

ROUND(..., 4) tracks to 0.0001% precision. Our threshold was 0.01%. At that precision, a $100 discrepancy on a multi-year revenue dataset is immediately visible.

This query ran three times per batch at different grains: total by month, then by customer, then by product line. A variance that disappeared when you drilled down was a grain issue. A variance that persisted at every grain was a logic error. Running all three cut root-cause isolation time by roughly half compared to starting at the lowest grain and working up.

Validation hierarchy: three query grain levels (month, customer, product line) with branching diagnosis — variance disappears means grain issue, variance persists at all grains means logic error

The parity-first discipline mattered here too. A variance between v1 and v2 could mean one of three things: a migration error, an intentional fix, or a data quality problem that existed in v1 all along. You can only tell them apart if you’ve separated the “replicate v1 exactly” work from the “correct the known bugs” work. When both are happening in the same batch, every variance is ambiguous.


A few things actually surprised us.

Claude Code is better at greenfield than migration. Writing new components from scratch went fast. Migrating existing logic while preserving existing behavior required far more explicit context about what v1 was doing and, more importantly, why. Sometimes that “why” is undocumented tribal knowledge. It still has to come from you. The classic case: v1 had a fiscal quarter definition that was technically wrong but had been wrong consistently for two years. We replicated it to validate parity, then corrected it. Trying to fix and migrate simultaneously is where most AI-assisted migrations go sideways.

The “almost done” trap is real and it’s specific. The sessions we pushed too long were almost always the evening sessions, not the morning ones. By evening you’ve been at this for hours and “just one more component” sounds reasonable. It wasn’t. Every time we crossed 60% without a fresh start, we paid for it in the next session catching drift.


Start PATTERNS.md on day one, not after the first bug.

We populated it reactively: hit a problem, fix it, document it. An audit of known environment limitations before day one is worth a few hours — for Snowflake native dbt, that means knowing upfront about correlated subquery support, seed type inference, and macro behavior in window functions. The JOIN+QUALIFY discovery in Batch 4 could have been in PATTERNS.md on day one. That knowledge is available; you just have to look before the sprint starts, not during it.

Every team using Claude Code on an existing codebase has environment-specific gotchas. Some are Snowflake quirks. Some are domain quirks. Some are fiscal calendar math that only makes sense if you understand the business reason it was built that way. Write them down before the session has to learn them the hard way.

The AI isn’t the bottleneck. Your ability to give it consistent, structured context across sessions is. Solve that, and a 14-day sprint becomes 9.


We write about the technical setup behind the sessions — CLAUDE.md, hooks, the guardrails system — in our post on AI-assisted analytics engineering with Claude Code and dbt.

Frequently asked questions

Why does Claude Code's output quality drop during long sessions?

Above roughly 60% of available context, and as early as 40-48% on some newer large-context models per practitioner reports, something shifts: code gets slightly sloppier, and asking Claude to apply a pattern it established two hours ago sometimes gets a different answer. This isn't a bug, it's how transformer architectures behave under load, the model isn't storing the conversation in memory, it's reasoning about it, and as context fills up, earlier decisions get compressed and start to fade. Pushing through a session past that point, rather than starting fresh, is what causes the drift.

What is the five-file memory system for keeping Claude Code sessions coherent?

Five markdown files that live in the repo alongside the code: PLAN.md (master plan and batch status), PATTERNS.md (proven patterns, like JOIN+QUALIFY replacing correlated subqueries), BATCH.md (current batch scope only, reset at the start of each batch), RESULTS.md (test results from the current batch), and SESSION.md (what this specific session needs to accomplish). Every session starts by reading all five and ends by updating them, so a fresh Claude Code instance can start useful work in minutes without needing the model to remember the project's history itself.

When should you start a fresh Claude Code session instead of continuing?

Five triggers worked on one 9-day sprint: the start of every new day, always, no exceptions; whenever Snowflake testing finished and there were results to act on; when moving to a new work batch; whenever context usage crossed 60%; and whenever Claude contradicted itself or code quality noticeably dropped. The last two are judgment calls that get easier to spot after a few days, longer responses, chattier explanations, slightly less precise code. Sessions pushed too long were almost always evening sessions, where "just one more component" felt reasonable but wasn't.

How do you validate that a migrated financial model matches the legacy system?

With a FULL OUTER JOIN variance query comparing old and new model output, run at the end of every batch at three grain levels, total by month, then by customer, then by product line. FULL OUTER JOIN matters because an INNER JOIN would miss months where only one system has data, the most common migration error, a missing grain, would be invisible. Rounding the percentage difference to 4 decimal places against a 0.01% threshold made a $100 discrepancy visible even on a multi-year revenue dataset. Running all three grains together cut root-cause isolation time roughly in half versus starting at the lowest grain.

Why replicate a known bug instead of just fixing it during a migration?

Because a variance between the old and new system can mean three different things: a migration error, an intentional fix, or a pre-existing data quality problem, and you can only tell them apart if you've separated replicating the legacy behavior exactly from correcting its known bugs. On one project, the legacy system had a fiscal quarter that spanned 15 months instead of 3; the team replicated that bug intentionally, validated parity against it, then fixed it in a separate pull request. Fixing and migrating simultaneously makes every variance ambiguous, which is where most AI-assisted migrations go sideways.

SIGNATURE PAGE · countersign this file

Bring us the data nobody trusts.

The strategy call is direct with the founder. We take the engagements we can lead end to end — which means we turn some down.

Book the call — and we'll defend these numbers on the record.

15 silent production bugs a migration surfaced
Book a 30-min strategy call

Direct with the founder. No pitch. Bring your messiest data question.

Not ready to book? Write to us: [email protected] A straight answer within one business day. Or read the questions buyers ask us →