| name | share-file |
| description | Upload a local file to private R2 temp storage and get back a public direct URL that auto-expires. Use when you need to host a screenshot, screencast, GIF, PDF, log or any artifact so it can be embedded in a PR description, pasted into Slack/Linear, or shared as a preview link — anything you would previously have sent to catbox, 0x0.st or an image host. Triggers on "upload this", "share this file", "host this screenshot", "get me a link for", "put this in the PR", "temp storage", "shareable link", "nahraj tohle", "sdílený odkaz". Default retention 90 days, customizable per upload. |
| allowed-tools | ["Bash(share-file:*)","Bash(./share-file.sh:*)","Bash(wrangler r2:*)","Read"] |
share-file
Uploads a file to the private Cloudflare R2 bucket storage and prints a public
direct URL. Replaces catbox.moe.
- Free at normal volume (R2 free tier: 10 GB stored, unlimited egress)
- Auto-expiring — 90 days by default, so temp files clean themselves up
- Direct links with correct
Content-Type, so images and GIFs render inline
Usage
share-file <file> [--expire 7d|30d|90d|365d|keep] [--name <slug>]
Prints the public URL on stdout and nothing else, so it is safe to capture:
URL="$(share-file ./screenshot.png)"
Examples:
share-file ./screenshot.png
share-file ./demo.gif --expire 30d
share-file ./architecture.pdf --expire keep
share-file ./out.gif --name login-flow
Choosing an expiry
| Use case | Flag |
|---|
| Throwaway preview, one reviewer | --expire 7d |
| Screenshot for a PR that will merge soon | --expire 30d |
| Default — PR assets, shared previews | (omit) → 90d |
| Docs or a link you'll reference for a while | --expire 365d |
| Permanent asset | --expire keep |
Pick a shorter window when the link is obviously disposable. Never use keep
unless the user asks for a permanent link — this is temp storage.
Embedding the result
Return the bare URL to the user. When the file is going into a PR description or
a markdown comment, hand back a ready-to-paste snippet:

GIFs, PNGs and JPEGs render inline in GitHub PRs. MP4 does not — GitHub only
plays video uploaded to its own CDN, so an external .mp4 URL degrades to a
plain link. If the user wants an inline video in a PR, convert to GIF first
(the screencast skill already does this) or post the MP4 as a labelled link.
How expiry actually works
R2 lifecycle rules are scoped per prefix, not per object — there is no
per-object TTL. So the script routes each upload into a TTL-named prefix
(90d/2026-07/name-abc123.png) and each prefix carries a matching lifecycle
rule on the bucket. Deletion happens within ~24h of the deadline.
This means: the prefix is the expiry. Never hand-write a key into a
different prefix than the retention you want, and don't move objects between
prefixes expecting the clock to change.
Setup
Nothing account-specific is baked into this skill — it works on any Cloudflare
account. Two one-time steps:
npm install -g wrangler && wrangler login
share-file setup
setup creates the bucket, adds the four lifecycle rules, and enables public
access. It is idempotent, so re-running it is harmless. Add --bucket <name> to
use something other than storage.
R2 must be enabled on the Cloudflare account first (dashboard → R2). Cloudflare
requires a payment method on file to enable it, even though normal
screenshot-sharing volume sits inside the free tier: 10 GB stored, unlimited
egress, 1M writes and 10M reads per month.
The public host is discovered at runtime from the bucket and cached in
~/.cache/share-file/, so a copy of this skill on another machine or account
resolves to that account's own bucket. Override either value if needed:
R2_PUBLIC_BASE="https://cdn.example.com" R2_BUCKET="scratch" share-file ./x.png
Custom domain (optional)
Public delivery defaults to Cloudflare's r2.dev subdomain, which is
rate-limited, uncached, and documented as development-only. That is fine when
links are consumed by a handful of people — GitHub's camo proxy fetches an
embedded image once and serves its own copy to every viewer.
To use your own hostname the domain must be a zone in the same Cloudflare
account. Full nameserver hosting works; so does a partial (CNAME) setup, but
that is Business plan only. Once the zone is present:
wrangler r2 bucket domain add storage --domain files.example.com
export R2_PUBLIC_BASE="https://files.example.com"
Existing object keys are unaffected — only the host changes.
Failure modes
wrangler not authenticated → run wrangler login.
- URL 404s right after upload → custom domain not attached to the bucket, or
DNS still propagating. Verify with
wrangler r2 object get storage/<key> --remote.
- Browser downloads instead of rendering → wrong
Content-Type. The script
maps common extensions explicitly; add to the case block if a format is missing.