Raycast Extensions
Build new Raycast extensions, extend or fix existing ones, and review them against the Raycast Store guidelines — using @raycast/api, @raycast/utils (v2), React 19, TypeScript, and ESLint 10 flat config. Check the extension's package.json for the exact @raycast/api version (pin the latest for Store submissions).
Operating rules
- Prefer official docs when uncertain: developers.raycast.com (API + guides) and manual.raycast.com/extensions-guidelines (the real review bar). Imitate real extensions in
github.com/raycast/extensions—CONTRIBUTING.mdis a thin stub; the conventions live in the source and the docs. - Use the
@raycast/utilshooks for all data fetching — never hand-rolluseEffect+fetch+useState. The hooks giveisLoading, error toasts, caching, abort,revalidate,mutate, and pagination for free, and reviewers reject the hand-rolled version. - Don't reinvent the framework: use the preferences API for config (not extra commands),
useNavigation/Action.Pushfor navigation (not a custom stack), andshowFailureToastfor errors (not bespoke failure toasts). These are explicit store-review rules. - Secrets never go in source: use
password-type preferences or OAuth (OAuthService). Hardcoded keys or Keychain access are auto-rejected. - This skill covers the framework, not any one service's API. When wiring a third-party API, confirm its current endpoints/SDK rather than guessing.
First, decide which mode you're in
- New extension — scaffold from a template, then build commands. Start at Build workflow.
- Extend / fix an existing one — read the extension's existing structure first and match its conventions (its client singleton, its hooks/, its error helper). Add a CHANGELOG entry. Don't impose a different architecture.
- Review an existing extension (for store-readiness or a PR) — go to Review workflow and apply
references/store-review.md.
If the user's intent is ambiguous (e.g. "help with my Raycast extension"), ask which of these it is before diving in.
Reference map — read the relevant file, don't load everything
The SKILL.md is the spine. Pull in a reference when the task touches it:
| Read this | When |
|---|---|
| references/manifest.md | Writing/auditing package.json: commands, modes, preferences, arguments, tools, categories, icons, folder layout. Has a full annotated example. |
| references/api-and-hooks.md | Writing command code: List/Detail/Form/Grid, ActionPanel/Action, navigation, feedback (toast/HUD/alert), storage/cache/clipboard, MenuBarExtra, and every @raycast/utils hook (useCachedPromise, useFetch, useForm, useExec, useSQL, OAuth). |
| references/ai-extensions.md | Anything with AI tools: src/tools/, the Input type + JSDoc, confirmation, the ai config, evals (ray evals), or AI.ask inside a command. |
| references/store-review.md | Preparing for the Store, reviewing an extension/PR, or any "is this store-ready?" question. Contains the full review checklist + asset specs. |
evals/ holds this skill's own trigger/behaviour test prompts and is intentionally unrouted (maintainer-facing, not read during a task).
Quick intake (new extension)
Before scaffolding, settle:
- What it does and the commands it needs — each command is one entry point. For each: a
view(renders UI),no-view(fire-and-forget action, feedback viashowHUD), ormenu-bar(MenuBarExtra) command? - Does the AI need to call it? If a discrete data fetch/mutation would be useful to Raycast AI, expose it as a tool (
src/tools/) in addition to or instead of a command. Seereferences/ai-extensions.md. - Config: API tokens (→
passwordpreference or OAuth), tunables (→textfield/checkbox/dropdownpreferences). Per-run input that belongs in root search →arguments(max 3). - Data source: REST (
useFetch), arbitrary async/SDK (useCachedPromise), local CLI (useExec), local SQLite (useSQL). - Platforms: macOS only, or macOS + Windows? (Affects
platformsand which APIs you can use —runAppleScriptis macOS-only.)
Build workflow
1) Scaffold
Preferred: Raycast's in-app "Create Extension" command (pick a template: Detail / List / Grid / Form / Menu Bar; it wires up eslint + tsconfig). CLI alternative: npm init raycast-extension -t <template>. Then npm install.
Requires Raycast 1.26+, a current Node (the @raycast/api engines field tracks the latest LTS — check it and use that Node or newer), and being signed into Raycast.
2) Manifest (package.json)
This is where most mistakes live. Get the structure right from references/manifest.md. The essentials:
- Required root:
name(kebab-case slug),title(Title Case display),description,icon(512×512 PNG inassets/),author(Raycast handle),categories(Title Case enum),license: "MIT",commands[]. Add"$schema": "https://www.raycast.com/schemas/extension.json"for editor validation. - Each command
namemaps tosrc/<name>.ts(x)exactly, or the build fails. - Folder layout matters and trips people up:
assets/= bundled runtime icons;metadata/= Store screenshots (2000×1250 PNG);media/= README images only. Don't conflate them.
3) Command code
- View command = a default-exported React component that returns
<List>/<Detail>/<Form>/<Grid>. No-view = a default-exportedasync function. Menu-bar = a component returning<MenuBarExtra>. - Data layer: reach for the right hook (see table in intake). Wire its
isLoadinginto the top-level view and itspaginationinto<List pagination>. Put changing inputs in the hook'sargsarray — that array is the cache key and the deps. - Writes/mutations: use the hook's
mutatewith anoptimisticUpdateand an Animated→Success/Failure toast, inside try/catch. - Errors:
import { showFailureToast } from "@raycast/utils"in every catch. For no-view commands useshowHUD(it survives the window closing);showToastdisappears with the window. - Preferences:
getPreferenceValues<Preferences>()(types auto-generated intoraycast-env.d.ts— never edit that file by hand). - Full primitives and hook signatures:
references/api-and-hooks.md.
4) Architecture (only as it grows)
A single-command extension stays flat: src/index.tsx, maybe api.ts, types.ts, preferences.ts. Once multiple commands share logic, adopt the convention the big extensions (github, linear) converge on:
src/
<command>.tsx # one per manifest command; default export
api/ # client singleton + raw data fns (getXClient() that throws if uninit)
hooks/ # use*.ts wrappers over useCachedPromise that call getXClient()
helpers/ # pure fns: errors.ts (getErrorMessage), icons.ts, dates.ts
components/ # shared List.Item / Form / actions pieces
tools/ # one file per AI tool (if any)
types.ts
The dominant auth pattern: an OAuthService + a module-level client in api/, wrapped with withAccessToken(service) (from @raycast/utils) on each command's default export and on each tool. Only call getXClient() inside a wrapped component/hook/tool — at module top level it throws before auth runs. See references/api-and-hooks.md (OAuth section).
5) Verify before declaring done
Run, in order:
npm run lint # ray lint (add --fix to autofix)
npm run build # ray build -e dist — does a full TypeScript type-check the dev build skips
npm run dev # ray develop — imports into Raycast with hot reload; actually exercise the command
ray build catches type errors that ray develop lets slide, so always run it before you consider a change finished. Then open the command in Raycast and verify the real behaviour — loading state, empty state, the happy path, and at least one error path.
Review / maintain workflow
When reviewing an existing extension (store-readiness, a PR, or "why is this rejected?"):
- Read
references/store-review.mdand apply its review checklist top to bottom. - Run
ray lintandray build -e dist— these catch a large fraction of review blockers mechanically. - Check the high-frequency rejection causes first: default/dark-mode-broken icon; wrong screenshot size;
licensenot MIT; verb-first or generic title; secrets in source / Keychain access; hand-rolled fetch instead of the hooks; missing or misformattedCHANGELOG.md. - For a fix to someone else's extension: add yourself to
contributors, add a CHANGELOG entry (## [Title] - {PR_MERGE_DATE}at the top), and keep the diff minimal and on-convention.
Publishing
Canonical commands are the ray-backed package scripts: npm run build, then npm run publish (= npx @raycast/api@latest publish), which opens a PR against raycast/extensions. Two things to flag:
- Store PRs require npm + a committed
package-lock.json. The CI uses npm; apnpm-lock.yaml/yarn.lockin the PR gets it rejected. So even if local dev uses pnpm, generate the lockfile with npm for a Store-bound submission, and surface this to the user before committing a lockfile. pnpm publishis a built-in pnpm command that publishes to the npm registry — it does not run Raycast'spublishscript. If using pnpm locally, invokepnpm run publish(explicitrun) orpnpm dlx @raycast/api@latest publish. TheprepublishOnly: "... && exit 1"guard in the manifest exists precisely to stop an accidentalnpm publishto npmjs.
For org/private extensions, npm run publish goes to the org's private store (no public PR).
High-value pitfalls (the why behind the rules)
onSearchTextChangesilently disables built-infiltering. Once you handle search text yourself, the list stops filtering — either implement server-side search or re-enablefiltering. AddthrottleifonSearchTextChangedoes network calls, or you fire a request per keystroke.- Action order is UX. The first
Actionis the primary (Enter) action. Put a destructive action first and Enter deletes. Mark destructive actions withstyle: Action.Style.Destructiveand order deliberately. InForm, primary submit is ⌘↵ (Enter inserts a newline), soAction.SubmitFormshould be first. LocalStorageis async,Cacheis sync and string-only. Prefer theuseLocalStorage/useCachedStatehooks over touching them directly; never store secrets inCache.- AI tool
InputJSDoc is the model's prompt. The tool's argument schema is derived from a type named exactlyInput; the JSDoc on each field is what the model reads to choose arguments. Thin descriptions cause wrong calls. Every mutating tool needs an exportedconfirmation. {PR_MERGE_DATE}is a literal placeholder, not a date you fill in — Raycast substitutes the merge date. Hardcoding a date is a common review nit.
When you finish a build or a review, state plainly what you verified (lint/build clean, command exercised in Raycast, which paths you checked) rather than asserting it works untested.