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.md is a thin stub; the conventions live in the source and the docs.
- Use the
@raycast/utils hooks for all data fetching - never hand-roll useEffect + fetch + useState. The hooks give isLoading, 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.Push for navigation (not a custom stack), and showFailureToast for 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 via showHUD), or menu-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. See references/ai-extensions.md.
- Config: API tokens (→
password preference or OAuth), tunables (→ textfield/checkbox/dropdown preferences). 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
platforms and which APIs you can use - runAppleScript is 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 in assets/), author (Raycast handle), categories (Title Case enum), license: "MIT", commands[]. Add "$schema": "https://www.raycast.com/schemas/extension.json" for editor validation.
- Each command
name maps to src/<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-exported async function. Menu-bar = a component returning <MenuBarExtra>.
- Data layer: reach for the right hook (see table in intake). Wire its
isLoading into the top-level view and its pagination into <List pagination>. Put changing inputs in the hook's args array - that array is the cache key and the deps.
- Writes/mutations: use the hook's
mutate with an optimisticUpdate and an Animated→Success/Failure toast, inside try/catch.
- Errors:
import { showFailureToast } from "@raycast/utils" in every catch. For no-view commands use showHUD (it survives the window closing); showToast disappears with the window.
- Preferences:
getPreferenceValues<Preferences>() (types auto-generated into raycast-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.md and apply its review checklist top to bottom.
- Run
ray lint and ray 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;
license not MIT; verb-first or generic title; secrets in source / Keychain access; hand-rolled fetch instead of the hooks; missing or misformatted CHANGELOG.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; a pnpm-lock.yaml/yarn.lock in 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 publish is a built-in pnpm command that publishes to the npm registry - it does not run Raycast's publish script. If using pnpm locally, invoke pnpm run publish (explicit run) or pnpm dlx @raycast/api@latest publish. The prepublishOnly: "... && exit 1" guard in the manifest exists precisely to stop an accidental npm publish to 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)
onSearchTextChange silently disables built-in filtering. Once you handle search text yourself, the list stops filtering - either implement server-side search or re-enable filtering. Add throttle if onSearchTextChange does network calls, or you fire a request per keystroke.
- Action order is UX. The first
Action is the primary (Enter) action. Put a destructive action first and Enter deletes. Mark destructive actions with style: Action.Style.Destructive and order deliberately. In Form, primary submit is ⌘↵ (Enter inserts a newline), so Action.SubmitForm should be first.
LocalStorage is async, Cache is sync and string-only. Prefer the useLocalStorage / useCachedState hooks over touching them directly; never store secrets in Cache.
- AI tool
Input JSDoc is the model's prompt. The tool's argument schema is derived from a type named exactly Input; the JSDoc on each field is what the model reads to choose arguments. Thin descriptions cause wrong calls. Every mutating tool needs an exported confirmation.
{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.
1---2name: raycast-extensions3description: Build, maintain, and review Raycast extensions (the @raycast/api / TypeScript apps published to github.com/raycast/extensions). Use whenever the user mentions a Raycast extension, command, or tool; the @raycast/api or @raycast/utils packages; List/Detail/Form/ActionPanel/MenuBarExtra UI; useCachedPromise/useFetch/useForm hooks; getPreferenceValues; the ray CLI (ray develop/build/lint/evals); an AI extension or AI tools; preparing/publishing an extension to the Raycast Store; or reviewing an existing extension against store guidelines. Also use for "scaffold a Raycast command", "my Raycast extension errors", "fix the manifest", "add a preference", "add an OAuth login", or porting a CLI/app idea to Raycast - even if they don't say "@raycast/api" explicitly.4---5
6# Raycast Extensions
7
8Build 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).
9
10## Operating rules
11
12- **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.md` is a thin stub; the conventions live in the source and the docs.
13- **Use the `@raycast/utils` hooks for all data fetching** - never hand-roll `useEffect` + `fetch` + `useState`. The hooks give `isLoading`, error toasts, caching, abort, `revalidate`, `mutate`, and pagination for free, and reviewers reject the hand-rolled version.
14- **Don't reinvent the framework**: use the preferences API for config (not extra commands), `useNavigation`/`Action.Push` for navigation (not a custom stack), and `showFailureToast` for errors (not bespoke failure toasts). These are explicit store-review rules.
15- **Secrets never go in source**: use `password`-type preferences or OAuth (`OAuthService`). Hardcoded keys or Keychain access are auto-rejected.
16- 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.
17
18## First, decide which mode you're in
19
201. **New extension** - scaffold from a template, then build commands. Start at *Build workflow*.
212. **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.
223. **Review an existing extension** (for store-readiness or a PR) - go to *Review workflow* and apply `references/store-review.md`.
23
24If the user's intent is ambiguous (e.g. "help with my Raycast extension"), ask which of these it is before diving in.
25
26## Reference map - read the relevant file, don't load everything
27
28The SKILL.md is the spine. Pull in a reference when the task touches it:
29
30| Read this | When |
31|---|---|
32| `references/manifest.md` | Writing/auditing `package.json`: commands, modes, preferences, arguments, tools, categories, icons, folder layout. Has a full annotated example. |
33| `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). |
34| `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. |
35| `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. |
36
37`evals/` holds this skill's own trigger/behaviour test prompts and is intentionally unrouted (maintainer-facing, not read during a task).
38
39## Quick intake (new extension)
40
41Before scaffolding, settle:
42
43- **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 via `showHUD`), or `menu-bar` (`MenuBarExtra`) command?
44- **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. See `references/ai-extensions.md`.
45- **Config**: API tokens (→ `password` preference or OAuth), tunables (→ `textfield`/`checkbox`/`dropdown` preferences). Per-run input that belongs in root search → `arguments` (max 3).
46- **Data source**: REST (`useFetch`), arbitrary async/SDK (`useCachedPromise`), local CLI (`useExec`), local SQLite (`useSQL`).
47- **Platforms**: macOS only, or macOS + Windows? (Affects `platforms` and which APIs you can use - `runAppleScript` is macOS-only.)
48
49## Build workflow
50
51### 1) Scaffold
52
53Preferred: 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`.
54
55Requires 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.
56
57### 2) Manifest (`package.json`)
58
59This is where most mistakes live. Get the structure right from `references/manifest.md`. The essentials:
60
61- Required root: `name` (kebab-case slug), `title` (Title Case display), `description`, `icon` (512×512 PNG in `assets/`), `author` (Raycast handle), `categories` (Title Case enum), `license: "MIT"`, `commands[]`. Add `"$schema": "https://www.raycast.com/schemas/extension.json"` for editor validation.
62- Each command `name` maps to `src/<name>.ts(x)` exactly, or the build fails.
63- 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.
64
65### 3) Command code
66
67- **View command** = a default-exported React component that returns `<List>`/`<Detail>`/`<Form>`/`<Grid>`. **No-view** = a default-exported `async function`. **Menu-bar** = a component returning `<MenuBarExtra>`.
68- **Data layer**: reach for the right hook (see table in intake). Wire its `isLoading` into the top-level view and its `pagination` into `<List pagination>`. Put changing inputs in the hook's `args` array - that array is the cache key *and* the deps.
69- **Writes/mutations**: use the hook's `mutate` with an `optimisticUpdate` and an Animated→Success/Failure toast, inside try/catch.
70- **Errors**: `import { showFailureToast } from "@raycast/utils"` in every catch. For no-view commands use `showHUD` (it survives the window closing); `showToast` disappears with the window.
71- **Preferences**: `getPreferenceValues<Preferences>()` (types auto-generated into `raycast-env.d.ts` - never edit that file by hand).
72- Full primitives and hook signatures: `references/api-and-hooks.md`.
73
74### 4) Architecture (only as it grows)
75
76A 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:
77
78```text
79src/
80 <command>.tsx # one per manifest command; default export
81 api/ # client singleton + raw data fns (getXClient() that throws if uninit)
82 hooks/ # use*.ts wrappers over useCachedPromise that call getXClient()
83 helpers/ # pure fns: errors.ts (getErrorMessage), icons.ts, dates.ts
84 components/ # shared List.Item / Form / actions pieces
85 tools/ # one file per AI tool (if any)
86 types.ts
87```
88
89The 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).
90
91### 5) Verify before declaring done
92
93Run, in order:
94
95```bash
96npm run lint # ray lint (add --fix to autofix)
97npm run build # ray build -e dist — does a full TypeScript type-check the dev build skips
98npm run dev # ray develop — imports into Raycast with hot reload; actually exercise the command
99```
100
101`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.
102
103## Review / maintain workflow
104
105When reviewing an existing extension (store-readiness, a PR, or "why is this rejected?"):
106
1071. Read `references/store-review.md` and apply its **review checklist** top to bottom.
1082. Run `ray lint` and `ray build -e dist` - these catch a large fraction of review blockers mechanically.
1093. Check the high-frequency rejection causes first: default/dark-mode-broken icon; wrong screenshot size; `license` not MIT; verb-first or generic title; secrets in source / Keychain access; hand-rolled fetch instead of the hooks; missing or misformatted `CHANGELOG.md`.
1104. 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.
111
112## Publishing
113
114Canonical 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:
115
116- **Store PRs require npm + a committed `package-lock.json`.** The CI uses npm; a `pnpm-lock.yaml`/`yarn.lock` in 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.
117- **`pnpm publish` is a built-in pnpm command** that publishes to the npm registry - it does *not* run Raycast's `publish` script. If using pnpm locally, invoke `pnpm run publish` (explicit `run`) or `pnpm dlx @raycast/api@latest publish`. The `prepublishOnly: "... && exit 1"` guard in the manifest exists precisely to stop an accidental `npm publish` to npmjs.
118
119For org/private extensions, `npm run publish` goes to the org's private store (no public PR).
120
121## High-value pitfalls (the why behind the rules)
122
123- **`onSearchTextChange` silently disables built-in `filtering`.** Once you handle search text yourself, the list stops filtering - either implement server-side search or re-enable `filtering`. Add `throttle` if `onSearchTextChange` does network calls, or you fire a request per keystroke.
124- **Action order is UX.** The first `Action` is the primary (Enter) action. Put a destructive action first and Enter deletes. Mark destructive actions with `style: Action.Style.Destructive` and order deliberately. In `Form`, primary submit is ⌘↵ (Enter inserts a newline), so `Action.SubmitForm` should be first.
125- **`LocalStorage` is async, `Cache` is sync and string-only.** Prefer the `useLocalStorage` / `useCachedState` hooks over touching them directly; never store secrets in `Cache`.
126- **AI tool `Input` JSDoc is the model's prompt.** The tool's argument schema is derived from a type named exactly `Input`; the JSDoc on each field is what the model reads to choose arguments. Thin descriptions cause wrong calls. Every mutating tool needs an exported `confirmation`.
127- **`{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.
128
129---
130
131When 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.