Set up your agent

Set up your agent

Teach a coding agent to upload and attach media on its own.

Install #

uploads install finds your agent runtime and adds the agent skills and the hosted MCP server:

uploads install

Using Claude Code or Codex? Install the plugin instead.

Using claude.ai or Cowork? Add uploads from the Claude directory instead.

Or set up by hand

Add the skills:

npx skills add buildinternet/uploads

Then register the MCP server with your runtime:

claude mcp add --transport http uploads https://agents.uploads.sh/mcp

Claude Code & Codex plugins #

The plugin bundles the skills, the MCP server, the pre-PR reminder, and, in Claude Code, mods. In Claude Code, add the marketplace, then install:

/plugin marketplace add buildinternet/uploads
/plugin install uploads@uploads

Codex loads the same repo as a plugin. After you enable it, open /hooks once and trust the hook if Codex asks.

The reminder hook calls the uploads CLI, so install it and run uploads login once.

Tell the agent to capture as it works #

Add a block like this to your project’s instructions file: AGENTS.md, CLAUDE.md, .cursor/rules, or whatever your runtime reads.

AGENTS.md
## Screenshots
When a change is visible (UI, layout, styling), capture it as you go.
Don't wait for the PR. After each meaningful visual change:
uploads put ./shot.png --meta path=/route --state after
On a branch, that stages the file for the PR. After opening the PR, run
`uploads attach --promote` to post everything staged. It's safe to run
even if the GitHub App already posted them.

For how staging works, see Stage before a PR exists.

Want the guided version? The agent walkthrough takes a coding agent from install to a first staged before/after.

Pre-PR screenshot reminder #

Before gh pr create, the reminder hook nudges the agent if the branch touches UI files and nothing is staged. When files are staged, it says whether they attach on their own when the PR opens. If those files are older than 30 days, or they belong to a different branch, it suggests uploads unstage --branch <name> instead. It never blocks the PR. Turn it off with UPLOADS_HOOK_DISABLE=1.

  • Claude Code and Codex: included in the plugin.
  • Grok and Cursor: uploads install adds it when those tools are installed.

Claude Code mods #

In Claude Code, the plugin also ships mods: code that runs inside Claude Code and can add to its interface. The plugin includes:

  • Staged media band: shows the files staged for your branch above the prompt, and attaches them when gh pr create opens the PR.

Mods use the local uploads CLI, so install it and run uploads login once. Change their options, such as the automatic attach, in /plugin. Mods need Claude Code 2.1.287 or later. Older versions, Codex, and hosted MCP setups get the rest of the plugin.

Skill vs. MCP #

The skills teach the agent when to capture and which uploads command to run. They need the CLI.

The MCP server gives the agent upload tools directly. There are two:

Server Use it when Differences
Local, uploads mcp The agent runs on your machine Same tools as the CLI, including attach.
Hosted, https://agents.uploads.sh/mcp The agent has no local checkout No attach and no git defaults. Pass repo and branch to put, then call promote once the PR exists. list_activity and list_repo_files list recent tagged files by pull request or by repo, and only this server has them.

For auth, scopes, and every tool’s parameters, see the MCP server reference.

To use the local server instead of the hosted one:

claude mcp add uploads -- uploads mcp

Experimental: AI labels #

Experimental and off by default. The uploads.sh team turns it on per workspace. To try it, open an issue on the GitHub repo.

When it’s on, uploads.sh labels each file shortly after it’s uploaded. The labels never replace a path or state you set, and a labeling error never fails the upload. A label it isn’t confident about is left off.

Key Values
ai.kind screenshot photo diagram document code ui other
ai.surface mobile desktop tablet unknown
ai.screen login signup settings profile dashboard analytics list detail form search modal onboarding empty error checkout other
ai.tags Older uploads only. The current labeler does not write tags.
ai.summary Older uploads only. The current labeler does not write a summary.
ai.classifier v5 on the default labeler. A resized image writes v6. A Luna try writes v4. No image writes skipped. A failed call writes failed.
ai.skip small, unsupported, resize_failed, or resize_timeout. Paired with ai.classifier=skipped. No model call, and no kind, surface, screen, or safety value.
ai.error timeout, provider_error, or no_answer. Paired with ai.classifier=failed. The model call ran and no label was stored.

