---
title: Chat attachments (PDFs, text docs, images, Office docs, pasted text)
---

# Chat attachments (PDFs, text docs, images, Office docs, pasted text)

## What it is

Five different ways content gets from the chat composer to Claude, picked automatically by file type or paste size. The user's experience is one paperclip / one drag-and-drop / one paste — they don't think about which path their content takes. Behind the scenes the main process partitions every attachment into one of five buckets (image / PDF / text doc / Office doc / pasted text) and routes each bucket the way Claude's API ingests it best:

| Bucket                                                                           | What Claude sees                                                                                    | Where the bytes go                                                                                                         |
| -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Images (`image/*`)                                                               | `image` content blocks (base64 inline)                                                              | Inline in the message — also a UI chip                                                                                     |
| PDFs (`application/pdf`)                                                         | `document` content block (base64 inline, vision-mode)                                               | Inline + saved to workdir so the agent can also `Read` / edit later                                                        |
| Text docs (`.md` `.txt` `.csv` `.json` `.html` `.xml` `.tsv` `.log` `.markdown`) | A line in the prompt prefix: "Please examine the following file(s): `<path>`"                       | Saved to workdir; only the path is in the prompt                                                                           |
| **Office docs (`.docx` `.xlsx` `.pptx` + legacy `.doc/.xls/.ppt`, ODF `.odt/.ods/.odp`, `.epub`, `.rtf`)** | Markdown extracted in main via the anydoc reader, saved as `<base>.md` next to the original; that `.md` path is in the prompt | The extracted `.md` lives in workdir alongside other docs; the original binary is kept only as chip metadata in `userData` |
| **Pasted text >5,000 chars**                                                     | Inline in the prompt body at the cursor position — no wrapper, no marker                            | Stored as range metadata on the message row; chip-rendered in history                                                      |

**Every office format is now accepted** — the modern OOXML trio plus legacy binary Office (`.doc`/`.xls`/`.ppt`), OpenDocument (`.odt`/`.ods`/`.odp`), EPUB, and RTF. The Firecrawl **anydoc** reader (the same on-device engine behind the "Convert to Markdown" button) parses the binary CFB / ODF / EPUB containers no cheap in-process parser could reach, so nothing is blocked at attach time anymore. The old `isBlockedOffice` gate + `BLOCKED_OFFICE_*` sets are kept, now **empty**, as a re-block hook should a future genuinely-unsupported format come along. The renderer allow-list, the IPC schema's `ATTACHMENT_MIME_TYPES` enum, and the main-process partition all now accept every office MIME/extension in lock-step (a build-time guard pins the enum ⊇ the renderer doc allow-list so they can't drift).

**Office extraction is lossy on purpose.** The extractor pulls _text/Markdown only_. `.docx`, `.pptx`, and the newly-supported formats (`.doc`/`.ppt`/`.odt`/`.ods`/`.odp`/`.epub`/`.rtf` + legacy `.xls`) route through the **anydoc** reader, which emits clean GitHub-flavored Markdown — real pipe tables for documents that contain them, structured text otherwise — with automatic fallback to the in-process mammoth/adm-zip extractor for `.docx`/`.pptx` if anydoc is ever unavailable. `.xlsx` keeps its own dedicated extractor (one `## sheetN` heading + pipe table per worksheet, first row treated as the header row, empty middle cells preserved via `r="A1"` position parsing, `|` and newlines inside cells escaped) — the shape Microsoft MarkItDown emits and the documented best-comprehension format for tabular data in LLM context. Formatting, merged cells, images embedded in the doc, charts, speaker notes, comments, tracked changes, and pivot tables are dropped. The output is capped at 100,000 characters; anything past that gets a `[content truncated]` marker appended. If the extraction fails outright (corrupt zip, password-protected file, no text content, or the native anydoc binary missing on an unsupported platform) the chip is still saved so the user sees what they attached, and a single `system`-source message is appended to the conversation: "⚠ Couldn't extract text from: `<filename>`. The file is attached but its contents were not sent to Claude." — so neither the user nor the agent misreads silence as "Claude read the file".

## Where to find it

Attachments are a composer feature, so there is no panel to open: the paperclip sits in the message box of any session, drag and drop and paste work directly on its textarea, and the chips appear in a strip just above it. The only setting involved is the camera button, which is off by default and is turned on with the other session settings.

## How it behaves

### How to use it

