@-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 composer
key, and Slash commands for /tree and /view, the
tree-sidebar and read-only-peek commands mentioned below.
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 |
| Esc | 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:
@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:
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 and
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 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
(<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
(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 KB cap all apply. The path text you typed is kept
as a label, and the contents follow it inline. See 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 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.