the context window ate your instructions

December 25, 2025

CLAUDE.md is frustrating to work with.

You can tell Claude to use atoms instead of useState, or to do literally anything else than fetch inside of useEffect. It’ll work—until it forgets, or the context window fills up, or you start a new session. The instructions drift and your carefully written CLAUDE.md gets summarized into nothing.

The fix is the same one we’ve used for years: make the linter yell at you.

Making the linter yell

What we need: match a code pattern, show a warning (or error) with the alternative.

Biome’s GritQL plugins do this with AST matching. You write patterns that look like code, with wildcards for the parts you don’t care about.

React is where agents go wrong most often—too many valid ways to do things, and they’ll pick whichever they saw most in training. The bad patterns sneak in, cause subtle issues, and agents struggle to debug what they can’t see. The feedback loop goes awry. Here’s a few rules I ended up using in a recent project:

no-react-memoization.grit — I use React Compiler, so manual memoization is (mostly) noise:

`$fn($args)` where {
    or {
        $fn <: `useCallback`,
        $fn <: `useMemo`,
        $fn <: `memo`
    },
    register_diagnostic(
        span = $fn,
        message = "Manual memoization (useCallback/useMemo/memo) is unnecessary with React Compiler. Use regular functions/components instead.",
        severity = "warn"
    )
}

no-usestate.grit — I use Effect atoms for state:

`$fn($args)` where {
    $fn <: `useState`,
    register_diagnostic(
        span = $fn,
        message = "useState is discouraged. Use atoms from @/atoms.ts instead for global state management.",
        severity = "warn"
    )
}

no-useeffect-data-fetching.grit — no fetch calls in useEffect, I use TanStack DB but literally any data fetching library will do:

`$fn($args)` where {
    $fn <: `useEffect`,
    register_diagnostic(
        span = $fn,
        message = "useEffect for data fetching is discouraged. Use TanStack DB (useLiveQuery) instead.",
        severity = "warn"
    )
}

Wire them up in biome.jsonc:

{
    "plugins": [
        "./no-react-memoization.grit",
        "./no-usestate.grit",
        "./no-useeffect-data-fetching.grit"
    ]
}

Now when the agent writes useState, Biome flags it. The agent sees the error, reads the message, and rewrites the code correctly. No prompt engineering required—just the same feedback loop we give human developers.

Same pattern applies to anything you want to enforce:

If you’re not using Biome, ast-grep offers similar AST-based linting with YAML rules. The same no-usestate rule looks like this:

id: no-usestate
language: tsx
severity: warning
rule:
  pattern: useState($$$)
message: useState is discouraged. Use atoms from @/atoms.ts instead.

Run with ast-grep scan. The pattern syntax is simpler ($VAR matches one node, $$$ matches zero or more), and it also supports ad-hoc searches and rewrites from the command line.

Have agents make the linter yell

The real fun in all this is of course that you can get the agent to write that rule for you.

The above rules I got with the following prompt:

Can you write a biome plugin in gritql that would discourage use of useCallback/useMemo/memo (use warnings instead of errors)?

[…]

now add a plugin that would warn against using useState (to use atoms @apps/desktop/src/atoms.ts ), also add a plugin that would warn against using useEffect (for data fetching only use tanstack db)

I ended up using it so much that I made it into a command .claude/commands/create-lint-rule.md:

---
allowed-tools: Read, Write, Glob, Grep
description: Create a GritQL lint rule for Biome
---

Create a .grit lint rule for Biome based on the user's description.

1. Ask what pattern to catch and what the fix should be
2. Write the rule to `{rule-name}.grit`
3. Add to biome.jsonc plugins array

## GritQL syntax

| Pattern | Matches |
|---------|---------|
| \`$var\` | Single AST node, binds to name |
| \`$_\` | Single node, anonymous |
| \`$...\` | Zero or more nodes |
| \`\`code\`\` | Literal code pattern (backticks) |
| \`<:\` | Match operator |

## Basic structure

\`\`\`grit
\`$fn($args)\` where {
    $fn <: \`targetFunction\`,
    register_diagnostic(
        span = $fn,
        message = "Why wrong. Use X instead.",
        severity = "warn"
    )
}
\`\`\`

## Examples

Ban multiple functions:
\`\`\`grit
\`$fn($args)\` where {
    or {
        $fn <: \`useCallback\`,
        $fn <: \`useMemo\`
    },
    register_diagnostic(span = $fn, message = "...", severity = "warn")
}
\`\`\`

Restrict imports:
\`\`\`grit
\`import $what from $where\` where {
    $where <: r".*@prisma/client/.*",
    register_diagnostic(span = $where, message = "Use @prisma/client", severity = "error")
}
\`\`\`

Exclude test files:
\`\`\`grit
\`console.log($msg)\` where {
    $msg <: not within or { \`it($_, $_)\`, \`test($_, $_)\` },
    register_diagnostic(span = $msg, message = "Use logger", severity = "warn")
}
\`\`\`

## Operators

- \`<:\` — match: \`$x <: \`pattern\`\`
- \`or { }\` / \`and { }\` — combine patterns
- \`not\` — negate
- \`within\` — node is inside pattern
- \`contains\` — node contains pattern
- \`r"..."\` — regex

Then /create-lint-rule scaffolds rules on demand.

If you use ast-grep instead of Biome, paste the above to Claude and ask it to convert—the concepts map directly.

< Back