1. **Open a chat session** (any provider — Claude virtual project, real project, Claude over OpenClaw).
2. **Attach files** any of these ways: click the paperclip in the composer, drag-and-drop into the textarea, paste with `Ctrl+V` (works for screenshots from clipboard too), or — if you've enabled it — click the **camera icon** (next to the paperclip) to attach a live screenshot of the Omniscio window itself — handy for "here's what I'm looking at". The camera button is **off by default**; turn it on at **Settings → Sessions → "Screenshot button"** (desktop only — it never appears on the web/mobile UI regardless of the setting). The captured screenshot enters as a normal image chip and rides the same pipeline. The matching `GET /ui/screenshot` endpoint that external AIs (and Ask Omniscio) use is unaffected by that setting and is documented in [cli-control.md](cli-control.md) under "UI Highlight, Discovery + Screenshot".
3. **Watch the chips appear** above the textarea: images get a small thumbnail; PDFs, text docs, and Office docs get a `FileText` icon + filename; pasted-text blocks (clipboard paste >5,000 chars) get a `Quote` chip with a one-line preview and char count. If you hit a per-file size limit, a single batched warning toast surfaces instead.

   **Remove a chip by `X` or by middle-clicking it — either way it is undoable.** A toast offers **Undo**, and **Ctrl+Z / Ctrl+Y** step back and forward through removals, restoring a chip into the slot it came from rather than appending it. See [attach-a-file.md](attach-a-file.md) for the full chip behaviour.

   **Reorder image/doc chips before sending.** Each image/document chip has a small drag handle (`GripVertical` icon, top-left) that appears on hover (desktop only — mobile chips scroll horizontally so a touch-drag would fight the scroll gesture). Drag a chip onto another to swap their position; the array order becomes the order Claude sees the attachments, so this is how you line chips up with prompt references like "the first screenshot" without re-pasting. Pasted-text chips are NOT reorderable — they splice into the prompt body at the position the cursor was in when they were pasted, so their array order has no effect on output.

4. **Type your prompt and send.** Claude receives images and PDFs natively (vision); for text docs Claude will use its own `Read` tool against the path you handed it — so it's allowed to also _edit_ the file in place, just like any file in the project. For Office docs Claude reads the extracted `.md` sibling, not the binary, so it can _read_ the content but can't faithfully edit the original `.docx`/`.xlsx`/`.pptx` (formatting would round-trip lossy anyway).

Caps and limits:

- **No per-type count cap** — attach as many images, PDFs, text docs, and pasted-text chips as you want. The old `MAX_IMAGE_ATTACHMENTS = 4` and `MAX_DOC_ATTACHMENTS = 4` renderer-side caps were removed in 2026-04 alongside paste-as-attachment so the chip strip never silently rejects what you tried to attach. The IPC schema still bounds extreme cases as defense-in-depth: 20 images per send, 50 pasted-text chips per send.
- **32 MB per file** for documents (Anthropic's limit), 30 MB for images, 2 MB per pasted-text chip (the IPC schema's `content.max(2_000_000)` — well above any realistic clipboard paste, but enough to keep a single huge HTML blob from blowing the IPC envelope). Enforced in the renderer; the IPC schema upper-bounds doc bytes at ~45 MB defense-in-depth.
- One toast per send batches every problem (e.g. a file too large) into a single friendly message — no toast spam.

