# @-mentions

> Type @ in the composer to pull a file's contents into your message. kin reads the file at send time and includes it inline, so the model sees exactly what's on disk. See Keybindings & cursor mode for every other compo…

Type `@` in the composer to pull a file's contents into your message. kin reads
the file at send time and includes it inline, so the model sees exactly what's on
disk. See [Keybindings & cursor mode](/docs/kin/guide/keybindings/) for every other composer
key, and [Slash commands](/docs/kin/guide/slash-commands/) for `/tree` and `/view`, the
tree-sidebar and read-only-peek commands mentioned below.

<!-- SOURCE: src/kin/harness/at_mentions.py, src/kin/tui/composer_assist.py, src/kin/tui/widgets/, src/kin/tui/paste_paths.py -->

## Outcome

Attach the exact file or line range you mean without asking Kin to search for
it first.

## Fast path

1. Type `@` at the start of a message or after whitespace.
2. Keep typing a filename or path fragment.
3. Move with ++up++/++down++.
4. Press ++tab++ or ++enter++ to insert the highlighted path.
5. Add your request after the mention, then send.

[![Kin composer with a ranked file-mention menu showing matching project paths and the highlighted selection.](/docs-assets/kin/assets/screenshots/file_mentions.svg)](/docs-assets/kin/assets/screenshots/file_mentions.svg)

*The picker inserts a workspace-relative path; file contents are read at send
time so the model sees the current bytes.*

## Using `@` in the composer

Type `@` (at the start of the message or after whitespace) and a file picker
opens above the composer. Keep typing to filter; the menu shows workspace-relative
paths ranked by how well they match.

| Key | Action |
|-----|--------|
| ++up++ / ++down++ | Move the highlight |
| ++tab++ or ++enter++ | Insert the highlighted path as `@path ` |
| ++escape++ | Close the picker |

The picker only inserts the path — it never previews file contents, so a stale
preview can't mislead you. You can also drop a mention in by hand, and you can
pin a line range:

```
@src/kin/harness/loop.py:40-80
```

`@path:lo-hi` attaches just those lines; `@path:lo` attaches one line. A mention
inside backticks (`` `@README` ``) stays literal, and an address-style
`name@host` never triggers because `@` only fires at the start of the message or
after whitespace.

Paths with spaces (every macOS screenshot — *Screenshot 2026-07-10 at 3.14.15 PM.png*)
have to be wrapped in double quotes:

```
@"Screenshot 2026-07-10 at 3.14.15 PM.png"
@"my code/my file.py":1-40
```

The picker is suppressed inside a quoted path (the token round-trips through
`mention_token` verbatim, so no fuzzy matching helps); the file tree (++ctrl+g++)
inserts the quoted form for you.

## Pasting files (drag-drop + terminal paste)

Dropping files onto the composer (Finder drag-drop, terminal paste of absolute
paths) does the same thing as the picker — every path becomes an `@`-mention
inline. The interception lives in `Composer._on_paste` and recognizes three
shapes:

- **Newline-joined** (iTerm shape): one path per line, all must exist.
- **Shell-escaped** (Ghostty shape): backslash-escapes around spaces, or
  double-quoted segments. `shlex.split(posix=True)` recovers the original.
- **Bare spaced path**: a single macOS-screenshot name with U+202F
  narrow no-break spaces — pasted as-is, the whole thing is treated as one
  candidate token.

The discriminator is "the ENTIRE paste is a drop". Any prose-shaped input —
relative tokens, a missing file, mixed token types, a paste longer than 4096
chars — falls through verbatim. So a sentence like
`please look at /Users/you/notes.md when reviewing` is untouched, and a paste
of five files plus a typo'd sixth is the same as a plain paste (the typo'd
path fails the `os.path.exists` check; nothing attaches).

