Giving memory to an AI that has none
In the previous article, we saw that context is king when working with an LLM. But there’s a fundamental problem: these models have no persistent memory. Every new conversation starts from scratch.
To work around this, there are instruction files that are automatically read by code agents at every exchange. That’s where we can pass our conventions, how we like to work, and the essential project information.
I’ll focus on Claude Code, because that’s the tool I use daily. But the concept exists in other agents — Codex has its AGENTS.md, Gemini its GEMINI.md. Same idea, different filename.
The file hierarchy
Claude Code doesn’t read a single instruction file — it reads several, organized from the most global to the most specific.
%%{init: {'flowchart': {'curve': 'basis'}}}%%
graph TD
A("<span style='font-size:1.1em'><b>~/.claude/CLAUDE.md</b></span><br/><i style='opacity:0.75'>Global — all projects</i>")
B("<span style='font-size:1.1em'><b>./CLAUDE.md</b></span><br/><i style='opacity:0.75'>Project — committed, shared</i>")
C("<span style='font-size:1.1em'><b>./CLAUDE.local.md</b></span><br/><i style='opacity:0.75'>Project local — gitignored</i>")
D("<span style='font-size:1.1em'><b>./src/Entity/CLAUDE.md</b></span><br/><i style='opacity:0.75'>Subdirectory — loaded on demand</i>")
E("<span style='font-size:1.1em'><b>.claude/rules/*.md</b></span><br/><i style='opacity:0.75'>Modular rules — scoped by path</i>")
A --> B --> C --> D --> E
style A fill:transparent,stroke:#10b981,color:#e2e8f0
style B fill:transparent,stroke:#10b981,color:#e2e8f0
style C fill:transparent,stroke:#10b981,color:#e2e8f0
style D fill:transparent,stroke:#10b981,color:#e2e8f0
style E fill:transparent,stroke:#10b981,color:#e2e8f0 The global file (~/.claude/CLAUDE.md)
This file applies to all your projects. That’s where I put my personal work preferences — the rules that don’t change from one project to another.
The project file (./CLAUDE.md or ./.claude/CLAUDE.md)
This one is read when you launch Claude Code in the project. It’s the file you commit to the repo and share with the team.
Subdirectory files
If Claude is working in a specific directory — say it’s editing an entity in src/Entity/User.php — and there’s a src/Entity/CLAUDE.md file, it will be loaded as well.
This is useful for targeted instructions. For example, in my test directories, I put ready-made business examples to help the model create consistent test cases faster.
Instructions are cumulative
Important point: instructions don’t replace each other, they stack up. The global file is read first, then the project file, then subdirectories. Claude receives everything.
Local files
There’s a local variant: CLAUDE.local.md. This file sits alongside the project’s CLAUDE.md, but it’s not committed — it goes in .gitignore.
Very useful when the project’s CLAUDE.md is shared across team members. Everyone can add their own personal instructions without polluting the shared file.
Modular rules
For more complex projects, there’s also the .claude/rules/ directory where you can split instructions into thematic files. Each file can even be scoped to specific project paths via YAML frontmatter:
---paths: - "src/Api/**/*.php" - "tests/Api/**/*.php"---
- All API endpoints must return JSON responses- Use DTOs for request/response objects- ...Content: less is more
This is probably the most recurring debate in the community. Opinions vary, and they evolve fast — understandable given how little hindsight we have.
So I’ll speak from my own experience.
Keep it minimal
The consensus is to keep these files short. The official documentation recommends staying under 200 lines per file. And to put the most important instructions at the top — that’s where they carry the most weight.
Every line of CLAUDE.md is injected into the context with every message you send — not just at the start of the conversation. The longer it gets, the more tokens it costs, and the more critical instructions risk being diluted in the noise.
The right question to ask: “If I remove this line, will Claude make mistakes?” If the answer is no, just remove it.
In the global file
Generic instructions that work across all your projects. This is also where I specify how I like it to work:
# Master Rules
- **NEVER** work directly on main (or master) branch - always create a feature branch- **NEVER** erase or modify previous commits - preserve the complete history, do not amend- **NEVER** use emoji in code (or comment)- **Always** comment in english (even if I prompt in french)- **CRITICAL**: Only modify code directly related to the initial request. Do not make unsolicited changes to unrelated code, even if you notice improvements. Mention issues but do not fix without explicit permission.In the project file
Here, I describe the project: what it is, the stack, the essential commands, and project-specific conventions.
If you have documentation in your codebase, I recommend creating an index file that summarizes each document in a few words with a link. The CLAUDE.md can then point to this index with the import syntax @docs/index.md. The model won’t be overloaded at startup, and when it needs a specific document, it can load it on demand.
# My Project
Order management application. Symfony 7, API Platform 4, PostgreSQL.
## Commands
- `make dev`: start dev environment- `make test`: run tests- `make lint`: check code
## Documentation
@docs/index.mdAvoid /init
I don’t recommend using the /init command to generate your CLAUDE.md. It tends to put in a lot of noise — project structure descriptions, file listings, conventions that Claude can figure out on its own by reading the code. The first time I ran it, the generated file was 180 lines. I kept 12.
By the way, a recent benchmark by TechLoom compared results between a well-filled CLAUDE.md and an identical prompt with an empty file, across 1,188 runs and 10 different instruction profiles. The result is striking: the empty profile scored best, with a total spread of only 1.44 points out of 100 between the best and worst profile.
A study from ETH Zurich tested four code agents (including Claude Code) across 438 tasks. Their conclusion: context files written by developers only provide a marginal performance gain, while increasing cost by over 20%.
I haven’t reproduced these benchmarks myself, but it matches my gut feeling: on the projects I work on, there are ultimately very few instructions with real high value. At the end of the day, what matters is having the few rules that actually count — not a novel.
When CLAUDE.md isn’t enough
This is a crucial point that took me a while to internalize: instructions in CLAUDE.md will always be subject to interpretation. They are never deterministic.
What I mean by that: I never want the model to push code to the remote by itself, nor to access my databases via the MySQL client — for fear of accidentally manipulating production data. No matter how well-worded it is, an instruction in CLAUDE.md remains a suggestion that the model can choose to ignore.
For these critical restrictions, you need deterministic mechanisms.
Permissions
Claude Code’s permission system lets you explicitly block or allow specific commands:
{ "permissions": { "deny": [ "Bash(git push *)", "Bash(gh pr merge *)", "Bash(mysql *)", "Bash(mysqldump *)", "Bash(mysqladmin *)", "Bash(mysqlsh *)" ] }}Here, no matter what the CLAUDE.md says: Claude physically cannot run these commands. It’s a lock, not a suggestion.
Careful though: permissions do pattern matching on the full command string. Bash(git push *) blocks git push origin main, but not cd /repo && git push origin main. To catch the variants, you need to go further.
Hooks
To go even further, hooks let you run custom code at key points in Claude Code’s lifecycle. The PreToolUse hook fires before each tool execution — the perfect place to block dangerous actions.
Here’s a concrete example. On my machine, I have Cloud SQL proxies configured that point to production databases. I want to make sure Claude can never access them, even by accident:
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "bash ~/.claude/hooks/block-database-access.sh" } ] } ] }}And the script that does the work:
#!/bin/bashINPUT=$(cat)CMD=$(echo "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null)
# Extract the first token (the binary being invoked)FIRST_TOKEN=$(echo "$CMD" | sed 's/^[[:space:]]*//' | awk '{print $1}')
# Block MySQL CLI toolsDB_TOOLS="mysql|mysqldump|mysqladmin|mysqlsh|mycli"if echo "$FIRST_TOKEN" | grep -qEi "^($DB_TOOLS)$"; then echo "BLOCKED: Direct database CLI access is forbidden." >&2 exit 2 # exit code 2 = block the actionfi
# Block cloud-sql-proxy socket accessif echo "$CMD" | grep -qF '/path/to/cloud-sql-sockets'; then echo "BLOCKED: Access to production database sockets is forbidden." >&2 exit 2fiExit code 2 is the key: it blocks the action and sends the error message back to Claude as feedback. The model understands it’s not allowed and adapts its approach.
This is defense in depth: permissions block known commands, the hook catches variants and indirect access.
And how does Claude still access the local development database? I use a custom MCP server that only exposes the dev database through typed, secured tools. But that’ll be the topic of a future article.
The automatic memory system
So far, we’re talking about files that you write to give instructions to Claude. But since version 2.1.59, Claude Code can also learn on its own through the automatic memory system.
How it works
When you work on a project, Claude observes and remembers certain things: build commands that work, architectural patterns, technical decisions, your style preferences. It stores these learnings in a MEMORY.md file, saved per project in ~/.claude/projects/<project>/memory/.
The first 200 lines of this file are automatically injected into the context at the start of each conversation. Claude doesn’t save something every session — it does so selectively, when it estimates the information will be useful in future conversations.
The difference with CLAUDE.md
| CLAUDE.md | MEMORY.md | |
|---|---|---|
| Who writes | You | Claude |
| Content | Instructions, rules, conventions | Learnings, observed patterns |
| Scope | Shareable (committed to git) | Personal (local to your machine) |
| Control | Full | Supervised (you can edit/delete) |
You can also explicitly ask Claude to memorize something (“remember that…”) or check its memory. And you can disable the feature with /memory in session or via the CLAUDE_CODE_DISABLE_AUTO_MEMORY environment variable.
It’s a useful complementary layer. CLAUDE.md provides the framework, automatic memory captures the operational details that accumulate over time. Together, they give an agent with no native memory a semblance of continuity between sessions.
What I take away from this
My CLAUDE.md files — global and project — are now several dozen lines each. They grew over time, shaped by mistakes, oversights, and corrections. The trap isn’t writing too much, it’s writing too early. The best rules are the ones you add after watching the model do something dumb.
But giving context and setting guardrails is only half the journey. There’s still a big piece left: giving the agent the ability to interact with the project environment — running tests, querying the dev database, executing business commands. That’s what we’ll cover in the next article.