# Building Raycast Extensions (v2-era) — Personal Guide

Last verified against Raycast: **macOS v2 public beta (May 2026)**, `@raycast/api` 1.103.x.

The Raycast v2 beta has no official extension docs yet — the developer changelog still ends at 1.103.0 (Sept 2025). So every time you start a new extension, **refresh your context first** by pulling the live docs below. The API itself hasn't been rewritten for v2; the changes are app-side (where the extension is shown, dual-install behavior, etc.).

---

## 0. Pre-flight: fetch fresh docs every time

Open these and skim for changes since this guide was written. If anything contradicts this file, the live docs win.

- Manifest reference (every `package.json` field): https://developers.raycast.com/information/manifest
- CLI commands (`ray develop`, `ray build`, etc.): https://developers.raycast.com/information/developer-tools/cli
- API changelog (look for entries newer than 1.103.0): https://developers.raycast.com/misc/changelog
- Store checklist (titles, platforms, icons): https://developers.raycast.com/basics/prepare-an-extension-for-store
- Best practices: https://developers.raycast.com/information/best-practices
- v2 manual page (what's different in the app): https://manual.raycast.com/new-in-v2

When you ask Claude / an AI assistant to help, paste at least the first two URLs so it grounds on current info.

---

## 1. Bootstrap a new extension

Use Raycast's own "Create Extension" command — don't hand-roll the scaffolding. It picks the right templates and the latest API.

1. Open Raycast → run **Create Extension**.
2. Pick template: `Script Command` for non-UI background commands, `Detail`/`List`/`Grid`/`Form` for UI.
3. Pick the parent folder. **Avoid `~/Documents`** — iCloud Drive can evict files inside `node_modules` and break the extension at random. Use something like `~/Builds/raycast/` or `~/dev/raycast/`.
4. After creation, in Terminal:

```bash
cd <extension-folder>
npm install @raycast/api@latest @raycast/utils@latest   # pin to latest
npm install
npm run dev
```

`@raycast/api@latest` is mandatory in v2 — older versions can target the wrong Raycast app on dual installs.

---

## 2. `package.json` essentials

Fields that matter and how to fill them:

```json
{
  "$schema": "https://www.raycast.com/schemas/extension.json",
  "name": "my-extension",
  "title": "My Extension",
  "description": "One-sentence purpose, sentence case, ends with a period.",
  "icon": "extension-icon.png",
  "author": "<your-raycast-username>",
  "platforms": ["macOS"],
  "categories": ["Developer Tools"],
  "license": "MIT",
  "commands": [
    {
      "name": "do-thing",
      "title": "Do Thing",
      "subtitle": "My Extension",
      "description": "What this specific command does.",
      "mode": "no-view"
    }
  ]
}
```

Rules of thumb:

- **`name` is the file slug.** It must match `src/<name>.ts(x)`. If they diverge, Raycast loads nothing.
- **`title` is what users see and search.** Use Title Case (Apple Style Guide), never kebab-case. `svg-path-extractor` is wrong; `Extract SVG Paths` is right. Bad titles make commands invisible in root search.
- **`subtitle` is the extension name**, so users searching by extension still find the command.
- **`platforms`** must match what your code actually uses. If you call `pbcopy`, `osascript`, or any macOS-only API, list only `"macOS"`. If you list `"Windows"` without supporting it, `ray build` may refuse to write the executable.
- **`mode`** is one of `"no-view"` (background script), `"view"` (renders React), or `"menu-bar"`.
- **`description`** on the command and the extension must be filled — empty strings will fail store validation and can confuse root search.

---

## 3. Source layout

```
extension-folder/
├── package.json
├── tsconfig.json
├── assets/
│   └── extension-icon.png     # 512×512, looks good in light + dark
└── src/
    ├── do-thing.ts            # matches commands[].name
    └── other-command.tsx      # one file per command
```

Each command file exports a `default` async function:

```ts
// src/do-thing.ts
import { Clipboard, showHUD } from "@raycast/api";

export default async function Command() {
  const text = await Clipboard.readText();
  if (!text) return showHUD("Clipboard empty");
  await Clipboard.copy(text.toUpperCase());
  await showHUD("Copied");
}
```

For `view` mode, default-export a React component instead.

---

## 4. Development workflow

There are three relevant npm scripts. They do *different* things — don't confuse them.

| Script           | What it does                                                                  | When to use                                            |
| ---------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------ |
| `npm run dev`    | `ray develop` — imports the extension into Raycast, watches files, hot-reloads | First-time install, and whenever you change the source |
| `npm run build`  | `ray build` — production bundle for the Store / CI. Does **not** import.       | Final validation before submitting to Store            |
| `npm run lint`   | ESLint                                                                        | Before committing                                      |

**Permanent install (no terminal needed afterwards):**

1. `npm run dev` — wait for `ready - built extension successfully`.
2. Open Raycast, run the command, confirm it works.
3. `⌃ C` to stop the dev process.
4. Run the command again from Raycast — it should still be there.

If step 4 fails ("Missing executable" or command vanishes), see Pitfalls below.

---

## 5. Pitfalls (from real debugging sessions)

**"Missing executable" error**

The compiled JS isn't where Raycast expects. Causes, in order of likelihood:

1. `ray develop` was never run, or it crashed silently. Re-run `npm run dev` and watch for errors.
2. Project is in iCloud-synced `~/Documents` and files got evicted. Move it to `~/Builds/` or similar.
3. The `platforms` field lists a platform your code can't actually run on — validation fails, executable isn't written.
4. `node_modules` is from another machine / arch. `rm -rf node_modules && npm install`.

**Command works during `npm run dev` but vanishes after `⌃ C`**

You probably have **two Raycast apps installed** (v1 and v2 during the beta). `ray develop` targets the one that's running, but your root search is in the other. Quit both, launch only the one you want, then `npm run dev` again. Also run `npm install @raycast/api@latest` to make sure the CLI honors v2.

**Command doesn't appear in root search even though it's listed in Extensions settings**

The `title` is kebab-case or all lowercase. Rename to proper Title Case and rerun `npm run dev`.

**Build succeeds but Raycast doesn't pick it up**

`npm run build` does *not* register the extension. Only `npm run dev` does. Run `npm run dev` once first, then `build` is fine for subsequent rebuilds.

**TypeScript errors after upgrading `@raycast/api`**

The most common one: `List.onSelectionChange` now correctly types as `string | null` instead of `string | undefined`. Update your handlers.

---

## 6. When you're ready to share

Three distribution paths, in order of effort:

- **Personal use** — just `npm run dev` once, ⌃C. Lives in your local Raycast.
- **Team private store** — `npm run publish` (requires Raycast for Teams membership). Visible to your org only.
- **Public Raycast Store** — submit a PR to https://github.com/raycast/extensions. The store guidelines page above is the checklist.

---

## 7. Cheat sheet — common API imports

```ts
import {
  Clipboard,            // readText, copy, paste, clear
  showHUD,              // brief toast
  showToast, Toast,     // longer toast with style
  Action, ActionPanel,  // user actions on results
  List, Grid, Detail, Form,  // UI components
  Icon, Color,          // built-in icons + theme colors
  open, openExtensionPreferences,
  getPreferenceValues,  // typed preferences
  LocalStorage,         // simple key-value store
  Cache,                // ephemeral cache
  environment,          // runtime info (isDevelopment, etc.)
} from "@raycast/api";

import {
  useFetch, useCachedState, useLocalStorage, useStreamJSON,
  runAppleScript, showFailureToast,
} from "@raycast/utils";
```

Look up signatures on https://developers.raycast.com/api-reference — it's exhaustive and accurate.

---

## 8. AI-assist prompt template

When you ask an AI to help with a new Raycast extension, lead with this so it doesn't hallucinate based on stale training data:

> I'm building a Raycast extension for Raycast for Mac v2 (beta, May 2026). Before writing any code, fetch these pages and base your output on them:
> - https://developers.raycast.com/information/manifest
> - https://developers.raycast.com/information/developer-tools/cli
> - https://developers.raycast.com/misc/changelog
>
> My target platform is `["macOS"]` only. I want a `<mode>` command named `<name>` that does `<what>`. Project lives at `<absolute path, not in ~/Documents>`. Use `@raycast/api@latest` and proper Title Case for command titles.

That preamble alone prevents 80% of the issues I've hit.