The same call also records four internal flags. They are not chips. They do not appear on the public file page, in a gallery, in a feed, in an embed, or in a pull request comment. Two of them can hold a file for review. See Held uploads.

Key Values
ai.rating safe suggestive explicit
ai.content none sexual violence gore drugs hate self_harm other
ai.secret none api_key password private_key token connection_string other. A credential that grants access. A name or an email is not one. Its confidence answers “does this show at least one credential?”, so several credentials on one screen don’t lower it.
ai.pii none contact identity_document financial other. A signed-in name or email in a header stays none.
ai.rating.confidence high at 0.85 or above. medium from 0.70 up to 0.85. low from 0.50 up to 0.70.
ai.content.confidence Same buckets as ai.rating.confidence.
ai.secret.confidence Same buckets as ai.rating.confidence.
ai.pii.confidence Same buckets as ai.rating.confidence.

An answer under 0.50 is left off, and so is its confidence key. ai.secret and ai.pii stay separate, so a broad personal-data hit does not hide a real credential.

A stored flag stays until a later classification writes that same key. A confident none replaces the old value. An answer under the floor, a model timeout, or an error leaves the previous flag in place. A skip writes no safety value, so it does not replace or restore one. Re-uploading identical bytes does not clear a flag. New bytes start fresh, and a skip on those bytes leaves the flags clear.

The classifier version does not change for these flags. v5, v6, v4, and skipped still name how the image was prepared. A file labeled before the flags existed simply has none of these keys.

Held uploads #

A high-confidence ai.secret (any value other than none), or a high-confidence ai.pii of identity_document, financial, or other, holds the file for review when it was uploaded with uploads 0.59.0 or newer, the first release that can approve it. The server writes ai.hold=pending. An upload from an older CLI or another client is not held. A held file works like a staged one:

  • The managed pull request comment leaves it out. One line says how many files are held and how to approve them. The line never names a file.
  • Public feeds and galleries show it as withheld. Its page asks for sign-in, and shows a signed-in member why it’s held, with Approve. For an image on hosted storage, the page shows the held copy with the flagged area outlined. The outline is approximate, so check the whole image.
  • On hosted storage, its raw URL returns 404 while it is held. Approval brings the file back at the same URL. A file in your own bucket keeps serving from your domain.

Any workspace member can approve, from the file’s page or the CLI. Check each held file before you approve it. If it does show a credential, rotate the credential, then replace or delete the file. Approved one by mistake? A member can hold it again from the ⋯ menu on its file page.

Terminal window
uploads approve --pr 12 # list held files and why
uploads approve --pr 12 --all # approve every held file on the PR
uploads approve <key> # approve one file

A CLI older than uploads approve can call the API directly. Find the keys with GET /v1/workspaces/$WORKSPACE/files/held:

curl -X POST -H "Authorization: Bearer $UPLOADS_TOKEN" \
"https://api.uploads.sh/v1/workspaces/$WORKSPACE/files/approve?key=$KEY"

With the Slack app connected, the uploader (or, without one, the workspace admins) gets a Slack message for each new hold, with an Approve button. To get an email for each new hold instead, or as well, turn on An upload is held for review under Email notifications in your account settings. It’s off by default.

Workspace admins can turn labels, or just holds, off under Settings → AI labels.

Approval writes ai.hold=approved and re-syncs the comment. Re-uploading identical bytes keeps the approval. New bytes are classified again and can be held again. ai.pii=contact never holds a file, and the ai.rating and ai.content flags never hold one.

Labels aren’t in the upload’s response. Read them afterward, or search by them:

Terminal window
uploads meta get <key> # one file's labels, including internal flags
uploads find ai.screen=login # files with a label
uploads find ai.secret=api_key # internal flag; workspace members only

From an MCP client, use get_metadata and find_files. Clients can’t set or delete ai.* keys.