When to paste code and when to point at a path
A pasted snippet is code with no address. A path is an address. What each one costs, what the agent does differently with them, and the handful of cases where pasting is still the right call.
Two prompts that look like the same request
Let's compare these two ways of saying the same thing, like "we want to replace hello with hey" but then saying it from different perspectives — first, inlining the entire function body (so we can ask to change hello), and second, just pointing at src/main.rs so the agent knows which file we're talking about:
Here's our greeting function:
pub fn greet(who: &str) -> String {
format!("hello, {who}")
}
Change "hello" to "hey".Change "hello" to "hey" in src/main.rs.The first one is a habit that we carried over from chat bots where you can't access the codebase anyway, so we need to provide the code. Now, given that the agent can access the code, it's good to be mindful that every code you carry over has a cost. The second form is a more optimal way of expressing this.
But pointing at a file is also a matter of two different approaches that people treat as identical, so let's test it on Claude Code 2.1.193 in an eight-liner Rust project.
First, we pointed to src/main.rs like this:
@src/main.rsAnd it worked perfectly after the first go, with zero calls of the read tool, because the harness inserted the file into the prompt automatically.
What's more, Anthropic's docs say it outright — "Use @ to quickly include files or directories without waiting for Claude to read them", and for a single file, "This includes the full content of the file in the conversation."
Then, we tried using this:
src/main.rsWhich also worked, but took two rounds. The second round can be seen in the transcript, with absolute paths shortened to …/:
TOOL_USE: Read {"file_path": "…/src/main.rs"}
TOOL_RESULT: 1 fn main() {
2 println!("{}", greet("world"));
3 }
4
5 /// Returns a greeting for `who`.
6 pub fn greet(who: &str) -> String {
7 format!("hello, {who}")
8 }But these are actually two different things, just in terms of their implementation: one is a paste (the first) and the other is an address (the latter). The good thing about the address is that the read tool's output is preformatted with line numbers so you can check yourself that it's indeed what it says, like "src/main.rs:7".
A paste is code without an address
Let's say we had a second file, src/legacy/mod.rs, holding a byte-identical copy:
//! Kept for the v1 API. Do not touch.
/// Returns a greeting for `who`.
pub fn greet(who: &str) -> String {
format!("hello, {who}")
}And then we pasted that function into the prompt, exactly as it appears on disk, with this request:
Change "hello" to "hey".TOOL_USE: Grep {"pattern": "pub fn greet", "type": "rust"}
TOOL_RESULT: Found 2 files
src/legacy/mod.rs
src/main.rs
TOOL_USE: Read {"file_path": "…/src/main.rs"}
TOOL_USE: Edit {"file_path": "…/src/main.rs", …}
TEXT: Done. Changed "hello" to "hey" in the greet function.And when the agent ran grep, it found both of these files. But the agent read and edited only src/main.rs, and then told us that they changed greet (in singular, with article), so they obviously didn't touch the other file.
What's more important, it didn't ask any questions to make sure we meant the single file; it didn't even say there were two matches. And it didn't read src/legacy/mod.rs, so it didn't see the "don't touch" line in this code.
It wasn't a bug, it was just that the model wasn't presented with a way to resolve the ambiguity. So it chose and presented its decision in a self-confident way.
Pasting is not only about carrying the code over, but also about deciding what part of the code you carry over, which you do in a second, before knowing what's important in the file. If your cut is too narrow, the agent will hallucinate what you omitted (the helper you didn't scroll up far enough to include, for example, or the import at the top of the file, or the constant defined in another file). If it's too wide, the actual twelve lines from the four hundred are being lost there.
A path, on the other hand, doesn't give you a choice. You send the entire file along with its neighbours, and have an option to look at something that hadn't crossed your mind before.
The stale-paste story is mostly wrong
The staleness thing is also often misunderstood. People say that if you paste a piece of code in the prompt, the agent will end up editing a version that doesn't exist anymore. Let's test it:
We've pasted a different version of the function than the one on disk and asked to change it, and as you can see the agent ran find in the terminal to locate the file, read the actual file, saw the actual content of the file, and edited that:
TOOL_USE: Bash {"command": "find … -name \"*.rs\" -type f"}
TOOL_USE: Read {"file_path": "…/src/main.rs"}
TOOL_USE: Edit {"old_string": " format!(\"hello, {who}\")",
"new_string": " format!(\"hey, {who}\")"}
TEXT: Done. Changed the greeting from "hello" to "hey".Anthropic's docs describe a read-before-edit rule — "Claude reads the file in the current conversation before editing it" — and they list Claude Haiku 4.5, the model we ran this on, among those that "always require the read". So in this scenario, the harness had to open the real file for the agent to edit it. Which means that the harness basically prevented you from pasting an outdated code.
That's not to say that pasting is a good practice. It's just that the main cost of pasting is those additional rounds spent on finding an address that you could have just typed. Also, the harness didn't tell you that the code you pasted was different than what's in the file — you were wrong about your own code, and the evidence was in the read tool's output, but you finished the session without realising it.
It's actually the difference between the code and the file that is the most harmful thing about a stale paste. Because usually, it's not about editing the wrong piece of the code; it's about having a false mental model of the codebase that nobody in the loop feels like pointing out.
What pasting is actually for
Pasting makes sense if you want to:
paste something that isn't a file (like stack trace from another machine or the production query), or
paste output of something that can't be run locally in this session (like the layout on the prod or a vendor's dashboard), or
paste a piece of another library or spec that is not part of this repo, or
paste what you want to have in place (what the interface should look like), or
paste a version from somewhere else (another branch, an open PR, a previous release)
Unless…
In theory, you could also paste a file that's not in the starting directory of the session. But then it's a workdir thing; you can paste but you'll be back here within an hour with the same problem. Better to use --add-dir, which all six harnesses take for widening the workspace, than to copy-paste the file.
@ means two different things, and you should know which one you have
Okay, so let's move on to the most important part, which is that @ has two meanings, and you need to know which one it is in your case. This is the non-standardised part, because all six harnesses place @ in their compositors, but the meanings are different across them, which influences what you actually pay for by pressing this key.
The first scenario is when @ means "inline". Then, the harness sends the file contents with the message. In the case of Claude Code and GitHub Copilot CLI, it's as follows:
Claude Code — "This includes the full content of the file in the conversation."
GitHub Copilot CLI — "This adds the contents of the file to your prompt as context."
In the case of Claude Code, a 2,500-line file came back answerable in one go without any call for the read tool. So the entire file went with the message. But let's not get too comfortable and treat this as part of the docs; if Copilot CLI's docs say that it sends the file contents with the message, we can assume it does, but they don't say when, so we don't know when.
The second scenario is when @ means "autocomplete". Then, the harness inserts a path into the prompt. In this case:
Codex —
@will "add its path to the prompt"Antigravity — "imports the absolute workspace file path directly into your prompt. This helps the agent target its code searches."
Kimi Code — selecting "inserts its relative form into your message"
We can see it with a debug command in Codex:
codex debug prompt-input "explain @src/main.rs"The user message comes back as literal text, contents nowhere:
{ "type": "input_text", "text": "explain @src/main.rs" }As you can see, it's just the path; no contents are added.
The last scenario is Cursor. One sentence — "Select files and folders to include in context with @" — and nothing about what it does with them. So you'll need to measure it yourself if you're a Cursor user.
It's also worth remembering that it's the same key on a different harness with different incentives:
on an inliner, like Claude Code,
@-ing a 3,000-line file is pasting with extra steps, and a bare path serves you better because the model can read one window of iton an autocompleter, like Codex,
@is almost free to type — and the risk reverses: you can point at five files, have the agent open two, and nothing in the transcript flags the other three
So make sure to identify which scenario you have. Also, remember that in the case of Codex and Antigravity, they only document what you put into your input box — not what the harness does afterwards. What's more, what is not said doesn't mean it's not happening, so keep measuring.
A bare path is a delegation; you tell the model "you can read this file" or "you can run grep in this directory" or "you can read this one window".
That's what Claude Code did in our run, and what Cursor's headless docs describe step by step. But it's also less than the inlined @ for a 3,000-lines-long file if only one function matters, and more than the inlined @ if the model defensively runs grep on its twelve neighbours.
But okay, you could say that a bare path is more expensive than pasting. And we agree with you, but only to a point. Because in such scenario, when we give the model a path, it tries to read the file by default; just like you and me if we were presented with a path. And what's more, now the same lines are in the window and you paid for a tool round trip to put them there.
So if we want this to beat pasting, we need to give the model not only the file's path but also what we want out of the file — that's what turns a read into a grep. "Look at src/auth/session.rs" is worse than pasting. "Find where the refresh token gets dropped in src/auth/session.rs" is better, and for a mechanical reason: the first one gives the agent no basis for reading less than everything.
Anthropic's docs say it plainly — "Claude Code sends your full conversation with every request, and each time Claude uses tools it sends another request carrying that batch of tool results" — and every call of a tool results in another request being sent, which means that the first turn's content is still being resent on the 40th turn. In other words, pasting is billed per turn until the end of the session.
And what's more, this is not just theory; it's already been incorporated into some products:
Claude Code — the harness writes everything to a file in the session dir if the output exceeds 30,000 characters, and gives the model the path with a preview
Copilot CLI — it's 20 KiB by default, but the output goes to a temp file anyway, and the model gets the path with a preview
So you can see that there are two companies with different products, and they both do the same thing once the output becomes too big. They give the model the file's path.
Narrow the address before you send it
That being said, if you have an address (a bare path), it's better to make it more specific. And it works in three ways:
appending a line range to a path — none of the six harnesses document this as part of their syntax, so treat it as a prose suggestion; still effective because all the read tools accept an offset and a length (Claude Code: offset and limit; Kimi Code: line_offset and n_lines), and the model needs to source these numbers from somewhere
file name and symbol name — both are better than either of them on their own
test name — pointing at the specific failing test
This is not about aesthetics, it's about efficacy. Because you can grep for names; you can't grep for descriptions.
Think about it: if we were to ask you to find a function in this code, you'd need to translate it in your head first to come up with an actual query. But if we tell you that its name is greet, you already have the query.
Anthropic's docs also follow this pattern:
weak example — "add a calendar widget"
strong example — "look at how existing widgets are implemented on the home page to understand the patterns. HotDogWidget.php is a good example."
So it's not about adding more words, it's about adding a filename.
Pointing in Claude Code
Using @ in a prompt, you can include any file (or folder) from your project in the conversation, and the docs are really explicit about what that costs. For a single file, "This includes the full content of the file in the conversation." For a directory you get something far cheaper — "a directory listing with file information", and the tips spell it out: "Directory references show file listings, not contents". Two examples:
Explain the logic in @src/utils/auth.js
What's the structure of @src/components?So the first one puts a whole file in the window and the second one only puts a listing there. But if you write a bare path like src/utils/auth.js into a sentence instead, Claude reaches for the Read tool, which "takes a file path and returns the contents with line numbers". That one pages by a token limit rather than a line count, so when a whole-file read is too big it comes back with a PARTIAL view notice telling Claude how to continue with offset and limit — which is exactly where a line range in your prose has somewhere to land.
But okay, let's get back to the "@" symbol; It doesn't need to be a file — you can also use it like this: @server:resource, which "fetches data from connected MCP servers" (for example if you want to reference a ticket or a row from the dashboard instead of just copying it there). There's also another option which is that you can include content from outside your machine by piping it into Claude's stdin — cat error.log | claude
Gotcha: @ doesn't just include the file you point it at. The docs note that "@ file references add CLAUDE.md in the file's directory and parent directories to context". Which means that if you have a neat monorepo using the "@" symbol in any of your files will make Claude include an entire chain of instruction files. And @path written inside a CLAUDE.md is an entirely different mechanism — it's an import, and "imported files load at launch", so it isn't a lazy pointer at all. It costs the same as pasting, every session.
Pointing in Codex CLI
It's probably good to be aware, even if you're used to other tools using the same shortcut, that in the case of codex-cli it works differently than you might expect. From the official docs:
When you type
@, it searches for a file within your workspace and inserts its path into the prompt.It returns only the path, the file's content is not included.
Using
/mentionis also the same thing — with a slash at the beginning.
To make sure, you can look what is in the prompt before sending it (without calling the LLM), using the debug subcommand which prints the prompt in JSON
codex debug prompt-input "explain @src/main.rs"— and you'll see that the prompt contains @src/main.rs, as a plain text. It doesn't contain the content of this file. You can say that @ is a low-fi reference — cheap to type, cheap to send but providing no value until you decide to open it. That's why in the docs prompt example they have explicitly told to read two files and describe the schema and the request/response flow — because if you came from Claude Code or Copilot CLI you may have trained yourself not to @ a big file — here that instinct is backwards. If you use five @s, you'll append five strings to the prompt; if the model doesn't get round to reading one of them, nothing about your message made it. That's the cost running the other way.
Line ranges go in prose here — the prompting guide has a whole section headed "CLI workflow (path + line range described in prompt)", and its worked example carries no range syntax at all. The one thing that is attached rather than referenced is images: -i / --image, comma-separated or repeated, and it's the only attachment flag in the CLI.
Gotcha: worth knowing that "The IDE extension automatically includes your open files as context. In the CLI, mention paths explicitly, or attach files with /mention and @ path autocomplete."
Pointing in GitHub Copilot CLI
If you want to reference files in GitHub Copilot CLI, it's all about using the @ sign and relative paths, which the docs make explicit — "This adds the contents of the file to your prompt as context for Copilot.". The shortcut table puts it in one row — "@ FILENAME — Include file contents in the context." You can see it in action in these two examples; the first one mentions a JS file to debug, and the second one a CI YAML config to describe.
Fix the bug in @src/app.js
Explain @config/ci/ci-required-checks.ymlThere's another type of reference as well, # NUMBER, also mentioned in the shortcut table; that's for GitHub issues or PRs, aiming to refer to something you'd otherwise paste there; like issue threads, for example, that's what the reference replaces. You can use absolute paths too if you want, the docs demonstrate it in case of a prompt spanning multiple repos (@/Users/me/projects/auth-service); same goes for images and PDFs, which are also supported with the same mention syntax, just like in this example asking about implementing a design based on a mockup image ("Implement this design: @mockup.png"). Alternatively there's an --attachment PATH flag, repeatable, but only on a non-interactive run.
Gotcha: What's important is that by default the @ picker ignores files listed in .gitignore — the respectGitignore setting "Exclude[s] gitignored files from the @ file mention picker", and a repository can tighten that but never disable it; that means that the files you'll most often want to paste somewhere are the ones it won't let you pick. These are typically local configs, artefacts or build logs, but you can always enter the path manually as it only influences the completion menu.
Pointing in Cursor
The CLI documentation for @ is one sentence long — "Select files and folders to include in context with @" — so the more useful page is the headless one, which spells out what happens when you write a plain path into a prompt:
1. The agent receives your prompt with the file path references
2. The agent uses tool calling to read the files automatically
3. Images are handled transparently
4. You can reference files using relative or absolute pathsThat's pointing, described step by step. And the vendor draws the same line this lesson does, in fewer words: "Use @ mentions when you know which files are relevant. If you're not sure which files matter, skip it — Agent finds relevant files through its own search."
There's a nice piece of corroboration in the hooks reference, if you'd rather see the shape of it than take a sentence's word for it. The beforeSubmitPrompt payload — what exists at the moment you hit send — carries attachments as {"type": "file" | "rule", "file_path": "<absolute path>"}. No content field. Content shows up one hook later, in beforeReadFile, documented for exactly the job you'd want it for: "Use for access control to block sensitive files from being sent to the model."
If you were to use it, for example, in a prompt like this:
agent -p "Analyze this image and describe what you see: ./screenshot.png"Then the agent would be able to analyse the screenshot on your machine
Gotcha: @ on a folder is documented as possible and nowhere documented as to what it does. No word about a listing, about recursing into subdirectories, no mention of a file cap — so @src/components/ is a gamble whose odds the docs can't tell you. And the CLI gives you no line-range syntax at all; the editor's "select code and press Cmd + L" has no command-line equivalent, so from the terminal your only way to narrow an address is prose.
Pointing in Antigravity CLI
agy's @ opens an interactive overlay for you to choose a path from, and the docs describe what happens next: "Type @ within your prompt box to trigger the Interactive Path Suggestion overlay. Highlighting and selecting a path imports the absolute workspace file path directly into your prompt. This helps the agent target its code searches."
Read that last clause again — the stated purpose is to help the agent search, which is a fair description of what a path is for. So what we know is that the text of the absolute path ends up in the prompt. Whether the contents are pulled in too, the documentation doesn't say either way, so treat that half as unknown rather than settled.
If the file you want lives outside the session workspace, widen the workspace rather than copy-pasting the file in:
During the session — using
/add-dir <path>Or during the start of the session — with the
--add-dirflag
Gotcha: If you came here from Gemini CLI, this is the specific habit to unlearn. Gemini CLI's docs are unambiguous that its @ inlines — "This forces the CLI to read the file immediately and inject its content into your prompt", and "The content is fetched and then inserted into your query before being sent to the Gemini model". And if you use "@" on a folder it includes files from within it, including from subfolders, and it warns you about it — "Be careful with large folders, as this consumes more tokens". The Antigravity documentation doesn't mention any of that for either case. So it's the same key but the potential cost it carries is different based on the docs.
Pointing in Kimi Code CLI
When you run Kimi Code in the terminal, it provides file paths completion with a @ symbol. The thing is, that for Kimi Code the @ symbol works differently than it does for Claude Code or Copilot, so just be mindful of it.
The docs put it like this: "Type @ to trigger file-path completion. Selecting a path inserts its relative form into your message; the agent loads the file content directly when it reads the message."
So what's important is that the path is what reaches your input field, but the fetching part happens on the agent side.
For example, if you were to ask about a missing null guard you could write sth like:
Check @src/components/Button.tsx for the missing null guardCompletion works in git and non-git directories, folder suggestions end with / so you can keep completing downwards, and .git is excluded from the suggestions.
What's more, Kimi Code is the only harness on this list whose own read tool publishes a per-call cap, which is what makes narrowing an address concrete here rather than a ritual. Read takes a path plus an optional line_offset — "negative values count from the end" — and n_lines, and it "Returns at most 1000 lines or 100 KB per call; content beyond that limit is accompanied by a truncation notice."
That means that for example if you have a file which is a few thousand lines long, a single read call won't cover the entire file so the range you provide is crucial to decide if the agent will see your function or just the first few lines.
Gotcha: that cap is documented for the tool. The docs don't say whether it applies to an @ reference, so don't assume it either way. And check which product you're actually driving: the Python kimi-cli is being wound down in favour of the Node-based kimi-code — "This project will be gradually wound down" — and both still answer to the name Kimi Code CLI in their own docs. The @ wording above is from the current TypeScript one.
And finally, if you want to use the @ symbol for files that are outside of your session's root directory, you can do it with the --add-dir flag.
The shape most real prompts want: paste the symptom, point at the code
In other words, these are not opposites. A good bug report is both a paste and a path, because the output of the failed test is not in this repo (so you need to paste it), but the code that needs to be analysed is in this repo (so you point at it).
Like here:
This is failing in CI on main:
thread 'auth::expired_session' panicked at src/auth/session.rs:212:
called `Option::unwrap()` on a `None` value
The refresh path is in src/auth/session.rs. Work out why the token is
missing by the time we get there, rather than making the unwrap safe.We pasted the CI panic trace (standard output:
unwraponNoneatsrc/auth/session.rs:212)We pointed at src/auth/session.rs and asked to diagnose why the token is missing rather than making the unwrap safe
As you can see, a stack trace is perfect to be pasted, because it's almost a path — a file with a line number.
In other words, what you always want to paste from any paste is the part that tells you where to look.