TL;DR
@pastes the whole file, plus whateverCLAUDE.mdsits above it in the tree. Type it once, on the turn the file needs to enter, then refer to the file by name. Repeating the mention cost me 4117 new tokens against 185 for the same turn without it.
@ looks like a pointer. You type @src/auth/session.ts and it reads as "look at this file", the way you'd point a finger.
It doesn't point. It pastes. And the part that stays out of sight is that it pastes again every time you type it, because the model has no memory of you handing it over last turn.
I measured it on a clean session, reading the session's own records.
Result:
turn what I typed new tokens history re-read
1 @gsc-report.md @gsc-report.md 15139 14112
2 @gsc-report.md (already inside) 4117 29251
3 a sentence the same length, no @ 185 33368
4 @voice-profile.md 4188 33553
Turn 3 is the control: same sentence length, no mention. 185 tokens. Turn 2, which only added one @ to a 2 KB file that had been sitting in context since turn 1, cost 4117. Twenty-two times more.
Three things that change how you use it
1. Inside one message it does deduplicate. On turn 1 I typed the same file twice and only one copy was attached. So the version going around, "mention it three times and you get three copies", is not true within a message.
2. Across turns it does not. There it holds. Turn 2 attached the full file again even though it had been in the history since turn 1. What you pay for is not the repeated mention, it is the repeated turn.
3. And it doesn't travel alone. Every @ also drags in the CLAUDE.md from that file's tree, as a separate attachment. I asked for harness/commands/gsc-report.md, 2235 bytes, and harness/CLAUDE.md came with it, 8383 bytes. 79% of what got attached was not the file I asked for. Turn 4 confirms it from the other side: I mentioned a file with no CLAUDE.md anywhere above it and that extra attachment never appeared.
The docs mention this in passing, without numbers: @ file references add the CLAUDE.md from the file's directory and its parent directories to context. In my case it walked up one level, from commands/ to harness/.
Look at your own session
Every turn's attachments sit in the session .jsonl, with a name and a type:
jq -r 'select(.type=="attachment") | .attachment
| select(.type=="file" or .type=="nested_memory")
| "\(.type)\t\(.filename // .path)"' \
~/.claude/projects/<your-project>/<session>.jsonl
One file per mentioned file, and one nested_memory for each CLAUDE.md that tagged along. If the same name shows up three times, you mentioned it on three turns.
And the price of each turn, in the same file:
jq -s '[.[] | select(.type=="assistant") | .message | select(.usage)]
| unique_by(.id) | to_entries
| map({turn: (.key+1),
new: .value.usage.cache_creation_input_tokens,
history: .value.usage.cache_read_input_tokens})' \
~/.claude/projects/<your-project>/<session>.jsonl
The unique_by(.id) is not decoration: the same response appears several times in the file, and without it you count double.
The part that actually hurts is that it stays
Look at the right-hand column again. Turn 3 mentioned nothing and still re-read 33368 tokens, which is turn 2's 29251 plus the 4117 turn 2 had just added.
A copy is not paid for once. It settles in and gets re-read on every turn the session has left. That is the flip side of why Claude Code burns so many tokens: there, the point is that the whole history is re-sent on every turn; here, that you are the one deciding how fat that history gets, and a repeated @ is the easiest way to fatten it without noticing.
So how do you use it
Type the @ on the turn the file needs to enter. After that, refer to it by name:
turn 1 review @src/auth/middleware.ts ← it enters here
turn 2 change the refresh in the middleware ← it already has it
turn 3 and add the test ← it still has it
This works because turn 1's attachment is still in the conversation. You don't have to resend it for Claude to see it.
Two cases where repeating the @ earns its cost:
- The file changed on disk. Verified: the second mention reads from disk again rather than replaying the old copy. I edited the file between two turns and the second attachment carried the new content. If Claude is holding the stale version, mention it again.
- After a
/compactor a/clear. The old attachment may be gone from context, so the copy is needed again. What exactly survives a compact is in the instructions that vanish without warning.
And when what you need is orientation rather than reading, point at a directory instead of a file: @src/api/ returns the listing of names, not the contents of each one. That one and the other ways to hand over context are in five ways to give Claude Code the right context.
Requirements
- Measured on Claude Code v2.1.251, on a fresh session.
Official docs: Reference files and directories