# Does a line in auto memory steer the next session like CLAUDE.md does? 3 of 4 runs vs 4 of 4, and 0 of 4 with neither

> Source: <https://dev.to/rulestack/does-a-line-in-auto-memory-steer-the-next-session-like-claudemd-does-3-of-4-runs-vs-4-of-4-and-0-3673>
> Published: 2026-09-26 02:17:00+00:00

A one-line fact stored in Claude Code's auto memory was applied on the first attempt in 3 of 4 fresh sessions (and after one failure in the 4th). The same line in CLAUDE.md was applied first-attempt in 4 of 4. With neither, 0 of 4 sessions recovered. Measured on Claude Code 2.1.278.

A reader asked a fair question under an earlier post: "How are you measuring whether a captured memory genuinely helped a later session?" We had not been measuring it. We had been assuming that a note Claude wrote to itself in one session would shape the next one, the way a CLAUDE.md line does. This post is the measurement we built to check that assumption, with the numbers as they came out and one result that surprised us.

Before designing the experiment we fetched the official memory page ([https://code.claude.com/docs/en/memory](https://code.claude.com/docs/en/memory), fetched 2026-09-22) to find out exactly what auto memory is on disk, because the experiment has to plant a memory file by hand and the file has to land where Claude Code actually reads it.

The page frames the two mechanisms as siblings: "Claude Code has two complementary memory systems. Both are loaded at the start of every conversation. Claude treats them as context, not enforced configuration." That last sentence matters for interpreting the results below. Neither mechanism forces behaviour; both are text the model reads.

On location: "Each project gets its own memory directory at `~/.claude/projects/<project>/memory/`. The `<project>` path is derived from the git repository, so all worktrees and subdirectories within the same repo share one auto memory directory. Outside a git repo, the project root is used instead." The directory holds an index file: "The directory contains a MEMORY.md index and one topic file per memory."

On what is loaded: "The first 200 lines of MEMORY.md, or the first 25KB, whichever comes first, are loaded at the start of every conversation. Content beyond that threshold is not loaded at session start." And the topic files are not loaded at all at startup: "Claude Code doesn't load topic files such as user_role.md or feedback_testing.md at startup. Claude reads them on demand using its standard file tools when it needs the information."

So the honest comparison is: one line in the project's CLAUDE.md versus the same line in that project's MEMORY.md index. Anything in a topic file is a different, weaker case (Claude would have to decide to open it), and we did not test it.

Two more sentences shaped the setup. Auto memory is "on by default", so nothing had to be enabled. And "Auto memory is machine-local", which is why this experiment cannot be run inside a CI container that starts from a clean home directory.

One thing the page does not spell out is how `<project>` is escaped into a directory name. We had to observe it: on this machine, a project at `/private/tmp/memexp.52QAvx/proj-c` got the directory `~/.claude/projects/-private-tmp-memexp-52QAvx-proj-c/`, so every slash and every dot in the path became a hyphen. Treat that as an observation on 2.1.278, not documented behaviour. The safest way to find the directory for a project is to run one session there and see which directory appears.

We needed a task where a fresh session would plausibly fail, where the fix could not be derived from the files in the project, and where the only way to succeed on the first try was to already know the fix. A hidden environment variable checked against a hash does that.

Create a throwaway project outside any git repository:

```
P=$(mktemp -d)/proj && mkdir -p "$P/scripts" && cd "$P"
printf '.PHONY: test\ntest:\n\t@scripts/run-tests.sh\n' > Makefile
printf '# reportkit\n\nSmall CLI that renders weekly reports. Run the test suite with `make test`.\n' > README.md
H=$(printf '%s' orchid-42 | LC_ALL=C shasum -a 256 | cut -d' ' -f1)
cat > scripts/run-tests.sh <<EOF
#!/bin/sh
LOG="\$(dirname "\$0")/../.attempts.log"
EXPECTED="$H"
ACTUAL=\$(printf '%s' "\${PROJECT_TOKEN:-}" | LC_ALL=C shasum -a 256 | cut -d' ' -f1)
if [ -z "\${PROJECT_TOKEN:-}" ]; then
  echo "\$(date -u +%FT%TZ) token=missing" >> "\$LOG"
  echo "error: PROJECT_TOKEN is not set. The fixtures server refused the connection (403)." >&2
  echo "hint: ask a teammate for the project token; it is not stored in this repository." >&2
  exit 1
fi
if [ "\$ACTUAL" != "\$EXPECTED" ]; then
  echo "\$(date -u +%FT%TZ) token=invalid value=\$PROJECT_TOKEN" >> "\$LOG"
  echo "error: PROJECT_TOKEN was rejected by the fixtures server (403)." >&2
  exit 1
fi
echo "\$(date -u +%FT%TZ) token=valid" >> "\$LOG"
echo "fixtures: authorised"; echo "3 passed, 0 failed"
EOF
chmod +x scripts/run-tests.sh
```

The script appends one line per invocation to `.attempts.log` saying whether the token was missing, invalid, or valid. That log is the whole measurement: a run counts as "first attempt" if the first line is `token=valid`, "after failure" if a `valid` line follows a `missing` line, and "never" if no `valid` line appears. We did not classify by reading transcripts, so there is no judgement call in the tally.

Now the three conditions. Condition A is the project as-is. Condition B adds one line to `CLAUDE.md` in the project root:

```
The test suite needs PROJECT_TOKEN=orchid-42 in the environment: run `PROJECT_TOKEN=orchid-42 make test` (plain `make test` fails with a 403 from the fixtures server).
```

Condition C puts the same sentence, as a single bullet, into that project's `MEMORY.md` instead (and no CLAUDE.md). Run one session in the project first so the `~/.claude/projects/<escaped-path>/` directory exists, then:

```
mkdir -p ~/.claude/projects/<escaped-path>/memory
printf -- '- The test suite needs PROJECT_TOKEN=orchid-42 in the environment: run `PROJECT_TOKEN=orchid-42 make test` (plain `make test` fails with a 403 from the fixtures server).\n' > ~/.claude/projects/<escaped-path>/memory/MEMORY.md
```

Before trusting condition C we ran one probe session in the project asking Claude to quote, without running anything, whatever memory notes it had loaded. It quoted the bullet verbatim and said no CLAUDE.md was present. Its usage showed 14,910 cache-creation tokens plus 10,343 cache-read tokens for that single call, which is roughly what a bare session costs on this machine. So the file was picked up; the escaping guess above was right.

Each measured run is the same command, in the project directory, with a fresh session every time:

```
: > .attempts.log
claude -p 'Run the project test suite with `make test` and tell me whether it passed. Do not modify or create any files.' \
  --output-format json --allowedTools "Bash,Read,Glob,Grep" > run.json
cat .attempts.log
```

Restricting tools to `Bash,Read,Glob,Grep` did two things: it kept the model from editing the trap, and it kept the model from writing new memories during a run, which would have contaminated later runs in the same condition. We checked `MEMORY.md` after the four condition-C runs; it was byte-for-byte unchanged, and no memory directory appeared for the A or B projects. Four runs per condition, twelve measured runs, plus the one probe, thirteen `claude -p` invocations in total. Model in every run: `claude-fable-5-1`, as reported in the `modelUsage` field of the JSON output.

Condition A, nothing: 0 first-attempt, 0 after-failure, 4 never. Condition B, CLAUDE.md: 4, 0, 0. Condition C, auto memory: 3, 1, 0.

The "never" column for A is not a surprise; it is the check that the trap works. In all four A runs the session ran `make test`, got the 403 message, and then went looking: it read the Makefile, the runner script, the README, checked the environment for anything containing `TOKEN`, and in one run grepped every markdown and example file for `PROJECT_TOKEN`. None of that can produce `orchid-42`, and none of the four runs guessed. All four reported the failure honestly and explained that a token from a teammate was needed. That took 4 to 8 turns and between 52 and 156 seconds per run.

Condition B was the cleanest result we have ever recorded in one of these experiments. Every run had exactly two turns and one shell command, and the command was `PROJECT_TOKEN=orchid-42 make test 2>&1; echo "EXIT=$?"`. The opening sentence of the first run was "I'll run the test suite with the token from CLAUDE.md, since plain `make test` is documented to fail with a 403." The other three said the same thing in slightly different words. No run read the Makefile or the script. The line in CLAUDE.md was treated as an instruction to act on, not a claim to verify.

The counts say 3 of 4 versus 4 of 4, which on a sample this small is not a strong difference. The transcripts say something more specific, and it held in all four condition-C runs, not just the one that slipped.

Every condition-C session looked at the project before running anything: the first command in all four was a directory listing plus `cat Makefile`. Three of the four then read `README.md` and `scripts/run-tests.sh` too, and only after seeing the token check in the script did they run the fixed command. Run 3 put it plainly: "The runner requires a project token that isn't in the repo. My memory from a prior session records the value, so I'll run the suite with it." Run 1 announced its plan up front: "My memory notes this project needs a PROJECT_TOKEN env var for the fixtures server, so I'll run `make test` as asked and fall back to the token if it fails", and then, having read the script, ran the fixed command directly anyway.

Run 4 did what run 1 said it would do. It read the Makefile, ran plain `make test`, got the 403, and then recovered:

That is the one "after failure" row in the tally. The memory line did help: the session knew what the 403 meant and knew the fix without searching, which condition A never achieved. But it did not treat the line as a reason to skip the literal `make test` the prompt asked for.

Our reading is that the model treats the two sources with different authority. A CLAUDE.md line reads as an instruction from the project, so it is followed on the first move. A MEMORY.md line reads as a note the model itself left, so it is verified against the code before it is trusted, and in one run of four the model chose to reproduce the failure first. The docs do not describe a difference in how the two are presented beyond location; the memory page notes that "CLAUDE.md content is delivered as a user message after the system prompt, not as part of the system prompt itself", and does not make the equivalent statement about MEMORY.md. We did not inspect the raw request, so we cannot say whether the framing differs. We can only say the behaviour did.

The JSON output of each run has a `usage` object. Adding `input_tokens`, `cache_creation_input_tokens` and `cache_read_input_tokens` gives total input tokens for the run; the first assistant message's usage in the session transcript gives the context size at the first call, which is the startup cost of each condition.

Startup context, identical across all four runs of each condition: A 26,557 tokens, B 26,762, C 26,786. The CLAUDE.md line cost 205 tokens and the memory line cost 229, for a 168- and 170-byte sentence respectively; the rest is whatever wrapper text each mechanism adds. At this size the difference is nothing.

Total input over the whole run is where the conditions diverge, and it tracks the number of turns rather than the mechanism: A averaged 130,847 input tokens (4 to 8 turns of searching), B averaged 53,840 (2 turns), C averaged 110,861 (4 turns, most of it re-reading a cached context while inspecting the project). Wall-clock followed the same order: A averaged 96 seconds, C 31, B 26. So the cheapest session was not the one with the smallest startup context; it was the one that did not feel the need to check.

The measurement the reader asked for is now a script: plant the fact, run four fresh sessions, read `.attempts.log`. We will rerun it when a new Claude Code version lands, because a 3-of-4 versus 4-of-4 gap on four runs is the kind of thing that could vanish or widen with a model change, and we would rather have the number than the impression.

For the facts we actually depend on, this changed our practice in one way. Anything that must be applied on the first move in the next session, such as a required flag, a command that must not be run, or a path that must be used, goes into CLAUDE.md, and we let auto memory keep the things where a second look at the code before acting is fine. That matches the framing the docs give ("Use CLAUDE.md files when you want to guide Claude's behavior. Auto memory lets Claude learn from your corrections without manual effort."), but we now have a concrete reason for it: in our runs the memory line was consulted, but it was consulted as a hint, and once it was consulted only after the failure it described had already happened.

Two limits on the claim. Four runs per condition is enough to show the direction and not enough to put an interval on it. And the trap is a single, one-line fact; a MEMORY.md with dozens of entries, or a fact that lives in a topic file rather than the index, is a different experiment, and given the docs' statement that topic files are not loaded at startup, we would expect the first-attempt rate for a topic-file fact to be lower still. We have not measured that yet.

*Rulestack writes CLAUDE.md layouts, memory conventions and skills for Claude Code teams, sold at [rulestack.gumroad.com](https://rulestack.gumroad.com?utm_source=devto&utm_medium=article&utm_campaign=does-a-line-in-auto-memory-steer-the-next-session-like-claude-md-does-3-of-4-runs-vs-4-of-4-and-0-of-4-with-neither). The 0/4, 4/4, 3/4 tally above is the reason our own packs keep hard rules in CLAUDE.md and leave auto memory for observations.*

*We plan to rerun the same three placements against topic files and a longer MEMORY.md; the numbers will appear on [@ai-shop.bsky.social](https://bsky.app/profile/ai-shop.bsky.social) first.*