On mobile (browser context with the soft keyboard up), the composer is shrunk to fit a keyboard-shrunk viewport (~350-450px tall): the chip strips scroll **horizontally** in a single row instead of wrapping to multiple rows; the auto-grown textarea is capped at 120px (down from 200px on desktop); and the drag-to-resize handle is hidden (touch can't operate it anyway). Desktop is unchanged — chips wrap, textarea grows to 200px, drag handle stays visible. The reason: the response input is the last flex child of a `100dvh` column and `100dvh` does NOT shrink under the on-screen keyboard, so an unbounded chip wrap + textarea + handle would push the send button below the visible viewport. Three caps are coordinated; the contract is enforced by [tests/unit/lint/mobile-attachment-input-fits-keyboard.test.ts](/tests/unit/lint/mobile-attachment-input-fits-keyboard.test.ts).

Where the files are saved:

- PDFs and text docs land at `<project-workdir>/.claude/amc-attachments/<sessionId>/<sanitizedName>`. The save uses an atomic write (`<name>.tmp` → `rename`), retries with a `(2)`, `(3)`, … suffix on filename collisions, sanitizes the name to strip path-traversal sequences and reserved Windows tokens, and refuses to write outside the session directory (symlink-escape defense via `fs.realpath`).
- Office docs (`.docx`/`.xlsx`/`.pptx`) land in TWO places: the original binary is stored as IPC chip metadata in `userData` (via the same `saveAttachment` images use — the agent never reads this copy), and the _extracted text_ is written to the workdir as `<original-basename>.md` (e.g. `meeting-notes.docx` → `meeting-notes.md`). Only the `.md` path is path-injected into the prompt; the agent's `Read` calls against the chip filename will find the `.md` next to it because the agent sees the `.md` path directly. If the agent tries to `Read` the original `.docx` it'll succeed but get binary noise — the documented contract is "the `.md` is the truth".
- If the workdir isn't writable (permissions, read-only mount), the save falls back to `${app.getPath('userData')}/attachments/<sessionId>/<name>` and the prompt prefix uses _that_ path instead. The agent can still `Read` it, but it's outside the project tree.
- Images are not saved to the workdir — they're stored only as IPC chip metadata (in `userData` via `saveAttachment`) and inline in the message. The agent doesn't need a path because it has the bytes.

### How attachments render in sent message history

Once a message is sent, every attachment shows up as a small chip under the message body in the bubble. The chip shape is decided per-attachment at render time from the file's MIME type — there's no stored "image vs document" flag, the renderer just looks at whether `mimeType.startsWith('image/')`:

- **Image chips** are small square thumbnails loaded via the custom `attachment://` protocol. Click opens the in-app image viewer. **On desktop (since 2026-07-04) that's a centered card sized to the image itself** — filename, position counter, Copy and Close buttons in the card's header, your workspace still visible behind a dimmed backdrop. The image opens **fitted, never inflated**: a small image shows at its real size, a large one is scaled down to fit within ~80% of the window. From there you can zoom to full resolution and move around: **scroll to zoom** and **drag to pan** (double-click also toggles zoom). **On a phone or tablet the viewer is full-screen** (the right pattern on a small display): **pinch to zoom**, **drag with one finger to pan**, and **double-tap to jump to full resolution** (double-tap again to snap back to fit) — without the touch gestures a wide screenshot opened on a portrait phone is a tiny strip with no way to read it. It's the same viewer the composer's image preview uses. The lightbox's arrow keys / on-screen arrows step through the _image-only_ subset of that message's attachments (so a message with two images and three PDFs shows "1 / 2" in the corner, not "1 / 5"). Right-click on the image opens the image context menu (**Copy Image** / **Save Image As…** / **Show in Folder**). Images embedded in the **agent's own messages** — a picture it links to on the web, or a `data:` image — now open this same lightbox on click too (previously they rendered inline with no way to zoom), so any image in chat is click-to-enlarge, not just files you attached. And when the agent shows you an image it read from a local file, Omniscio stores that **original at full resolution** rather than the model's vision-downscaled copy, so enlarging it stays sharp — see [full-resolution-chat-image-contract.md](/.claude/memory/contracts/full-resolution-chat-image-contract.md).
- **Document chips** are pills containing a `FileText` icon, the original filename, and the file size. Click opens the file in the OS default application (Adobe Reader for PDFs, Word for `.docx`, your text editor for `.md` / `.txt`, etc.) via Electron's `shell.openPath`. Right-click opens the document context menu (**Open** / **Save As…** / **Show in Folder**). PDFs, Word/Excel/PowerPoint, plain text, markdown, CSV, JSON, and any other non-image MIME route through this shape.

The two chip shapes are mutually exclusive — every attachment is either an image chip or a document chip, never both, and there's no third shape. The lightbox is deliberately image-only; previewing PDF or Office content belongs to the OS application that's already best at it (and would require shipping a heavyweight in-renderer viewer that would inflate the bundle for a feature few users want inside the chat UI).

If the bytes for a chip ever fail to load (file moved, permission revoked, disk error), the chip stays visible but shows the broken-image placeholder for images / falls back to the filename + icon for documents. **Reporting "PDF preview is not working" or "the file icon won't open" with a session reference is the right path** — the renderer logs the failure path; the chip itself is intentionally minimal.

**On mobile (web access), an image chip is loaded over an authenticated HTTP route, and that path is reliability-hardened.** Desktop loads chip images through the local `attachment://` protocol (no auth, always available); the phone has no such protocol, so it fetches `/attachment/<sessionId>/<file>` over the tunnel, authenticated by the same durable `amc_web_token` cookie the rest of the web session uses. A picture an agent puts in its own reply as an `attachment://` image line (what `POST /capture/image/show` hands back) takes the same route on the phone: the chat's markdown image resolves it through the same `getAttachmentUrl` helper the chips use, for both the inline picture and its zoom view. That cookie is now established and refreshed on every authenticated request (not only when you first pair), so an image still loads after the phone puts the tab to sleep and reloads it — previously the tab-sleep wiped the in-tab token and the image came back as a broken thumbnail while the surrounding text (served from cache) still looked fine. As a second layer, a mobile image that fails to load is retried a couple of times (with a cache-buster) before it falls back to the broken-image placeholder, so a momentary connection blip self-heals instead of sticking. See the [durable-cookie postmortem](/.claude/memory/postmortems/mobile-attachment-image-durable-cookie-postmortem.md).

Single render site for both shapes: [src/renderer/src/components/ui/MessageBubble/MessageBubble.tsx](/src/renderer/src/components/ui/MessageBubble/MessageBubble.tsx) (attachment `.map()` branch around line 1454). The two context menus are split components in [src/renderer/src/components/ui/ImageContextMenu.tsx](/src/renderer/src/components/ui/ImageContextMenu.tsx) — `ChatImageContextMenu` and `ChatDocumentContextMenu`. The viewer chrome lives in ONE shared shell, [src/renderer/src/components/ui/ImageLightboxShell.tsx](/src/renderer/src/components/ui/ImageLightboxShell.tsx) — [MessageLightbox.tsx](/src/renderer/src/components/ui/MessageBubble/MessageLightbox.tsx) (sent messages) and [PendingImageLightbox.tsx](/src/renderer/src/features/sessions/PendingImageLightbox.tsx) (composer + Quick Launch) are thin data wrappers over it. The shell is a `createPortal` overlay (mounted on `document.body` so its `fixed` + `100dvh` box fills the real **visible** viewport — not a `contain`/`transform` scroll ancestor that used to render the image oversized, and not the larger layout viewport that hides behind a phone's address bar; on desktop it is offset below the 50px window-controls titlebar — `top: isMobile ? 0 : TITLEBAR_HEIGHT`). **Desktop renders a centered image-sized card**: the image area is a **definite pixel box** — `fitImageBox(natural, 85% × overlay width, 80% × overlay height − header)` from [image-lightbox-fit.ts](/src/renderer/src/components/ui/image-lightbox-fit.ts), measured via ResizeObserver on the overlay (never `window.innerWidth/Height`), never upscaled, min card width 320px — with the filename + counter + Copy/Close in the card header; loading state is derived per-source (src-keyed), so a cached image can never strand the spinner. **Mobile renders the full-bleed viewer** (floating 48px controls, safe-area aware) wrapping the same shared [`ImageCanvas`](/src/renderer/src/components/ui/ImageCanvas.tsx), which **fits the image with CSS** — an intrinsic-capped `<img>` (`max-w/max-h` + `w/h auto`), never a JS `window.innerHeight` box calc (that was the mobile fit bug) — and provides zoom via the battle-tested [`react-zoom-pan-pinch`](https://www.npmjs.com/package/react-zoom-pan-pinch) engine: wheel + drag on desktop, pinch / one-finger-pan / double-tap-to-full-res on touch, all gated behind `touch-action: none` and only on interactive (non-`disableInteractivity`) canvases. A click/tap on the dimmed area closes the viewer; on the picture it does not (so double-click/double-tap zoom works). The contract and invariants for both shapes — including the viewer's portal + fit invariants **`lightbox-renders-through-a-portal`** and **`lightbox-image-is-a-css-fitted-canvas`**, the touch-gesture invariant **`canvas-is-touch-zoomable`**, and the desktop image-sized-card invariant **`desktop-opens-an-image-sized-card`** — live in [.claude/memory/contracts/message-attachment-chip-contract.md](/.claude/memory/contracts/message-attachment-chip-contract.md).

### Token-size warnings (banner + Send-anyway gate)

Big drafts can quietly approach — or blow past — Claude's 200,000-token context window. A 180,000-token paste looks the same in the chip strip as a 6,000-token paste (a single chip with a one-line preview), so the user has no UI cue that they're about to overrun the model unless we surface one. Two layers do that, both keyed off the same chars-divided-by-4 estimate that the rest of Omniscio uses:

- **Cumulative draft banner.** Above the textarea (next to the legacy attachment warnings), an inline banner (`role="status"` `aria-live="polite"`) shows the **typed text + every pasted chip** combined: amber once cumulative tokens cross 50,000 (**"This draft is large"**), red once they cross 130,000 (**"This draft is near Claude's 200,000-token context window"**). The banner deliberately does **not** restate the token count — each pasted-text chip already shows its own `~N tokens`, so the banner stays a pure warn/danger flag (the exact total still surfaces in the Send-anyway confirm dialog, where the chips can be obscured behind the modal). The banner is live: every keystroke and every chip add/remove re-derives it. Sub-50k → no banner at all. There is **no per-paste toast** — the banner is the single in-composer signal, so a paste that lifts the draft past a threshold surfaces by the banner appearing or changing colour rather than a separate transient toast (which had been pinging users multiple times during normal large-paste workflows).
- **Send-anyway confirm at the danger threshold.** Every send path that consumes the composer state runs the same gate before clearing the draft. A red `ConfirmDialog` ("Send this large draft?" / "Send anyway" / "Cancel") opens, and Cancel returns to the composer with the textarea and chip strip intact — the gate runs **before** `setIsSending`/`clearDraft`/`clear-session-input`, so a cancelled send doesn't lose anything. The gated paths are:
  - **Regular send** (Enter / Send button / Alt+S auto-submit snippet / Alt+Z Zap) — `executeSend` in [`useSessionPanel/useSessionSend.ts`](/src/renderer/src/features/sessions/useSessionPanel/useSessionSend.ts).
  - **Aside** (Ctrl+B aside mode + Send) — `handleSendAside` in [`useSessionPanel.ts`](/src/renderer/src/features/sessions/useSessionPanel.ts).
  - **Send Later (scheduled response)** — both the initial schedule (`handleSelect` send-later branch) and the replace-existing path (`handleConfirmReplace`) in [`SnoozePalette.tsx`](/src/renderer/src/components/ui/SnoozePalette.tsx). Cancelling here leaves the palette closed but the SessionPanel composer untouched — the user can re-trigger Send Later without retyping.
  - A **reentry guard** (`if (sendConfirmOpen) return false`) at the top of `executeSend` and `handleSendAside` silently drops a second send call while a previous confirm dialog is still open, so a rapid double-Enter doesn't swap dialogs underneath the user.

Token estimation uses the `chars / 4` heuristic from [`src/shared/token-estimate.ts`](/src/shared/token-estimate.ts) — accurate to roughly ±5% on prose, **under-counts code-heavy pastes by 10–20%**, and over-counts long whitespace runs. The Send-anyway confirm message surfaces this caveat directly to the user ("This is an estimate (chars / 4); code-heavy pastes can run 10–20% larger in true Claude tokens"), so they understand the displayed token count is approximate. The danger threshold of **130,000** (not 150,000) is set with the under-count in mind: a draft the estimator reports at 130k can land in the ~145–160k true-token range, which is where context exhaustion or model truncation starts to bite. Warn stays at **50,000** (~25% of the window — heads-up only).

Implementation is centralised in [`src/shared/token-warning.ts`](/src/shared/token-warning.ts):

- `TOKEN_WARN_THRESHOLD = 50_000`, `TOKEN_DANGER_THRESHOLD = 130_000`, `CLAUDE_CONTEXT_WINDOW = 200_000`.
- `classifyDraftTokens(tokens)` returns `'ok' | 'warn' | 'danger'`.
- `computeDraftTokens(typedText, chips)` sums the chars/4 estimate across the typed text and every chip's content — the single source of truth used by the banner, every danger gate, and the unit tests. Tolerates null/undefined inputs (partially-hydrated drafts) defensively.
- `buildDraftSizeBanner(tokens)` returns `{ kind: 'amber' | 'red', text }` (or `null` below 50k) — `kind` drives the banner colour.
- `buildDangerConfirmOptions(tokens, kind)` returns the shared `useConfirmDialog` options used by all four gated send paths. `kind` is `'send' | 'aside' | 'schedule'` and only changes the subject noun ("This send" / "This aside" / "This scheduled response") — title, confirm/cancel labels, and the estimator caveat are identical, so the four gates can never drift out of sync.

The warning helpers live in `src/shared/` (not `src/renderer/src/lib/`) because the cumulative banner and all four Send-anyway gates (`executeSend` in `useSessionPanel/useSessionSend.ts` / `handleSendAside` in `useSessionPanel.ts`, plus `handleSelect` send-later branch / `handleConfirmReplace` in `SnoozePalette.tsx`) need the same thresholds — putting the constants anywhere else would have created two copies that could drift. The renderer's promise-returning [`useConfirmDialog`](/src/renderer/src/hooks/useConfirmDialog.ts) wires each danger gate: each caller instantiates its own `useConfirmDialog` (one in `useSessionPanel`, one in `SnoozePalette`), renders the `dialogElement` at its JSX root, and a cancelled confirm returns `false` so the send pipeline short-circuits without clearing any draft state.

### Send paths that preserve attachments

The composer has three ways to fire a send, and **all three pass every attachment type — images, PDFs, text docs, and pasted-text chips — through the same pipeline**:

- **Regular send** (Enter / Send button) — typed text + every attachment in the composer (images, PDFs, text docs, pasted-text chips).
- **Auto-submit snippet** (Alt+S → pick a snippet whose **Auto-submit** toggle is on, or click an auto-submit snippet in the picker) — the snippet's text is appended to whatever's in the textarea, and the result fires immediately, carrying any pending images / PDFs / text docs / pasted-text chips.
- **Zap / Quick Reply** (⚡ button or Alt+Z) — the configured Quick Reply phrase fires immediately, also carrying any pending attachments. Unlike the snippet path, the textarea contents are NOT included — Zap is a one-button preset, not a textarea-aware send — but attachments still ride along, including pasted-text chips (which splice into the Zap phrase at their captured offsets).

If the send fails on any of the three paths, the attachment chips reappear in the composer and the in-memory image cache is restored, so a retry doesn't make the user re-attach. Pasted-text chips are restored with their original ids, contents, and insert positions — the user's only copy of the pasted block is never lost to a transient send failure. The text-restore behavior differs: regular and snippet paths restore the textarea (snippet restores the combined text); Zap does not (its source text lives in settings, never in the textarea).

Earlier builds dropped attachments silently on the snippet auto-submit and Zap paths — fixed in 2026-04. The contract is locked by the regression suite in [tests/unit/hooks/useSessionPanel-drafts.test.ts](/tests/unit/hooks/useSessionPanel-drafts.test.ts) under `useSessionPanel — pendingImages flow through all three send paths`, and the matching `pendingPastedTexts` parity is locked by [tests/unit/hooks/useSessionPanel-pasted-restore.test.ts](/tests/unit/hooks/useSessionPanel-pasted-restore.test.ts).

**Send Later (scheduled responses) is the fourth send path** and preserves attachments the same way. At scheduling time the renderer captures `pendingImages` + `pendingPastedTexts` into a JSON payload stored in the new `sessions.scheduled_attachments` column (migration v154). At delivery time `prepareScheduledAttachments` in [/src/main/services/send-later-service.ts](/src/main/services/send-later-service.ts) re-hydrates the payload, splices pasted-text chips back into the body, and runs the same `partitionAttachments` + `saveDocToWorkdir` + `buildPromptWithAttachments` pipeline this page describes. See [send-later.md](send-later.md) for the dispatch table and migration details.

## Related

- [use-quick-responses.md](use-quick-responses.md) — the composer-side surfaces (Alt+S snippet picker, Alt+Z Zap, Alt+1/2/3 AI chips) that fire those send paths.
- [project-docs-auto-injection.md](project-docs-auto-injection.md) — `.claude/docs/` auto-attaches files on first message via the same path-prefix mechanism. Different scope (per-project, every session) but same delivery primitive.
- [session-stuck-in-needs-you.md](session-stuck-in-needs-you.md) — what to do when a send appears to succeed but Claude never responds.

This page is split across two parts: [part 2](chat-attachments-part-2.md) covers the send pipeline that sorts and delivers each bucket, how non-Claude engines receive attachments, and how a large paste becomes a chip. The chips themselves are also described from the attaching side on [attach a file](attach-a-file.md), a big paste can arrive from [clipboard history](clipboard-history.md), and a scheduled send carries its attachments through the same pipeline on the [send later](send-later.md) page.
