Raycast Extensions
Build new Raycast extensions, extend or fix existing ones, and review them against the Raycast Store guidelines — using current @raycast/api (v1.10x), @raycast/utils (v2.x), React 19, TypeScript, and ESLint 10 flat config.
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. |
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 — 22.22+ at the time of writing, so use Node 22 LTS 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.