attachments
Discover, select, download, and inspect GitHub ticket attachments. Use when an issue or pull request contains screenshots, mockups, documents, or uploaded files needed as task context.
Файлы1
Открыть в GitHubSKILL.mdflow/skills/attachments/SKILL.md
GitHubConversation-owning agent only: selection requires user interaction in Step 2. Never delegate this procedure to a worker/subagent that cannot surface questions to the active conversation.
Attachment Handling
After fetching the ticket, check for attachments that may provide additional context (mockups, design specs, API docs, screenshots, etc.).
Step 1: Discover Attachments
If the calling skill already provides a discovered attachment list (e.g. from the context-gatherer digest), use it and skip the scan below — continue at Step 2.
Scan body and each comments[].body for URLs matching these domain patterns, embedded with either  (image embed) or [text](url) (link) markdown syntax:
https://user-images.githubusercontent.com/...https://github.com/<owner>/<repo>/assets/...https://github.com/user-attachments/files/...https://github.com/user-attachments/assets/...
Note: user-attachments/assets/ URLs are often extensionless. If embedded with ![...]() syntax, classify as image; if embedded with [...]() syntax, classify as document (file download).
Extract the display name (alt text or link text, fallback to filename from URL) and URL. Size is unknown until download.
Note: this display name is for presentation only (Step 2 prompt). It is issue-author-controlled and is never used as the on-disk download filename — Step 3 derives that separately and validates it.
If no attachments found → skip the rest of this procedure and return to the calling skill.
Step 2: Present to User
Classify each attachment by extension:
- image: png, jpg, jpeg, gif, svg, webp
- document: md, txt, json, yaml, yml, csv, xml, html, log, pdf
- binary: everything else
Use the client's available user-input mechanism and allow multiple selections:
"Found N attachment(s) on this ticket. Which would you like to download for context?"
Options (one per attachment if ≤ 4, grouped by type if > 4):
"<name> (<type>, <size>)"per attachment when ≤ 4"All images (N)","All documents (N)","All other files (N)"when > 4
If user selects none → skip the rest of this procedure and return to the calling skill.
Step 3: Download Selected Attachments
Store downloads under a freshly created, atomically-unique private directory —
never a shared, predictable path, which would let a symlink be pre-planted at a
guessable resolved download filename before the run starts. Create it as its
own standalone Bash call — never a shell-variable assignment that captures
the command's own output (an assignment wrapper never matches a granted
Bash(mktemp -d:*) prefix, and shell-rules forbids command substitution
where the agent can run the command and read the result):
mktemp -d "${TMPDIR:-/tmp}/cenci-attachments-XXXXXX"
mktemp -d both creates the directory and guarantees its name is unique and
unpredictable, so concurrent runs never collide and nothing can pre-plant a
symlink at it — no <scope> subdirectory is needed for uniqueness.
Verify before downloading, as its own standalone Bash call:
test -d "<attach-dir>"
Confirm the command succeeded and the printed directory is a non-empty,
existing directory before any download proceeds. If mktemp -d fails (disk
full, unwritable TMPDIR, etc.), abort the entire attachment step rather than
continuing with an unverified directory.
Carry the printed path forward as the literal text <attach-dir> for
every reference in the rest of this step; do not hold it in a shell
variable. Bash-tool shell state does not persist between calls, so a
shell-variable reference to a directory captured earlier would expand to
empty in a later, separate Bash call, and every path built from it would
silently collapse to an absolute root-relative path — precisely the hazard
the verification above guards against. This mirrors refine/SKILL.md's
per-run <token> rule: carry the literal value forward as text, never
re-derive it or rely on shell state across calls.
Prefer a connected GitHub attachment-download tool when one is available, especially
for private user-attachments URLs. Otherwise use curl. Before downloading, resolve
a safe on-disk filename for each attachment — never the Step 1 display/alt text, which
is issue-author-controlled and presentation-only, not a trusted filesystem input:
Derive a basename from the URL. Strip any
?queryand#fragment, then take the last/-separated path segment of what remains. Do not percent-decode this segment — decoding could re-introduce/or..sequences that the raw segment didn't contain.Validate the basename. It must match
^[A-Za-z0-9._-]+$, must not be exactly.or.., and must not contain..anywhere — the same rule used for scope keys in theshell-rulesskill (do not weaken this to a bare character-class check). Then cap its length: if the validated basename exceeds 200 characters, truncate the stem (everything before the last.) sostem+ext ≤ 200, preserving the extension unchanged — a conservative cap that leaves headroom below commonNAME_MAX(255) filesystem limits for the-<k>collision suffixes applied later.Fall back if invalid, absent, or extensionless. Generate
attachment-<n>(where<n>is the attachment's 1-based index within the current run's selected set — stable and deterministic, not a global counter) when the basename fails validation, when the URL has no last path segment at all, or when the segment is valid but has no extension — no.in it (e.g. extensionlessuser-attachments/assets/<uuid>URLs). An extensionless basename cannot satisfy Case A below, which requires the extension to already be known before download; Content-Type sniffing in Case B supplies it instead. Never fall back to the raw invalid value and never to the Step 1 display/alt text.Resolve and download — the sequencing differs by which path produced the name, because a fallback name's extension can only be known after the response arrives:
Case A — valid URL basename (step 2 validation passed). The filename, including its original extension, is already fully known before any download starts — no sniffing applies. Check it for a collision against files already written this run in
<attach-dir>; if it collides, append-<k>before the extension, starting atk=2and incrementing until unique (e.g.report.pdf→report-2.pdf). For a multi-dot name, split on the last.only — the extension is everything after the final dot, soarchive.tar.gz→archive.tar-2.gz. Then download straight to the resolved name:curl -fsSL "<url>" -o "<attach-dir>/<resolved-filename>"Case B — fallback (
attachment-<n>). The extension is unknown until the response's Content-Type is inspected, so the collision check and final name cannot be resolved before downloading:Download to a provisional, extension-less path using the fallback name, capturing the response's Content-Type via curl's write-out format (or the connector tool's response, if used instead):
curl -fsSL "<url>" -w '%{content_type}' -o "<attach-dir>/attachment-<n>.partial"If this download reports failure (non-zero curl exit, or the connector tool's equivalent failure signal), discard the
.partialfile and do not proceed to steps 2–4 — this is a download failure for this attachment: apply the "On any download failure → warn and continue with remaining files" rule below.Map the captured Content-Type to an extension:
image/png→.png,image/jpeg→.jpg,image/gif→.gif,image/webp→.webp,image/svg+xml→.svg,application/pdf→.pdf,text/plain→.txt,text/markdown→.md,application/json→.json,text/csv→.csv,text/html→.html,application/xmlortext/xml→.xml. An unknown or missing Content-Type means no extension is appended.Check the extension-bearing name (
attachment-<n><ext>) for a collision against files already written this run in the same directory; if it collides, append-<k>before the extension, starting atk=2and incrementing until unique.Move the provisional file to the final resolved name with a portable
mv(no bash-only construct):mv "<attach-dir>/attachment-<n>.partial" "<attach-dir>/<resolved-filename>"If this
mvfails (disk full, permissions, etc.), treat it as a download failure for this attachment — apply the same "On any download failure → warn and continue with remaining files" rule; never leave the.partialfile in place as a substitute and never read/inspect it directly under its provisional name.
If a private-repository download fails and no connector can download it, retry with the authenticated GitHub CLI token without printing the token.
Post-download: check size with stat. If > 10 MB and user wasn't warned → report and skip that file.
On any download failure → warn and continue with remaining files.
Step 4: Load Attachment Content
For each downloaded file, use the client's matching file or image inspection tool:
- Images (png, jpg, jpeg, gif, webp): use multimodal image inspection
- SVG: read as text unless visual rendering is specifically needed
- Text files (md, txt, json, yaml, yml, csv, xml, html, log): read as text
- PDF: use the client's PDF reader or extraction support
- Binary/other: Report file path, note content cannot be read as text
After reading each image, produce a brief structured summary (5–15 lines) covering applicable aspects:
- Type: mockup, wireframe, screenshot, diagram, flowchart, error state, etc.
- Layout: overall structure, regions, and spatial arrangement
- UI elements: buttons, inputs, tables, navigation, modals, etc. — include their labels
- Text content: headings, labels, error messages, data values visible in the image
- Visual style: colors, spacing, typography, and key design details
- Annotations: arrows, callouts, numbered markers, or highlighted areas
Keep all attachment context available for the rest of the calling skill's execution.