After attachment you get one toast: `attached N file(s) as @-mentions`, with
`· K already attached` appended when some tokens already existed in the
composer. Files >5 MB get downscaled via the macOS `sips` builtin
(`-Z 1568` — matches the harness's `ImageBlock` max-edge budget) before
attachment, with a `downscaled shot.png 8.2 MB → 1.1 MB` toast on success;
a sips failure or non-macOS host falls back to mentioning the original. >10
files in one drop warns and inserts the paths verbatim — a visible choice
beats a silent partial attach. A path that matches `is_secret_path` (the
credential glob) still inserts (Rock 1 makes the mention inert visibly) but
pops a warning toast.

Turn the whole thing off via `KIN_PASTE_MENTIONS=0` or
`paste_file_mentions = false` in `~/.kin/settings.toml` (project files
inherit). The knob is PROJECT-SAFE (no egress / secret / containment surface
— identical to a manual paste) and human-only (NOT model-writable). See
[Environment variables](/docs/kin/reference/environment-variables/) and
[Settings (TOML)](/docs/kin/reference/settings-toml/) for the full knob reference.

## Capturing a clipboard image (macOS)

++ctrl+v++ captures whatever image is on the macOS clipboard and attaches it
as an `@`-mention. Behind the scenes `kin.tui.clipboard` runs `pngpaste` if
installed (the brew-once path) and falls back to `osascript`'s `«class PNGf»`
otherwise — both are subprocess-argv, no shell, no new dependency. The image
is written to
`<project_dir>/<session_id>.attachments/clip-<hhmmss>-<rand4>.png` (directory
0700, file 0600 — a clipboard image can carry anything, so the attachment
dir is user-only). Oversize captures (>5&nbsp;MB) get the same `sips`
downscale as a drag-drop (the `-Z 1568` ceiling matches the harness's
`ImageBlock` max-edge budget); the downscale helper reuses
`paste_paths.run_sips_downscale`, so the failure mode (mention-the-original
+ Rock 2's oversize dropped-note + the `sips` hint) is identical.

The filename is harness-generated (timestamp + 4 random hex chars), so no
user text enters the path — display-sink discipline is preserved (no
control / bidi / zero-width vectors on the path itself). The attachments
live beside the [journal](/docs/kin/guide/sessions/#the-journal)
(`<project_dir>/<session_id>.attachments/`) so resume-durability and the
secret-path checks treat them like the journal: same dir family, same
containment story. A best-effort >30-day sweep runs
at app mount; a vanished attachment degrades to `read_file`'s graceful
not-found. If the clipboard has no image, you get an info toast:
`no image on the clipboard — text paste is unaffected` (text paste is
unaffected because bracketed-paste and `ctrl+v` are separate event paths).
On non-macOS hosts the action toasts `clipboard image capture is macOS-only
for now` (wl-paste / xclip are a planned follow-up DR).

> **Tip**
>
> Picking a file in the toggleable [file-tree sidebar](/docs/kin/guide/keybindings/#global-keys)
> (++ctrl+g++ or `/tree`) inserts it as an `@`-mention — press ++v++ on the
> focused file to peek it in the read-only viewer instead (also `/view <path>`).

## What gets expanded

When you send the message, each `@path` token is replaced with the file's
contents, read with the same logic as the `read_file` tool — line numbers, PDF
and Word/Excel (`.docx`/`.xlsx`, macros in `.docm`/`.xlsm` ignored) text
extraction, and the 50&nbsp;KB cap all apply. The path text you typed is kept
as a label, and the contents follow it inline. See [Tools](/docs/kin/guide/tools/) for how
`read_file` renders.

A mention that can't be read never blocks the send: the token is left in place
with an inline `(not attached: …)` note (file not found, is a directory, binary,
or a read timeout), and the rest of the message goes through unchanged.

Names in kin's read-deny list (`is_secret_path`) are refused before the
existence probe — `@~/.ssh/id_rsa` and a project-local `@.env` both fail
with a `refused — known credential store` note and never inline contents.

Image attachments use a by-reference marker: an `@image.png` mention inside the
5&nbsp;MB cap becomes a harness-authored text marker (path + MIME + size + "View
it by calling read_file with this exact path") rather than inline bytes. A
`<system-reminder>` on that turn instructs the model to call `read_file` on each
attached path before responding (and to batch the calls in a single round). The
bytes only enter the conversation through `read_file`'s audited vision path, so
the marker travels cleanly across provider swaps, compaction, and history
re-renders — and an oversize drop comes with a downscale hint instead of a
silent cap. Notebook mentions remain text-only — call `read_notebook` for
structured cell access.

## Ranking & filtering

The picker ranks candidates with fuzzy matching over the full relative path (not
just the basename), so `helper` finds `src/utils/helper.py`. When the optional
`rapidfuzz` dependency is present it uses a weighted-ratio scorer; otherwise it
falls back to a stdlib substring match. An empty query shows the head of the
corpus.

The corpus is the set of files under your workspace. It honors `.gitignore` (via
the optional `pathspec` dependency) and always prunes heavy or noise directories
(`.git`, `node_modules`, `__pycache__`, `.venv`, `dist`, `build`, and similar),
capped at 10,000 files. It's rebuilt on a 60-second TTL and the walk runs off the
UI thread, so a large repo can't freeze the picker on the first keystroke.

## Out-of-workspace files

A file inside your workspace is treated as ground-truth context and inlined
directly. A file outside the workspace carries the same prompt-injection surface
as a fetched web page, so its contents are wrapped in hardened untrusted-content
framing before they enter the conversation. Symlinks are resolved before the
membership check, so an in-workspace symlink that points outside still gets the
untrusted framing. The model is told to treat that content as data, not
instructions.

## Troubleshooting

- **The picker does not open:** `@` must begin the message or follow
  whitespace. Text inside backticks and address-like `name@host` stays literal.
- **A path contains spaces:** wrap it in double quotes or insert it from the
  file tree.
- **The file was not attached:** the inline note names the reason. Directories,
  missing/binary files, timeouts, and known credential paths are not inlined.
- **Results feel stale:** the candidate walk has a 60-second cache; send a
  hand-written path immediately or retry after it refreshes.

## Reference

- [Files & tools](/docs/kin/guide/tools/)
- [Workspace search](/docs/kin/guide/workspace-search/)
- [Keybindings](/docs/kin/guide/keybindings/)
- [Settings](/docs/kin/reference/settings-toml/)
