Move bytes between Gini upload space, external URLs, and workspace files. Used by every attachment / file-upload / file-download flow regardless of the target system (Linear, GitHub, S3, Notion, etc.).
62
76%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
Fix and improve this skill with Tessl
tessl review fix ./skills/attachments/SKILL.mdYou move bytes between three places:
<id> references for files the user attached in chat, downloaded from a URL, or promoted from workspace.This skill ships four scripts you invoke via skill_run, plus a recipe for each common direction. The base primitive vision_query (asking the model to describe an image upload) is in core, not here — combine it with the scripts below when the model needs to "see" what it just moved.
code_exec / terminal_exec produced a workspace file (chart, exported PDF, downloaded artifact) and you need to send it somewhere or run vision_query on it.assetUrl / signed URL pointing to file content and you want to do something with the bytes.signed-upload — chat-attached upload → external URLPUT bytes from a Gini upload (chat attachment, downloaded file, promoted workspace file) to a signed URL the model obtained from an API's prepare step. Used in 3-step attachment flows: prepare via the API → signed-upload → finalize via the API.
skill_run({
skill: "attachments",
script: "signed-upload",
args: {
uploadId: "abc-123-...", // from the user message marker, signed-download, or promote-file
url: "https://uploads.linear.app/...?X-Goog-...",
headers: { // pass through whatever the prepare step returned, verbatim
"content-type": "image/png",
"x-goog-content-length-range": "36116,36116"
}
}
})
// → { ok: true, status: 200, bytesSent: 36116 }
// or { ok: false, status, error: "..." }Only https URLs are accepted. The script never fabricates headers — pass through what the prepare step gave you. Signed URLs typically expire in 60 seconds; move immediately from prepare → PUT.
signed-download — external URL → Gini uploadGET bytes from a URL and store them as a Gini upload. Used to ingest content (Linear attachment URLs, GitHub raw files, user-pasted URLs, S3 presigned downloads) into the upload-addressable space so vision_query or signed-upload can consume them.
skill_run({
skill: "attachments",
script: "signed-download",
args: {
url: "https://uploads.linear.app/asset/abc.png",
headers: { authorization: "Bearer ..." }, // optional, depends on the URL
filename: "screenshot.png" // optional, defaults to URL basename
}
})
// → { ok: true, uploadId: "xyz-456-...", mimeType: "image/png", size: 36116 }Only https URLs accepted. Body capped at 50MB. Inferred mime comes from the content-type response header; falls back to application/octet-stream.
promote-file — workspace file → Gini uploadRegister a workspace-relative file as a Gini upload. Used when code_exec / terminal_exec left a file on disk that you want to attach somewhere or run vision_query on.
skill_run({
skill: "attachments",
script: "promote-file",
args: {
path: ".charts/sales-q4.png",
mimeType: "image/png" // optional, sniffed from extension when omitted
}
})
// → { ok: true, uploadId: "ghi-789-...", mimeType: "image/png", size: 24512 }Path is workspace-relative and escape-protected (same guard as file_read).
When the user asked you to send / show them a file (a screenshot, a chart, a generated picture, a PDF, a CSV), promote-file is all you need. Its result includes an attachmentMarkdown field — a ready-to-paste markdown tag like  (for images) or [name](gini-upload://<id>) (for other files). Paste that tag verbatim into your reply at the spot where the attachment should appear — it renders inline there (an image shows as a picture, any other file as a download chip), so you can place a screenshot mid-sentence, right where you're describing it. Do not paste the raw uploadId on its own — use the provided attachmentMarkdown tag. The bare uploadId is only an argument to a follow-up tool (signed-upload, vision_query, materialize), never user-facing text.
materialize — Gini upload → workspace fileThe inverse of promote-file: write a Gini upload's bytes to a workspace file. Used when you need a chat-attached (or downloaded / promoted) upload on disk so terminal_exec, code_exec, or a git flow can read the actual file — e.g. committing an image to an asset branch.
skill_run({
skill: "attachments",
script: "materialize",
args: {
uploadId: "abc-123-...", // from the user message marker, signed-download, or promote-file
path: "assets/diagram.png" // optional, workspace-relative; defaults to the manifest filename
}
})
// → { ok: true, path: "assets/diagram.png", absPath: "/abs/.../assets/diagram.png",
// mimeType: "image/png", size: 36116, filename: "diagram.png" }Destination is workspace-relative and escape-protected (same guard as promote-file). When path is omitted it defaults to the upload's original filename (basename, sanitized) at the workspace root, or <uploadId>.<ext> when the manifest has none. absPath is the absolute on-disk path — hand it to commands that need an absolute path (e.g. git hash-object -w <absPath>).
Applies to providers that expose a public prepare → PUT → finalize upload API (Linear, S3 / any presigned-URL backend):
mcp_call. Linear: prepare_attachment_upload({issue, filename, contentType, size}). The response carries the signed URL, headers to send verbatim, and an asset URL for the finalize step.skill_run({skill: "attachments", script: "signed-upload", args: {uploadId, url: <prepared.url>, headers: <prepared.headers>}}).create_attachment_from_upload({issue, assetUrl, title}).uploadId is in the user message marker: Attached image uploads (in order): - <id> (<mime>, <bytes> bytes). Read the mime and size from the same marker so the prepare call doesn't fail with EntityTooLarge / EntityTooSmall.
Targets without a public upload API (e.g. GitHub issues) don't use this recipe — materialize the upload to disk instead (recipe below) and follow that integration skill's own attach flow.
skill_run({skill: "attachments", script: "signed-download", args: {url}}) → {uploadId}.vision_query({uploadId, question: "describe this image"}) → {answer}.mcp_call({server: "linear", tool: "get_attachment", arguments: {id: "..."}}) → response includes the asset URL.skill_run({skill: "attachments", script: "signed-download", args: {url: <assetUrl>}}) → {uploadId}.vision_query({uploadId, question: "..."}) → {answer}.code_exec (matplotlib / d3 / etc.), save under ./chart.png.skill_run({skill: "attachments", script: "promote-file", args: {path: "chart.png"}}) → {uploadId, mimeType, size}.uploadId.When an integration needs the actual file (not an upload id) — e.g. a git flow that has to git hash-object the bytes:
skill_run({skill: "attachments", script: "materialize", args: {uploadId: "<id-from-marker>"}}) → {path, absPath}.absPath to the integration skill that consumes a file — it owns the provider-specific attach flow (e.g. github-issues' "Attaching an image to an issue").When the user attaches a file in chat, the runtime delivers it to you in core — you do not need this skill to read it. The content arrives in the user message (a native document part, or inlined extracted text wrapped in <<<BEGIN/END UNTRUSTED FILE <nonce>>>> markers), and the file is already saved to your workspace at the uploads/<id>/<name> path named in the message. Read the workspace file directly with file_read / code_exec when you need more than the inlined preview. See ADR chat-file-attachments.md. This skill is for agent-initiated byte movement (URL downloads, promoting generated files, sending to an external system), not for reading the user's chat uploads.
skill_run with skill: "attachments". The scripts read JSON from stdin and write JSON to stdout — don't try to terminal_exec them by hand.signed-upload failure, re-run the provider's prepare step rather than retrying with the stale URL.vision_query only accepts image/png and image/jpeg. Convert via terminal_exec (sips -s format jpeg ...) and promote-file the result if you need to vision a different format.SKILL.md documents Linear's args; github-issues documents GitHub's attach flow). This skill owns the byte-PUT / byte-GET / materialize middle steps only.6c5d85e
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.