Files & hand-off
How files move between the user's Mac and your app — refs instead of paths, the consent card, prepare, limits, returning files, and picking files in your panel.
Apps never see paths on the user's Mac, and never read the project folder. Files move as refs, and Chataway moves the bytes — only the files the user or the agent explicitly passes to you, and only after the user agreed to the hand-off.
#Refs
A ref is an opaque handle. The agent uses a few kinds:
| Ref | Points at |
|---|---|
img_…, vid_…, aud_… (or media:<id>) | An item in the project's Media library — generated images, stock, imports |
project-file:<path> | A file in the chat's workspace |
plugin-file:<app>/<path> | A file in an app's storage — including files your tools returned |
They come from the Media library, files the user attaches or uploads, the agent's own work in the project, and other apps' results.
Refs mean nothing outside the user's Chataway. You can't fetch one; you receive the file's bytes when a ref is passed to one of your file params.
#Receiving files in a tool
Declare the param under the tool's files in plugin.json, with the files:receive grant:
"files": {
"images": {
"accept": ["image/png", "image/jpeg"],
"multiple": true,
"maxBytes": 20000000,
"prepare": { "op": "resize", "width": 512, "height": 512, "fit": "contain", "format": "png" }
}
}| Field | Default | |
|---|---|---|
accept | any type | MIME types or wildcards: image/png, image/*, audio/* |
multiple | false | true → the param is a list of files |
maxBytes | 20 MiB | Per file, after prepare. At most 20 MiB (20,971,520 bytes). |
prepare | none | A host media operation applied to every file before hand-off. Needs host:media. |
When the agent calls the tool, for each file param Chataway:
- resolves every ref in the calling chat's scope (a chat can't pass refs from another project),
- asks for hand-off consent if this owner → your app pair isn't allowed yet in the project,
- runs
prepare, if declared, - checks
acceptandmaxBytes— a file that fails is not sent, and the agent gets an error naming the file and the rule, - sends each file to your server as an MCP embedded resource, in the argument's place:
{
"images": [
{
"type": "resource",
"resource": {
"uri": "chataway-file://a1b2c3/sunset.png",
"mimeType": "image/png",
"blob": "iVBORw0KGgoAAAANSUhEUgAA…"
}
}
]
}blob is base64. The uri is opaque; its last segment is the file name. With @chataway/apps/server, fileParam() decodes this into { name, mimeType, bytes, size } for you — see Server helpers.
#Consent
The first time files move from one owner to a different owner in a project — the Media library to your app, another app to your app, the project to your app — Chataway asks the user:
- Allow once — this hand-off only.
- Always for this project — stored per (project, from, to). Future hand-offs from the same owner to your app in this project don't ask again. The user can revoke it in the app's settings.
- Deny — nothing is sent; the agent is told the user declined.
Your own files (ones you returned earlier) coming back to you don't need consent.
Consent and confirm are separate on purpose: consent is about whose files may reach you, confirm is about whether this action should happen. A tool with both shows the consent card first (once), then your confirm card (every call).
#Returning files
With the files:return grant, files in your tool results are kept:
imageandaudiocontent, embeddedresources with ablob, andresource_links pointing at yourremote.domainsare saved to your app's storage in that project (a link to any other host stays a plain link in the text),- the user sees them as a gallery,
- the agent gets a ref for each one, so it can pass them on: to another tool, to Save to project, into the Media library.
import { returnFile } from '@chataway/apps/server';
return {
content: [
{ type: 'text', text: 'Rendered the banner.' },
returnFile(pngBytes, 'image/png', 'banner.png'),
],
};For large files you already host, return a link instead of the bytes — Chataway downloads it (the host must be in remote.domains):
{ "type": "resource_link", "uri": "https://cdn.example.com/exports/9f1/video.mp4", "name": "video.mp4", "mimeType": "video/mp4" }Without files:return, files in results are dropped: the user doesn't see them and the agent is told your app isn't allowed to hand files back.
#From your storage to the user's project
Your storage is private to your app. Every gallery of returned files has Save to project and Add to Media library buttons, so the user decides what goes where — the file is copied into the chat's workspace (never overwriting) or the library.
The project:write grant is reserved for apps that ask to save files into the project themselves; every such write is shown to the user on a card. A panel API for it isn't available yet — rely on the gallery buttons (or files.addToLibrary from your panel) for now.
#Picking files in your panel
Your panel can ask the user for files with the bridge:
import { connect } from '@chataway/apps/bridge';
const app = await connect();
const files = await app.pickFiles({ accept: ['image/*'], multiple: true });
for (const f of files) {
const blob = await f.blob(); // the bytes
await fetch('https://api.example.com/upload', { method: 'POST', body: blob });
}pickFiles opens Chataway's own picker — Media library, project files, or upload from the device. The iframe receives only the chosen files, as Blobs, plus a ref for each (useful with host.media). The user cancelling rejects with a cancelled error. Your panel may then upload them to your own domains; the CSP allows nothing else.
Picking needs files:receive.
#Limits
| Limit | Value |
|---|---|
| One file sent to your server | ≤ maxBytes, at most 20 MiB |
| One tool response | 50 MB in total (a larger response fails the call) |
Media inputs to prepare / host.media | 10 minutes of audio/video, 8192 px on either side |
| Concurrent host media jobs | 2 per app |
See Limits & quotas for everything else.
#Good practice
- Use
prepare. If your API wants 512×512 PNGs, say so and let Chataway do it. You'll get predictable input, smaller uploads, and users get faster calls. - Accept narrowly.
["image/png", "image/jpeg"]is better thanimage/*if that's what you handle — the agent gets a clear error before anything is sent. - Don't keep what you don't need. Delete received files once processed, and say so in your privacy policy. Reviewers check your data use.