uploads-cli skill
Reference for the uploads CLI and its stdio/hosted MCP tools — exact flags, keys, and contracts for put and attach, screenshot capture, stable PR/issue keys, the managed attachments comment, metadata and search, galleries, change feeds (repo-wide or one PR/issue), config defaults, issue artifacts, login/doctor, and output formats. Use when driving the `uploads` CLI or its MCP tools (including the hosted MCP at agents.uploads.sh for agents without local filesystem/git access), when you need a public URL for a local file ("upload this", "host this image", "give me a public URL for this file"), when the CLI itself prints a hint or nudge you need to act on (a `hint` field in `--format json`, or the stderr note suggesting `--pr`/`attach --branch`), or when you need exact flags, key layouts, or setup and auth details. For the when-and-how of getting a screenshot or recording into a GitHub PR or issue, start with the github-screenshots skill — it defers here for CLI detail.
- GitHub stars
- 20
- Files
- 1
- Last commit
- Nov 12, 2026
- License
- Apache-2.0
- Allowed tools
- —
What does the uploads-cli skill do?
uploads-cli is an agent skill. Reference for the uploads CLI and its stdio/hosted MCP tools — exact flags, keys, and contracts for put and attach, screenshot capture, stable PR/issue keys, the managed attachments comment, metadata and search, galleries, change feeds (repo-wide or one PR/issue), config defaults, issue artifacts, login/doctor, and output formats. Use when driving the `uploads` CLI or its MCP tools (including the hosted MCP at agents.uploads.sh for agents without local filesystem/git access), when you need a public URL for a local file ("upload this", "host this image", "give me a public URL for this file"), when the CLI itself prints a hint or nudge you need to act on (a `hint` field in `--format json`, or the stderr note suggesting `--pr`/`attach --branch`), or when you need exact flags, key layouts, or setup and auth details. For the when-and-how of getting a screenshot or recording into a GitHub PR or issue, start with the github-screenshots skill — it defers here for CLI detail.
How to install the uploads-cli skill
Each command copies the skill folder into the client's skills directory. Restart the client or start a new session to load it.Install the uploads-cli skill in Plugin
/plugin marketplace add buildinternet/uploads /plugin install uploads@uploads
Install the uploads-cli skill in Claude Code
~/.claude/skills/uploads-cli/SKILL.md
$ npx degit buildinternet/uploads/plugins/claude/uploads/skills/uploads-cli ~/.claude/skills/uploads-cliInstall the uploads-cli skill in Claude Code (project)
.claude/skills/uploads-cli/SKILL.md
$ npx degit buildinternet/uploads/plugins/claude/uploads/skills/uploads-cli .claude/skills/uploads-cliInstall the uploads-cli skill in Codex
~/.agents/skills/uploads-cli/SKILL.md
$ npx degit buildinternet/uploads/plugins/claude/uploads/skills/uploads-cli ~/.agents/skills/uploads-cliInstall the uploads-cli skill in Gemini CLI
~/.gemini/skills/uploads-cli/SKILL.md
$ npx degit buildinternet/uploads/plugins/claude/uploads/skills/uploads-cli ~/.gemini/skills/uploads-cliInstall the uploads-cli skill in Claude.ai
npx degit buildinternet/uploads/plugins/claude/uploads/skills/uploads-cli uploads-cli && zip -r uploads-cli.zip uploads-cli # claude.ai → Customize → Skills → + → Create skill → Upload a skill → uploads-cli.zip
SKILL.md
View on GitHubUploading files to uploads.sh and embedding in GitHub
What this does and why
GitHub's native image hosting (github.com/user-attachments/…) is reachable from
a browser session and, since GitHub CLI 2.99 (September 2026), from
gh … --attach on an existing PR or issue with push access. There is still no public
REST endpoint, and the hosted file is private to GitHub. Any other image URL you
put in a PR/issue body written with gh … --body-file must already point at
something publicly hosted. The github-screenshots skill says when gh --attach
alone is enough.
This skill covers both transports: the uploads CLI (local files, git,
localhost) and the hosted MCP at https://agents.uploads.sh/mcp (bytes you
already have, or a public HTTPS URL to fetch, no checkout). Both PUT to the
uploads.sh API and return a stable public URL plus ready-to-paste markdown.
For PRs and issues the managed attachments comment is available on both.
The CLI can fall back to local gh. Hosted MCP is bot-only.
MCP vs CLI
Same product, two transports. Skills do not install a binary.
| Need | Use | Why |
|---|---|---|
| Bytes already in context (ChatGPT attachment, base64) | Hosted MCP put |
files: [{ filename, contentBase64 }]. Pass repo + (pr | branch). No git inference. |
| File on a sandbox disk, shell but no CLI | Hosted MCP sign_upload_url |
Returns one URL + curl -T line per file (15 min, up to the plan's upload limit). The PUT response is put's JSON. With pr/issue, call comment once after. Sandbox must allow agents.uploads.sh. |
| File already at a public HTTPS URL | CLI put --url or hosted MCP put |
CLI: uploads put --url https://… --pr 123. Hosted: { contentUrl } (filename optional when the URL path has a leaf). Worker/CLI fetches; no auth headers. Hosted rejects private/internal hosts. CLI (and stdio MCP) also fetch http://localhost / 127.0.0.1 / *.localhost. LAN and link-local stay rejected. |
| List, find, metadata, comment, promote | Either | Hosted: list, find_files, get_metadata / set_metadata, comment, promote. CLI: uploads list / find / meta / comment / attach --promote. |
| Recent tagged files, by pull request or by repo | Hosted MCP list_activity / list_repo_files |
Hosted only. list_activity groups by pull (default) or repo. list_repo_files needs repo. Times are tag metadata, not upload time. Does not publish a feed. |
| Live page of this PR's (or repo's) screenshots | Either | CLI uploads feed create or MCP feed_create. Repo-wide: repo only. One PR: repo + pr, or github (owner/repo#123 / a GitHub URL). Same product — not a curated gallery. feed_get / uploads feed show opens an existing feed. |
| Local path or current-branch attach | CLI | Hosted server has no filesystem and no attach tool. Use put instead. |
localhost / private-network screenshot |
CLI uploads screenshot --via local |
Remote render cannot reach your machine. |
| Selector annotate on a live page | CLI uploads screenshot --annotate --via local |
Remote backend rejects selector-bearing specs. |
| Neither transport | Stop | Do not treat npm install -g as the ChatGPT path. OAuth on https://agents.uploads.sh/mcp is the published remote path. |
CLI examples in the rest of this skill assume a checkout and the uploads
binary. Hosted tool contracts live under Notes and cautions (the MCP
bullet) below.
For the common case, use uploads attach <file...>. It infers the current branch's
PR, uploads every file under stable attachment keys (in parallel), and maintains
the comment by default. One bad file does not block the rest — JSON includes
uploads and failures (exit 1 when any failed):
uploads attach ./before.png ./after.png
uploads attach ./shot.png --issue 45 --repo buildinternet/uploads
Pass --no-comment when only stable URLs are wanted. Use put for lower-level
naming and output control.
Issue artifacts (on for issues). GitHub issue artifacts are not the
managed comment. gh issue artifact create --attach is fine for GitHub-only
media on an issue plan or root-cause write-up: it copies bytes to
github.com/user-attachments, which is auth-gated and not shareable. Use
uploads for pull requests, comments, public links, video, and
private-to-public files. The github-screenshots skill has the choice in full.
On an issue, uploads attach also creates or updates one Markdown artifact
named uploads.sh media (the same block as the managed comment; images use
uploads.sh embed or storage URLs; video is a link; no --attach and no byte
copy to GitHub). When the issue has a gallery, it also creates or updates one
link artifact named uploads.sh gallery. A later run finds them by those
names and updates them in place. Pull requests are unchanged. A GitHub error,
including HTTP 429, prints a warning and does not fail the attach.
Opt out, first match wins:
--no-issue-artifacts, or stdio MCPissueArtifacts: false.comment.issueArtifacts: falsein.uploads.yml.- Issue artifacts off under the workspace's Settings → GitHub.
- Otherwise on.
The CLI and stdio MCP read that workspace setting from the comment API.
Hosted MCP has no attach tool and does not write issue artifacts.
Attach an already-uploaded object (issue #702). An attach argument that
doesn't exist on disk but resolves as a workspace object key (e.g.
f/AbC123/shot.webp) or an uploads.sh URL (storage host, embed host, or
/f/ page) attaches via a server-side copy instead of erroring
file not found — no re-download/re-upload round trip. The source's own
derived metadata (path/url/viewport/state/…) rides along; gh.repo/
gh.kind/gh.number/gh.ref are stamped fresh. Copy by default; --move
deletes the source after a successful copy. A path that exists on disk always
wins as a local file, even if it would also parse as a key.
uploads attach f/AbC123/shot.webp --pr 123
uploads attach https://storage.uploads.sh/<workspace>/f/AbC123/shot.webp --pr 123 --move
Stage as you go, before a PR exists. uploads attach ./shot.png --branch [name] stages files under gh/<owner>/<repo>/branch/<branch>/<filename>
instead of a PR/issue number — same upload path, no target flags, no comment
(there's nothing to comment on yet). With no value, --branch resolves the
current git branch; / in the name sanitizes to -. Attach this way at every
visual milestone during the work, not just once at the end. Staged files carry
gh.status=staged until promotion flips them to promoted, so
uploads find gh.status=staged (add gh.branch=<name> to narrow) lists what's
still in flight. The server also stamps gh.uploader/gh.uploader-id (from
the token's minting user) on gh.*-tagged uploads, so
uploads find gh.status=staged gh.uploader=<login> narrows to one
contributor's in-flight files.
Check what's staged: uploads staged. A dedicated read-only view —
"what's staged for this branch, and will it auto-attach?" — instead of
hand-building the find/list query above:
uploads staged # current branch, repo from gh/git remote
uploads staged --branch feature/thing --repo owner/name
uploads staged --format json
Same branch/repo resolution as attach --branch (current git branch by
default, worktree-safe). A file already on a pull request is not listed.
That includes gh.status=promoted, a gh.promoted-to or gh.promoted-at
marker, and a pull-request copy of the same filename whose copy is at least
as new as gh.staged-at. A newer re-stage of that filename stays. On a
private repo the staged key is gh/private/<id>/branch/<filename>. The
branch name is not in the key, because the id belongs to one branch.
uploads staged --branch lists that id and keeps a file whose gh.branch
is that branch. A file with no gh.branch stays, because it was listed
under this branch's prefix. A file with gh.status=unstaged is omitted.
uploads staged never writes.
Release held files: uploads approve. The classifier can hold a file
it thinks shows a secret or sensitive personal data. The managed comment then
shows 🔒 N files are held for review instead of the image. Check each file
before approving; if it shows a real credential, rotate it and replace the
file. Needs files:write.
uploads approve --pr 123 # list held files and why; changes nothing
uploads approve --pr 123 --all # approve every held file on the PR
uploads approve gh/owner/name/pull/123/settings.png
From MCP: list_held (pass pr/issue, plus repo on the hosted server)
lists held files and why, and approve takes keys or a pr/issue.
get_metadata still answers for a held file and shows ai.hold=pending.
The classifier decides after the upload returns, so a put that may still be
held carries holdCheck: "pending". Later syncs of the PR comment report
held: <n> (and the CLI prints a line) while files there are held.
Clear a branch: uploads unstage. Marks files gh.status=unstaged so
they drop out of uploads staged, auto-promotion, and the Claude plugin
band. The object and its URL stay. Needs files:write, not files:delete.
uploads unstage --branch feature/thing
uploads unstage --branch feature/thing --repo owner/name --dry-run
uploads unstage gh/owner/name/branch/feature-thing/shot.png
--branch does not default to the current git branch. Pass keys instead of
--branch to unstage those objects, including one that was already promoted.
On a private repo the command lists this branch's prefix ids
(gh/private/<id>/branch/…), the same set uploads staged uses. Keys and
--branch together are an error.
Local stdio MCP mirrors this as unstage (branch or keys, optional
repo and dryRun). The hosted MCP tool is also unstage. Hosted has no
git defaults, so repo is required with branch.
uploads list has no --branch. An unknown flag, including --branch, is
an error. Use uploads staged --branch <name>.
Human mode prints one compact line per staged file (filename, size,
gh.staged-at, public URL), then a binding: line and once the PR exists: uploads attach --promote (the promote line is omitted for binding: other
— promoting from a non-owning workspace would be rejected by the
cross-tenant gate). Nothing staged prints a single zero-state line.
--format json (or global --json) always emits a valid document —
{ repo, branch, files, binding } — even with zero files; files is [],
never empty stdout.
binding folds in the same repo↔workspace check the stage-time warning uses
(see "Repo binding" below), so you don't have to separately reason about it:
binding.state |
binding.autoAttach |
Meaning |
|---|---|---|
self |
true |
Repo is bound to this workspace — staged files auto-attach on PR open. |
none |
false |
Repo isn't linked yet — link it (uploads github link) or nothing auto-attaches. |
other |
false |
Repo is linked to a different workspace — these files won't auto-attach from here. |
unknown |
false |
Binding check failed (offline, or an older server without the route) — advisory only, never blocks the view. |
The none/other wording is the exact same advisory text as the
attach --branch stage-time warning (issue #398) — one source of truth, so
the two surfaces never drift.
Local stdio MCP mirrors this as the staged tool (branch/repo args,
same { repo, branch, files, binding } shape). The hosted MCP has no
dedicated staged tool (no git defaults) — list/find_files recipes and
hosted put/promote with explicit repo/branch are under Notes and
cautions (the MCP bullet) below.
Getting those files into the PR's attachments comment needs no extra step once a PR exists for that branch:
- GitHub App installed on the repo: a webhook auto-promotes staged files into the PR's attachment prefix and creates/updates the managed comment the moment the PR opens, reopens, or gets a new commit.
- No GitHub App: the next
uploads attachtargeting that PR auto-promotes those staged files into the PR's attachment prefix before the comment refresh. If that first attach has nothing new to upload, runuploads attach --promote(zero file arguments) to promote and refresh the comment on its own; it exits0even when nothing was staged. Skip auto-promotion on a given call with--no-promote.
Promotion only applies to PRs, never issues, and both paths degrade silently (no error) if the workspace's server doesn't support promotion yet.
Branch renamed before the PR existed? Staged keys embed the branch name at
stage time, so promotion under the new head ref would otherwise find nothing.
This is followed automatically: any uploads staging or promote command run
on the renamed branch (attach --branch, put, attach --pr, attach --promote) reads the rename from the branch's git reflog, registers it with
the server, and promotion sweeps the old name too. That needs one more
uploads run on the branch before the PR opens. If the branch was renamed or
deleted and the PR opened without one, the files sit staged (URLs still work)
but never attach — recover with
uploads attach --pr <n> --from-branch <old-branch-name> (zero file arguments
is fine): it promotes that stale branch prefix into the PR and refreshes the
comment. Works for both plain and private-repo staging.
Promotion needs the repo already bound to the workspace. Both the webhook
and the CLI-triggered path above rely on the same repo↔workspace binding used
by the managed comment (see "Repo binding" below) — any earlier successful
attach/comment/promote call against that repo binds it implicitly, or
uploads github link claims it explicitly. A repo that has never been
bound and is only ever staged with --branch sees no error and no comment —
promotion is a silent no-op at PR-open time. If you can't confirm the repo is
already bound, don't promise auto-attach; the zero-setup fallback that works
regardless of binding history is running uploads attach --promote (or any
targeted uploads attach) once the PR exists.
Comment missing? First, give it a moment — if the App is installed and
subscribed to issue_comment, a deleted or mangled bot comment self-heals on
the next webhook delivery; don't panic-repost. If it's still missing, check
the repo↔workspace binding — uploads github link --status (read-only, shows
the binding without claiming it). See "Repo binding" below.
The killer feature for GitHub: --pr/--issue produce hash-free, stable keys
(gh/<owner>/<repo>/pull/<num>/<name>), so re-uploading the same filename overwrites
in place and the URL never changes. There is no confirmation prompt — hot-swap is
intentional for agents and re-runs. Human mode prints
>> replaced existing object (same URL) after overwrite; JSON has
"replaced": true|false. Use --dry-run to preview: it prints
>> would replace existing object (same URL) when the key already exists,
without writing.
Every other key is strict (issue #174): an explicit --key, or the
default put path with no --pr/--issue, refuses to overwrite an existing
object — the CLI error names the existing object's URL and tells you to add
--replace (MCP: replace: true). Set UPLOADS_OVERWRITE=1 to restore
always-overwrite for those paths. --dry-run previews the refusal too:
>> would refuse: key already exists. This never applies to --pr/--issue
keys, which always hot-swap regardless.
Responses include two public URLs when the shared dual-host setup applies:
| Field | Host (default) | Use for |
|---|---|---|
url |
storage.uploads.sh |
Durable link, click-through, non-GitHub embeds |
embedUrl |
embed.uploads.sh |
GitHub PR/issue markdown (<img src> / ![]()) |
embedUrl is the same object with badge-style no-cache headers so GitHub Camo
revalidates after an overwrite. CLI/MCP markdown and the managed attachments
comment already prefer embedUrl. Override with UPLOADS_EMBED_PUBLIC_BASE_URL
(empty disables; self-host set your no-cache CDN base).
Prerequisites
- No shell / ChatGPT? Skip this section. Use the hosted MCP
(
https://agents.uploads.sh/mcp) and the table above. Do not install the CLI. - Node.js ≥ 22.
- The CLI. Install globally for repeated agent use, or run it once with
npx:
Every example in this skill uses the globalnpm install --global @buildinternet/uploads npx @buildinternet/uploads --help uploads --versionuploads …binary (as after install). Inside the uploads monorepo only,pnpm uploads …builds from local source first — do not write product/PR examples that way. Prefer--jsonor--quietfor scripted steps (keeps stderr clean and skips optional update-available hints). - A configured token (one-time — see below). Check with
uploads doctor. ghCLI, authenticated — only for the--comment/commentfeatures that write to a PR/issue. Plain uploads don't need it.
Files in this skill
1uploads-cli skill questions
How do I install the uploads-cli skill in Claude Code?
Run npx degit buildinternet/uploads/plugins/claude/uploads/skills/uploads-cli ~/.claude/skills/uploads-cli. Claude Code loads skills from ~/.claude/skills (all projects) or .claude/skills (one project) and uses a skill when its description matches the task.
Which clients can use the uploads-cli skill?
Any client that reads the Agent Skills format: Claude Code, Codex, Gemini CLI and claude.ai (upload as a ZIP). Scripts in the skill run only where the client allows code execution.