Cloudflare R2
einverne/dotfiles
Guide for implementing Cloudflare R2 - S3-compatible object storage with zero egress fees.
A skill your agent uses when a PinMe Cloudflare Worker needs R2 object storage, including secure file or image upload, streaming download, metadata lookup, deletion, listing, Range requests, or…
$ npx skills add glitternetwork/pinme --skill pinme-r2 -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install glitternetwork/pinme pinme-r2 --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/glitternetwork/pinme.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/pinme-r2 .claude/skills/pinme-r2 && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "pinme-r2" agent skill from https://github.com/glitternetwork/pinme/tree/main/skills/pinme-r2 into .claude/skills/pinme-r2/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "pinme-r2", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/glitternetwork/pinme/tree/main/skills/pinme-r2Type this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add glitternetwork/pinme --skill pinme-r2 -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install glitternetwork/pinme pinme-r2 --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/glitternetwork/pinme.git skills-src && mkdir -p .agents/skills && cp -r skills-src/skills/pinme-r2 .agents/skills/pinme-r2 && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "pinme-r2" agent skill from https://github.com/glitternetwork/pinme/tree/main/skills/pinme-r2 into .agents/skills/pinme-r2/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "pinme-r2", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add glitternetwork/pinme --skill pinme-r2 -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install glitternetwork/pinme pinme-r2 --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/glitternetwork/pinme.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/skills/pinme-r2 .cursor/skills/pinme-r2 && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "pinme-r2" agent skill from https://github.com/glitternetwork/pinme/tree/main/skills/pinme-r2 into .cursor/skills/pinme-r2/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "pinme-r2", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/glitternetwork/pinme.git --path skills/pinme-r2--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add glitternetwork/pinme --skill pinme-r2 -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install glitternetwork/pinme pinme-r2 --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/glitternetwork/pinme.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/skills/pinme-r2 .gemini/skills/pinme-r2 && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "pinme-r2" agent skill from https://github.com/glitternetwork/pinme/tree/main/skills/pinme-r2 into .gemini/skills/pinme-r2/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "pinme-r2", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install glitternetwork/pinme pinme-r2Installs for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add glitternetwork/pinme --skill pinme-r2 -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/glitternetwork/pinme.git skills-src && mkdir -p .github/skills && cp -r skills-src/skills/pinme-r2 .github/skills/pinme-r2 && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "pinme-r2" agent skill from https://github.com/glitternetwork/pinme/tree/main/skills/pinme-r2 into .github/skills/pinme-r2/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "pinme-r2", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add glitternetwork/pinme --skill pinme-r2 -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install glitternetwork/pinme pinme-r2 --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/glitternetwork/pinme.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/skills/pinme-r2 .opencode/skills/pinme-r2 && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "pinme-r2" agent skill from https://github.com/glitternetwork/pinme/tree/main/skills/pinme-r2 into .opencode/skills/pinme-r2/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "pinme-r2", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
pinme-r2A skill your agent uses when a PinMe Cloudflare Worker needs R2 object storage, including secure file or image upload, streaming download, metadata lookup, deletion, listing, Range requests, or…
Pinme R2 is an agent skill from glitternetwork/pinme. Use when a PinMe Cloudflare Worker needs R2 object storage, including secure file or image upload, streaming download, metadata lookup, deletion, listing, Range requests, or R2+D1 coordination. Guides AI to use PinMe's automatically injected env.R2 binding without R2 credentials or manual Wrangler configuration.
Its SKILL.md is about 3.6k tokens, which your agent loads only when the skill is triggered. The skill folder holds 2 other files (for example `agents/openai.yaml`).
It sits in Backend & APIs, covering File uploads and storage. It works with Cloudflare Workers. The repository describes itself as: Deploy Your Frontend in a Single Command. Claude Code Skills supported. The licence is MIT.
Read from SKILL.md and the folder at commit 7822b05. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
No scripts in the folder and no shell commands in SKILL.md (its code samples are typescript).
From the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md.
From URLs in SKILL.md, links to its own repository left out.
Names these keys or tokens, usually read from environment variables:
API_KEYLLM_API_KEYUNIWEB_SECRETFrom names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Pinme R2 loads about 3.6k tokens when it runs. Until then it costs about 81 tokens; SKILL.md has 1,107 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from glitternetwork/pinme at commit 7822b05, republished under its MIT licence (© glitternetwork). 1,107 words, ~3,601 tokens.
.claude/skills/pinme-r2/SKILL.md (or your agent's skills folder). This skill also uses 1 other file; get the full folder from GitHub.Use the project-scoped R2 bucket that PinMe binds to every deployed Worker as env.R2. Do not create credentials, choose a bucket name, or edit generated Wrangler configuration.
PinMe rebuilds trusted Worker metadata on create, save, and update. Client metadata cannot replace the R2 binding.
| Binding | TypeScript type | Availability |
|---|---|---|
DB | D1Database | Always injected |
R2 | R2Bucket | Always injected; current project's bucket |
API_KEY | string | Always injected |
LLM_API_KEY | string | Always injected |
BASE_URL | string | Always injected |
WORKER_URL | string | Always injected |
PROJECT_NAME | string | Always injected |
Payment-specific bindings such as UNIWEB_SECRET are conditional and unrelated to R2 access.
Declare only the bindings used by the Worker module. R2 code normally starts with:
export interface Env {
R2: R2Bucket;
PROJECT_NAME: string;
WORKER_URL: string;
}When the same module coordinates file metadata in D1, also declare DB: D1Database as a required field.
Apply this sequence to every upload, download, metadata, delete, and list route:
authenticate request
→ authorize the project/user action
→ validate size and media policy
→ generate or normalize a scoped object key
→ call env.R2
→ return a sanitized responseUse the application's existing authentication. The examples below accept a trusted userId that the route must obtain from verified identity claims, never from an untrusted request body or query parameter.
Keep object keys server-controlled. Prefer opaque IDs under an owner prefix:
const FILE_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
function ownerPrefix(userId: string): string {
if (!userId) throw new Error('Authenticated user id is required');
return `users/${encodeURIComponent(userId)}/files/`;
}
function objectKey(userId: string, fileId: string): string {
if (!FILE_ID_RE.test(fileId)) throw new Error('Invalid file id');
return `${ownerPrefix(userId)}${fileId}`;
}Never accept a complete object key from the client. Reject empty identifiers, . or .. segments, backslashes, control characters, and any attempt to access another user's prefix.
Use small helpers and explicit business limits. Adapt the allowlist to the product rather than accepting every client-supplied media type.
const MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
const ALLOWED_CONTENT_TYPES = new Set([
'image/jpeg',
'image/png',
'image/webp',
'application/pdf',
]);
function json(data: unknown, status = 200): Response {
return Response.json(data, { status });
}
function safeDownloadName(value: string | null): string {
const cleaned = (value || 'download')
.replace(/[\r\n"\\]/g, '_')
.replace(/[\x00-\x1f\x7f]/g, '')
.trim();
return (cleaned || 'download').slice(0, 128);
}
function requestedFileId(request: Request): string | null {
const url = new URL(request.url);
const value = url.pathname.split('/').filter(Boolean).at(-1) || '';
return FILE_ID_RE.test(value) ? value : null;
}Client filenames and Content-Type are hints, not proof of content. For sensitive formats, inspect magic bytes or send the object through an asynchronous validation/scanning workflow before marking it ready.
Require authentication before calling this handler. Pass request.body directly to R2; do not call arrayBuffer(), text(), json(), formData(), or base64 conversion first.
async function handleUpload(
request: Request,
env: Env,
userId: string,
): Promise<Response> {
if (!request.body) return json({ error: 'File body is required' }, 400);
const lengthHeader = request.headers.get('content-length');
if (!lengthHeader) return json({ error: 'Content-Length is required' }, 411);
const declaredSize = Number(lengthHeader);
if (!Number.isSafeInteger(declaredSize) || declaredSize < 0) {
return json({ error: 'Invalid Content-Length' }, 400);
}
if (declaredSize > MAX_UPLOAD_BYTES) {
return json({ error: 'File is too large' }, 413);
}
const contentType = (request.headers.get('content-type') || '')
.split(';', 1)[0]
.trim()
.toLowerCase();
if (!ALLOWED_CONTENT_TYPES.has(contentType)) {
return json({ error: 'Unsupported media type' }, 400);
}
const fileId = crypto.randomUUID();
const key = objectKey(userId, fileId);
const filename = safeDownloadName(request.headers.get('x-file-name'));
const object = await env.R2.put(key, request.body, {
httpMetadata: {
contentType,
contentDisposition: `attachment; filename="${filename}"`,
},
customMetadata: { ownerId: userId },
});
if (object === null) return json({ error: 'Upload precondition failed' }, 412);
// Content-Length is only a precheck. Enforce the actual stored size too.
if (object.size > MAX_UPLOAD_BYTES) {
await env.R2.delete(key);
return json({ error: 'File is too large' }, 413);
}
return json({ id: fileId, size: object.size, etag: object.httpEtag }, 201);
}Do not return the bucket name or internal object-key layout. Return an opaque file ID that later routes resolve under the authenticated owner's prefix.
Validate a single Range header before passing it to R2. R2 may return null when the object does not exist, or metadata without a body when a conditional request fails.
function validRangeHeader(value: string | null): boolean {
if (!value) return true;
const match = /^bytes=(\d*)-(\d*)$/.exec(value);
return Boolean(match && (match[1] || match[2]));
}
async function handleDownload(
request: Request,
env: Env,
userId: string,
): Promise<Response> {
const fileId = requestedFileId(request);
if (!fileId) return json({ error: 'Invalid file id' }, 400);
if (!validRangeHeader(request.headers.get('range'))) {
return json({ error: 'Invalid Range header' }, 416);
}
const object = await env.R2.get(objectKey(userId, fileId), {
onlyIf: request.headers,
range: request.headers,
});
if (object === null) return json({ error: 'Not found' }, 404);
if (!('body' in object)) return new Response(null, { status: 412 });
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set('etag', object.httpEtag);
headers.set('accept-ranges', 'bytes');
if (object.range) {
const { offset, length } = object.range;
headers.set(
'content-range',
`bytes ${offset}-${offset + length - 1}/${object.size}`,
);
headers.set('content-length', String(length));
} else {
headers.set('content-length', String(object.size));
}
return new Response(object.body, {
status: object.range ? 206 : 200,
headers,
});
}For routes backed by D1 metadata, authorize the D1 row's owner before calling env.R2.get. Do not infer ownership only from a client-provided path.
async function handleHead(
request: Request,
env: Env,
userId: string,
): Promise<Response> {
const fileId = requestedFileId(request);
if (!fileId) return json({ error: 'Invalid file id' }, 400);
const object = await env.R2.head(objectKey(userId, fileId));
if (object === null) return new Response(null, { status: 404 });
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set('etag', object.httpEtag);
headers.set('content-length', String(object.size));
return new Response(null, { status: 200, headers });
}Use head() when only size, ETag, upload time, or metadata is needed. Do not download the body to answer metadata requests.
async function handleDelete(
request: Request,
env: Env,
userId: string,
): Promise<Response> {
const fileId = requestedFileId(request);
if (!fileId) return json({ error: 'Invalid file id' }, 400);
const key = objectKey(userId, fileId);
const object = await env.R2.head(key);
if (object === null) return json({ error: 'Not found' }, 404);
await env.R2.delete(key);
return new Response(null, { status: 204 });
}R2 can delete up to 1000 keys in one delete([...keys]) call. Batch deletion must still derive and authorize every key server-side.
Never list the whole bucket for an end-user request. Derive the prefix from verified identity and treat the cursor as opaque.
async function handleList(
request: Request,
env: Env,
userId: string,
): Promise<Response> {
const cursor = new URL(request.url).searchParams.get('cursor');
if (cursor && cursor.length > 2048) {
return json({ error: 'Invalid cursor' }, 400);
}
const page = await env.R2.list({
prefix: ownerPrefix(userId),
cursor: cursor || undefined,
limit: 100,
include: ['httpMetadata', 'customMetadata'],
});
return json({
objects: page.objects.map((object) => ({
id: object.key.slice(ownerPrefix(userId).length),
size: object.size,
uploaded: object.uploaded.toISOString(),
etag: object.httpEtag,
contentType: object.httpMetadata?.contentType,
})),
nextCursor: page.truncated ? page.cursor : null,
});
}An R2 list call returns at most 1000 entries and may return fewer than the requested limit when metadata is included. Continue only when page.truncated is true; never use objects.length === limit as the pagination condition.
Authenticate once in the router, derive a trusted userId, then pass it to the handlers. Return an Allow header for unsupported methods.
| Status | Meaning |
|---|---|
| 400 | Invalid file ID, body, cursor, or media type |
| 401 | Missing or invalid authentication |
| 403 | Authenticated but not allowed to access the object |
| 404 | Object or owned metadata record not found |
| 411 | A capped upload route requires Content-Length but it is absent |
| 412 | Conditional R2 operation failed |
| 413 | Business or platform upload limit exceeded |
| 416 | Invalid or unsatisfiable Range request |
| 500 | Sanitized internal storage failure |
Catch storage failures at the route boundary, log only non-sensitive context, and return a generic error. Never return a raw provider error, bucket name, credential, or internal object key.
Translate a valid-but-unsatisfiable R2 Range failure to 416 without returning the provider error text.
R2 and D1 do not share a transaction. Use an explicit state transition when business metadata is required:
insert D1 row with status=pending
→ stream body to R2
→ update D1 row to status=readyUse request.body → env.R2.put for small and medium uploads. Streaming avoids Worker memory amplification but does not bypass the Cloudflare request-body limit for the account plan.
Use multipart only when the object exceeds that request limit or resumability is an explicit product requirement. A multipart API must:
Do not generate a public multipart controller by default. Multipart state and security are substantially more complex than a single streaming upload.
backend/wrangler.toml to add an R2 binding.R2Bucket mock.pinme update-worker or pinme save.| Do not | Use instead |
|---|---|
| Expose an unauthenticated upload route | Authenticate and authorize before every mutation |
| Accept a complete object key from the client | Generate an opaque ID under a server-derived owner prefix |
| Trust a user ID from JSON or query parameters | Derive identity from verified claims |
| Read a large body into an ArrayBuffer or base64 string | Stream request.body directly into env.R2.put |
| Store file bodies or base64 in D1 | Store bodies in R2 and searchable metadata in D1 |
| List the whole bucket | Restrict with an owner prefix and paginate |
| Stop pagination based on returned object count | Check page.truncated and return page.cursor |
| Drop response metadata | Apply writeHttpMetadata, httpEtag, length, and Range headers |
Persist with fs or local directories | Use the injected R2 binding |
| Add R2 keys or secrets to source/config | Use env.R2; PinMe owns the binding |
| Edit generated Wrangler binding configuration | Deploy through pinme save or pinme update-worker |
© glitternetwork, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 1 other file in skills/pinme-r2 of glitternetwork/pinme.
Open the folder on GitHubat commit 7822b05
Pinme R2 next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Pinme R2 this skillglitternetwork/pinme | 3.7k | — | ~3.6k | Automated safety check: Pass | MIT | |
| Cloudflare R2einverne/dotfiles | 121 | — | ~2.8k | Automated safety check: Pass | GPL-3.0 | |
| Cloudflare R2sickn33/agentic-awesome-skills | 47k | 2 repos | ~2.5k | Automated safety check: Pass | MIT | |
| Cloudflare 3sundial-org/awesome-openclaw-skills | 663 | — | ~1.5k | Automated safety check: Pass | None | |
| Stripe Projectsfossasia/eventyay | 1.7k | 5 repos | ~2k | Automated safety check: Notes | Apache-2.0 | |
| FoundatioFoundatioFx/Foundatio | 2.1k | — | ~3.9k | Automated safety check: Pass | Apache-2.0 |
einverne/dotfiles
Guide for implementing Cloudflare R2 - S3-compatible object storage with zero egress fees.
sickn33/agentic-awesome-skills
Manage Cloudflare R2 buckets, lifecycle, and signed URLs. An agent skill from sickn33/agentic-awesome-skills.
sundial-org/awesome-openclaw-skills
Manage Cloudflare Workers, KV, D1, R2, and secrets using the Wrangler CLI.
fossasia/eventyay
A skill your agent uses when the user wants to provision infrastructure or third-party services using Stripe Projects.
FoundatioFx/Foundatio
A skill your agent uses when working with Foundatio infrastructure abstractions for .NET -- caching, queuing, messaging, file storage, distributed locking, or background jobs.
duckdb/duckdb-skills
Answer questions about spatial data using DuckDB. An agent skill from duckdb/duckdb-skills.
glitternetwork/pinme
A skill your agent uses when the user mentions "pinme", or needs to upload files, store to IPFS, create/publish/deploy websites or full-stack services (including frontend pages, backend APIs…
glitternetwork/pinme
A skill your agent uses when a PinMe project (Worker TypeScript) needs to integrate user authentication — creating email/password users, verifying idtokens, querying user info, or listing users via…
glitternetwork/pinme
A skill your agent uses when a PinMe project (Worker TypeScript) needs to integrate email sending (sendemail).
glitternetwork/pinme
A skill your agent uses when a PinMe project (Worker TypeScript) needs to call OpenRouter-backed LLM APIs, including models, chat/completions, streaming, or OpenRouter web search.
glitternetwork/pinme
A skill your agent uses when the user wants to share, publish, or upload a static result through PinMe, especially by generating a static HTML share page for a PinMe project link, deployed…
glitternetwork/pinme
A skill your agent uses when generating, modifying, or reviewing PinMe Worker (Cloudflare Worker TypeScript) code that accepts payments through UniwebPay — payment links, products/prices, checkout…
Works with
Categories
A skill your agent uses when a PinMe Cloudflare Worker needs R2 object storage, including secure file or image upload, streaming download, metadata lookup, deletion, listing, Range requests, or…. Pinme R2 is an agent skill from glitternetwork/pinme. Use when a PinMe Cloudflare Worker needs R2 object storage, including secure file or image upload, streaming download, metadata lookup, deletion, listing, Range requests, or R2+D1 coordination.
Pinme R2 fits situations like: A PinMe Cloudflare Worker needs R2 object storage; including secure file; streaming download; metadata lookup.
Run `npx skills add glitternetwork/pinme --skill pinme-r2 -a claude-code`. Or copy the skill folder (skills/pinme-r2 in glitternetwork/pinme) into .claude/skills/pinme-r2 in your project. Claude Code loads it when a task matches its description.
Run `npx skills add glitternetwork/pinme --skill pinme-r2 -a codex`. Or copy the skill folder (skills/pinme-r2 in glitternetwork/pinme) into .agents/skills/pinme-r2 in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add glitternetwork/pinme --skill pinme-r2 -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/pinme-r2, .gemini/skills/pinme-r2, .github/skills/pinme-r2 and .opencode/skills/pinme-r2 in your project.
Going by SKILL.md and its folder, Pinme R2 needs credentials named API_KEY, LLM_API_KEY and UNIWEB_SECRET. Our summary lists: A credential in API_KEY; A credential in LLM_API_KEY.
SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Pinme R2 is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 3.6k tokens (SKILL.md is roughly 14k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Pinme R2: Cloudflare R2 (einverne/dotfiles, 121 stars), Cloudflare R2 (sickn33/agentic-awesome-skills, 47k stars), Cloudflare 3 (sundial-org/awesome-openclaw-skills, 663 stars) and Stripe Projects (fossasia/eventyay, 1.7k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
glitternetwork (a GitHub organization) maintains it in glitternetwork/pinme, which has 3,748 GitHub stars. The repository holds 7 skills in this directory. The repository was last updated on September 12, 2026.
Source: glitternetwork/pinme on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.