# Uploads Cli

> >-

- **Type:** Skill
- **Install:** `agentstack add skill-buildinternet-uploads-uploads-cli`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [buildinternet](https://agentstack.voostack.com/s/buildinternet)
- **Installs:** 0
- **Category:** [Cloud & Infrastructure](https://agentstack.voostack.com/c/cloud-infrastructure)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [buildinternet](https://github.com/buildinternet)
- **Source:** https://github.com/buildinternet/uploads/tree/main/skills/uploads-cli
- **Website:** https://uploads.sh

## Install

```sh
agentstack add skill-buildinternet-uploads-uploads-cli
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Uploading files to uploads.sh and embedding in GitHub

## What this does and why

GitHub's native image hosting (`github.com/user-attachments/…`) is only reachable
through an authenticated **browser session** — there is no `gh` CLI or REST endpoint
for it. So any image URL you put in a PR/issue body written with `gh … --body-file`
must already point at something publicly hosted.

This skill solves that with the **`uploads` CLI**: it PUTs a local file to the
uploads.sh API, which returns a stable public URL you can drop straight into
markdown. No browser, no repo bloat, no signing by hand. For PRs and issues it can
also create and maintain a single "attachments" comment for you via your local
`gh` auth.

For the common case, use `uploads attach `. 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):

```bash
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.

The killer feature for GitHub: `--pr`/`--issue` produce **hash-free, stable keys**
(`gh///pull//`), 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. 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** (`` / ``) |

`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

- **Node.js ≥ 22.**
- **The CLI.** Install globally for repeated agent use, or run it once with `npx`:
  ```bash
  npm install --global @buildinternet/uploads
  npx @buildinternet/uploads --help
  uploads --version
  ```
  Every example in this skill uses the **global** `uploads …` 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 `--json` or `--quiet` for scripted steps (keeps stderr clean and skips
  optional update-available hints).
- **A configured token** (one-time — see below). Check with `uploads doctor`.
- **`gh` CLI, authenticated** — only for the `--comment` / `comment` features that
  write to a PR/issue. Plain uploads don't need it.

## One-time setup

Config lives in a user-owned file so it survives skill reinstalls:

```
~/.config/buildinternet/config        # or $XDG_CONFIG_HOME/buildinternet/config
```

Resolution is **per key, first match wins**: CLI flags (`--api-url`, `--token`,
`--workspace`) → `UPLOADS_*` environment vars → `--env-file ` →
`$BUILDINTERNET_CONFIG` → the shared config file. For a one-off against a different
API or workspace, just export the var or pass `--env-file`.

The fastest path is `uploads login`. Have a workspace admin invite your
email to a workspace first, then run it once, interactively, to sign in:

```bash
uploads login          # opens a browser to approve sign-in, saves config, runs doctor
uploads login --workspace acme   # only needed if your account can access more than one
```

That's a one-time, human-in-the-loop step (device sign-in needs a browser); once the
config file is written, every later `uploads` invocation — including from a
non-interactive agent — just reads the saved token. Routine agents never need
`ADMIN_TOKEN`.

**Inviting a teammate** (workspace admin/owner only): `/account/workspaces` in the
browser, or:

```bash
uploads invite create --email teammate@example.com --workspace acme
```

Device login as you (not `ADMIN_TOKEN` / not a workspace token). The CLI prints an
accept URL to share if email isn’t configured. Invitee accepts, then `uploads login`.

For headless machines with no browser at all, an operator can mint a token directly
(`/admin/tokens`, `ADMIN_TOKEN`-gated — see `docs/admin-tokens.md`) and hand it to the
agent as `UPLOADS_TOKEN`, or an enrollment code (`upe_…`, an alternative invite-link/code path — useful
when you don't have the recipient's email) can be exchanged with `uploads login --code`.
Neither is the normal path for new setups.

The resulting token defaults to 90 days and `files:read` plus `files:write`; it cannot
delete files unless an administrator explicitly grants `files:delete`. Verify or inspect
setup at any time:

```bash
uploads setup                                  # shows effective configuration
uploads doctor                                 # version + health + auth + workspace
uploads doctor --json
```

Tokens encode their workspace (`up__…`), so the CLI infers `--workspace`
when you don't set it. Legacy administrator-minted tokens remain valid. See "Config
commands" for setting put defaults
(default repo, prefix, image width) once instead of per-command.

## Core workflow: `uploads put`

Upload one or more files and get back URL(s) plus ready-to-paste markdown.
Multiple paths upload in parallel; multi-file JSON is `{ uploads, failures }`
(exit `1` when any failed). Single-file JSON stays a flat object.

```bash
uploads put ./shot.png --repo myorg/myapp --ref 1722 --alt "New live feed cards" --width 700
uploads put ./before.png ./after.png
```

Human output goes to stderr; the URL and markdown to stdout, so you can pipe or
capture them. Use `-` as the file to read from stdin.

Key options (`uploads put --help` for all):

| Flag                                  | Purpose                                                                                                                                                |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--alt `                        | Alt text for the markdown (default: filename). Always write meaningful alt text.                                                                       |
| `--width `                        | Emit sized `` HTML instead of `` (markdown can't size images).                                                                       |
| `--repo `                 | Repo segment of the auto key (default: git remote, or `UPLOADS_DEFAULT_REPO`).                                                                         |
| `--ref `                          | PR/issue/branch/date segment (default: today, or `UPLOADS_DEFAULT_REF`).                                                                               |
| `--destination `                  | Typed root: `screenshots` \| `gh` \| `f` (sets key prefix).                                                                                            |
| `--prefix `                     | Key prefix (default: `screenshots`, or `UPLOADS_DEFAULT_PREFIX`).                                                                                      |
| `--key `                         | Set the object key explicitly; skips the auto-naming below.                                                                                            |
| `--name `                       | Clean filename for the key's leaf + default alt (no `/`); keeps the `--pr`/default path. Not with `--key`.                                             |
| `--dry-run`                           | Resolve + print the key and final public URL without uploading; reports if the key would replace an existing object. Not with `--comment`/`--gallery`. |
| `--content-type `               | Override the content type (else inferred from extension; ignored when optimize rewrites the body).                                                     |
| `--frame `                        | Opt-in chrome before optimize: `phone`, `browser`, `iphone-16-pro`.                                                                                    |
| `--frame-url `                   | Address bar text for `--frame browser`.                                                                                                                |
| `--frame-fit cover\|contain`          | How the shot fills the screen (default: `cover`).                                                                                                      |
| `--no-optimize`                       | Skip client-side image optimization (default: still images → WebP). Or `UPLOADS_NO_OPTIMIZE=1`.                                                        |
| `--optimize-max-edge `            | Max long edge when optimizing (default: 2400).                                                                                                         |
| `--optimize-quality `          | WebP quality when optimizing (default: 85).                                                                                                            |
| `--keep-exif`                         | Keep EXIF/XMP/ICC when optimizing (default: **strip** for privacy). Or `UPLOADS_KEEP_EXIF=1`.                                                          |
| `--no-git`                            | Don't derive `--repo` from the git remote (or `UPLOADS_NO_GIT=1`).                                                                                     |
| `--format human\|url\|markdown\|json` | Control stdout. `--json` (global) forces json.                                                                                                         |
| `-w, --workspace `              | Override workspace (wins over env and token inference).                                                                                                |

**Image optimization (default on):** PNG/JPEG and similar still images are re-encoded to
WebP (long edge capped at 2400px, quality 85) before upload so PR/issue embeds stay
lean. The object key/filename extension follows the output (e.g. `shot.png` →
`…/shot.webp`). **EXIF/XMP is stripped by default** (public URLs + privacy); pass
`--keep-exif` when the discussion needs the embedded image metadata. Animated GIF,
SVG, video, and non-images are left alone; if the optimized payload is not smaller,
the original is uploaded. Use `--no-optimize` when you need lossless originals.

**Frames (opt-in):** `--frame phone` (generic bezel), `--frame browser`, or
`--frame iphone-16-pro` (community device art, cached under
`~/.cache/uploads/frames`). Default is **no frame**.

**How keys work** — three paths, no extra naming modes:

| Intent                                | Command                                                        |
| ------------------------------------- | -------------------------------------------------------------- |
| Just upload it, give me a URL         | `uploads put ./file.png`                                       |
| Explicit typed destination            | `uploads put ./file.png --destination screenshots`             |
| Stable GitHub embed I might re-upload | `uploads put ./file.png --pr `                            |
| Stable `--pr` path but a clean leaf   | `uploads put ./capture-2026-…Z.png --pr  --name hero.png` |
| I know exactly where it goes          | `uploads put ./file.png --key screenshots/…/x.png`             |

Timestamped captures break stable `--pr` keys — pass `--name hero.webp` to keep a
clean leaf. Use `--dry-run` to preview the exact public URL before uploading.

Default `put` is the fast path; you don't need `--key`, `--prefix`, or `--repo`. Without
`--key`, keys look like
`///-.` — the short hash
prevents collisions without random names or a separate "preserve name" flag. Prefer
`--destination screenshots` (or `gh` with `--pr`/`--issue`) over inventing roots —
workspaces may allowlist only those destinations. Override with `--key` only when you
have a reason, and keep the key under an allowed root.

**Output formats** — pick what you'll consume:

```bash
uploads put ./shot.png --format url        # just the URL, for scripting
uploads put ./shot.png --format markdown   # just the / snippet
uploads put ./shot.png --json              # {workspace,key,url,size,markdown}
```

## Custom metadata & search

Every object can carry queryable key-value metadata (distinct from optimize/frame
provenance) — tag uploads at put time, then find them later. Useful pairs for
screenshots:

| Key    | Example                        | Use                                |
| ------ | ------------------------------ | ---------------------------------- |
| `url`  | `https://app.example/settings` | Page URL the screenshot represents |
| `path` | `/settings`                    | In-app route or navigation path    |
| `app`  | `web` / `ios` / `android`      | Which surface is shown             |

```bash
uploads put ./settings.png \
  --meta url=https://app.example/settings \
  --meta path=/settings \
  --meta app=web

uploads attach ./checkout.png \
  --meta url=https://app.example/checkout \
  --meta path=/checkout \
  --meta app=ios

uploads meta get screenshots/myapp/42/settings.webp
uploads meta set screenshots/myapp/42/settings.webp path=/onboarding --delete url
uploads list --meta app=web --meta path=/settings   # ANDed, repeatable
uploads find app=web path=/settings                 # same filter, positional pairs
```

Rules (validated client-side, fail-fast, before uploading): key
`^[a-z][a-z0-9._-]{0,63}$` (lowercase, dot-namespacing allowed, e.g. `gh.repo`);
value 1–512 printable ASCII characters; `--meta k=v` may repeat up to 24 times per
request; a value may itself contain `=` (only the first `=` splits key from value).
`content-sha256` and `visibility` are reserved (server-computed / the real R2
visibility gate, respectively). `uploads attach` writes its own `gh.*`
reserved-by-convention keys automatically — see below.

On the default `screenshots/…` path, `put` also auto-derives GitHub context and
stamps `gh.repo`/`gh.kind`/`gh.number`/`gh.ref` from the current branch's PR (or
a numeric `--ref`), so the file's `/f/` page shows an "Attached to" link. This is
on by default and best-effort; disable it with `--no-auto`, `--no-git`, or `UPLOADS_NO_AUTO_META=1`.
On this auto path an explicit `--meta gh.*` overrides the auto-derived value — the
opposite of the `--pr`/`--issue` precedence below, where the target's own `gh.*` always wins.

**Re-upload semantics:** re-uploading to an existing key **with** `--meta` replaces
that file's entire metadata set (delete-then-set, not a merge); re-uploading
**without** `--meta` at all preserves the existing metadata untouched. Use
`uploads meta set` to edit individual keys without disturbing the rest.

Non-`gh.*` metadata values supplied via `--meta` (CLI) or `metadata` (MCP) render
on the object's public `/f/` file page. Treat them like the URL itself:
don't put internal notes, secrets, tokens, IDs, or private paths in them.

## Public media galleries

Use galleries when several existing public uploads should be shared as one ordered collection.
A gallery has an opaque, API-returned public URL; do not derive one in scripts. **Anyone who
knows the URL can view the gallery and its media**. GitHub repository visibility does not make
it private, and

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [buildinternet](https://github.com/buildinternet)
- **Source:** [buildinternet/uploads](https://github.com/buildinternet/uploads)
- **License:** MIT
- **Homepage:** https://uploads.sh

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-buildinternet-uploads-uploads-cli
- Seller: https://agentstack.voostack.com/s/buildinternet
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
