
raycast-extension
Build Raycast extensions with React and TypeScript. Use when the user asks to create a Raycast extension, command, or tool.
- Installs
- 322
- Last 24h
- —
- Trending rank
- —
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/johnlindquist/claude --skill raycast-extension- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads raycast-extension automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
Raycast Extension Development
Quick Start
- Create project structure
- Write package.json with extension config
- Implement command in src/
- Run
npm install && npm run dev
Project Structure
my-extension/
├── package.json # Extension manifest + dependencies
├── tsconfig.json # TypeScript config
├── .eslintrc.json # ESLint config
├── raycast-env.d.ts # Type definitions (auto-generated)
├── assets/
│ └── extension-icon.png # 512x512 PNG icon
└── src/
└── command-name.tsx # Command implementation
package.json Template
{
"name": "extension-name",
"title": "Extension Title",
"description": "What this extension does",
"icon": "extension-icon.png",
"author": "author-name",
"categories": ["Productivity", "Developer Tools"],
"license": "MIT",
"commands": [
{
"name": "command-name",
"title": "Command Title",
"description": "What this command does",
"mode": "view",
"keywords": ["keyword1", "keyword2"]
}
],
"dependencies": {
"@raycast/api": "^1.83.1",
"@raycast/utils": "^1.17.0"
},
"devDependencies": {
"@raycast/eslint-config": "^1.0.11",
"@types/node": "22.5.4",
"@types/react": "18.3.3",
"eslint": "^8.57.0",
"prettier": "^3.3.3",
"typescript": "^5.5.4"
},
"scripts": {
"build": "ray build --skip-types -e dist -o dist",
"dev": "ray develop",
"fix-lint": "ray lint --fix",
"lint": "ray lint"
}
}
Command Modes
| Mode | Use Case |
|---|---|
view | Show UI with Detail, List, Form, Grid |
no-view | Background task, clipboard, notifications only |
menu-bar | Menu bar icon with dropdown |
Hotkey Configuration
Add to command in package.json:
"hotkey": {
"modifiers": ["opt"],
"key": "m"
}
Modifiers: cmd, opt, ctrl, shift
Note: Hotkeys in package.json are suggestions. Users set them in Raycast Preferences → Extensions.
tsconfig.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"jsx": "react-jsx",
"lib": ["ES2022"],
"module": "ES2022",
"moduleResolution": "bundler",
"noEmit": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "ES2022"
},
"include": ["src/**/*", "raycast-env.d.ts"]
}
.eslintrc.json
{
"root": true,
"extends": ["@raycast"]
}
Command Patterns
No-View Command (Background Task)
import { showHUD, Clipboard, showToast, Toast } from "@raycast/api";
export default async function Command() {
const toast = await showToast({
style: Toast.Style.Animated,
title: "Working...",
});
try {
// Do work
const result = await doSomething();
await Clipboard.copy(result);
await showHUD("✅ Done!");
} catch (error) {
toast.style = Toast.Style.Failure;
toast.title = "Failed";
toast.message = error instanceof Error ? error.message : "Unknown error";
}
}
View Command (List)
import { List, ActionPanel, Action } from "@raycast/api";
export default function Command() {
return (
<List>
<List.Item
title="Item"
actions={
<ActionPanel>
<Action.CopyToClipboard content="text" />
</ActionPanel>
}
/>
</List>
);
}
View Command (Detail)
import { Detail } from "@raycast/api";
export default function Command() {
const markdown = `# Hello World`;
return <Detail markdown={markdown} />;
}
Performance & Caching
Instant Load Pattern (No Empty Flash)
Use synchronous cache read + async refresh for instant perceived load:
import { List, Cache } from "@raycast/api";
import { useCachedPromise, withCache } from "@raycast/utils";
const cache = new Cache();
const CACHE_KEY = "myData";
// Read cache synchronously at module load (before React renders)
function getInitialData(): MyData[] {
const cached = cache.get(CACHE_KEY);
if (cached) {
try {
return JSON.parse(cached);
} catch {
return [];
}
}
return [];
}
// Expensive async operation wrapped with withCache (5 min TTL)
const fetchExpensiveData = withCache(
async () => {
// Your expensive operation here
return await someSlowOperation();
},
{ maxAge: 5 * 60 * 1000 }
);
async function fetchAllData(): Promise<MyData[]> {
const data = await fetchExpensiveData();
// Update cache for next launch
cache.set(CACHE_KEY, JSON.stringify(data));
return data;
}
export default function Command() {
const { data, isLoading } = useCachedPromise(fetchAllData, [], {
initialData: getInitialData(), // Sync read - instant render!
keepPreviousData: true,
});
return (
<List isLoading={isLoading && !data?.length}>
{data?.map(item => <List.Item key={item.id} title={item.name} />)}
</List>
);
}
Key Caching Utilities
| Utility | Purpose |
|---|---|
Cache | Persistent disk cache, sync read/write |
withCache(fn, {maxAge}) | Wrap async functions with TTL cache |
useCachedPromise | Stale-while-revalidate pattern |
LocalStorage | Async key-value storage |
Avoiding CLS (Content Layout Shift)
Load all data in ONE async function:
// BAD - causes layout shift
const [customData, setCustomData] = useState([]);
useEffect(() => {
loadCustomData().then(setCustomData); // Second render!
}, []);
// GOOD - single fetch, no shift
async function fetchAllData() {
const [dataA, dataB] = await Promise.all([
fetchDataA(),
fetchDataB(),
]);
return combineData(dataA, dataB);
}
Non-Blocking Operations (Prevent UI Freeze)
Root cause of "tiny delay": Sync operations (execSync, statSync, readdirSync) block the event loop during revalidation, freezing the UI even with cached data displayed.
// BAD - blocks event loop, UI freezes during revalidation
import { execSync } from "child_process";
import { statSync, readdirSync, copyFileSync } from "fs";
function fetchData() {
copyFileSync(src, dest); // Blocks!
const result = execSync("sqlite3 query"); // Blocks!
const entries = readdirSync(dir); // Blocks!
for (const entry of entries) {
statSync(join(dir, entry)); // Blocks N times!
}
}
// GOOD - fully async, UI renders cached data while refreshing
import { exec } from "child_process";
import { promisify } from "util";
import { stat, readdir, copyFile, access } from "fs/promises";
const execAsync = promisify(exec);
async function fetchData() {
await copyFile(src, dest); // Non-blocking
const { stdout } = await execAsync("sqlite3..."); // Non-blocking
// Use withFileTypes to avoid extra stat calls
const entries = await readdir(dir, { withFileTypes: true });
const results = entries
.filter(e => e.isDirectory()) // No stat needed!
.map(e => ({ path: join(dir, e.name), name: e.name }));
}
Key optimizations:
- Replace
execSyncwithpromisify(exec)for shell commands - Replace
existsSyncwithaccess()fromfs/promises - Replace
readdirSync+statSyncloop withreaddir(dir, { withFileTypes: true }) - Run all path validations in parallel with
Promise.all - Use SQLite URI mode for direct read-only access (no file copy needed)
SQLite Direct Access (Skip File Copy)
When reading SQLite databases from other apps (like Zed, VS Code, etc.), avoid copying the database file. Use URI mode for direct read-only access:
// BAD - copies entire database file (slow, blocks)
import { copyFileSync, unlinkSync } from "fs";
const tempDb = `/tmp/copy-${Date.now()}.sqlite`;
copyFileSync(originalDb, tempDb); // Expensive!
execSync(`sqlite3 "${tempDb}" "SELECT..."`);
unlinkSync(tempDb); // Cleanup
// GOOD - direct read-only access via URI mode
const uri = `file:${originalDb}?mode=ro&immutable=1`;
const { stdout } = await execAsync(`sqlite3 "${uri}" "SELECT..."`);
URI parameters:
mode=ro- Read-only mode, no write locks acquiredimmutable=1- Skip WAL/lock checks, treat file as immutable
This eliminates the file copy entirely, saving significant I/O time.
execFile vs exec (Bypass Shell)
exec spawns a shell (~20ms overhead), execFile calls binary directly (~4ms):
// BAD - spawns shell, parses command string
import { exec } from "child_process";
const execAsync = promisify(exec);
await execAsync(`sqlite3 -separator '|||' "${db}" "${query}"`);
// GOOD - direct binary execution, ~16ms faster
import { execFile } from "child_process";
const execFileAsync = promisify(execFile);
await execFileAsync("sqlite3", ["-separator", "|||", db, query]);
Sidecar Pattern (True Background Preloading)
For truly instant cold starts, use a background worker to pre-warm the cache before the user opens the extension.
The Problem: view commands cannot use interval (background scheduling). Only no-view and menu-bar modes support it.
The Solution: Create two commands that share the same cache:
// package.json
{
"commands": [
{
"name": "main",
"title": "My Extension",
"mode": "view"
},
{
"name": "background-sync",
"title": "Background Sync",
"mode": "no-view",
"interval": "15m"
}
]
}
// shared-cache.ts - both commands import this
import { Cache } from "@raycast/api";
export const sharedCache = new Cache(); // Shared across extension
// background-sync.tsx (no-view worker)
import { sharedCache } from "./shared-cache";
export default async function Command() {
const data = await fetchExpensiveData();
sharedCache.set("projects", JSON.stringify(data));
}
// main.tsx (view command)
import { sharedCache } from "./shared-cache";
function getInitialData() {
const cached = sharedCache.get("projects");
return cached ? JSON.parse(cached) : [];
}
export default function Command() {
const { data } = useCachedPromise(fetchData, [], {
initialData: getInitialData(), // Instant from pre-warmed cache!
});
}
Key points:
- Worker runs silently on interval, user never sees it
- Both commands share the same
Cache(scoped to extension, not command) - View command reads synchronously from pre-warmed cache
- Use
15mto1hintervals to avoid battery/rate-limit issues
Large Datasets: useSQL over JSON Cache
For >1,000 items, use SQLite instead of JSON cache for instant filtering:
// BAD - loads entire 10MB JSON into memory to filter
const allProjects = JSON.parse(cache.get("projects"));
const filtered = allProjects.filter(p => p.name.includes(query));
// GOOD - SQLite queries only matching rows
import { useSQL } from "@raycast/utils";
const { data } = useSQL(dbPath, `SELECT * FROM projects WHERE name LIKE ?`, [`%${query}%`]);
Optimistic UI (Instant Actions)
For write operations, update UI immediately before API confirms:
const { mutate } = useCachedPromise(fetchItems);
async function deleteItem(id: string) {
await mutate(deleteItemAPI(id), {
optimisticUpdate: (current) => current.filter(i => i.id !== id),
rollbackOnError: true, // Revert if API fails
});
}
User sees instant feedback; rollback happens automatically on failure.
// BAD - sequential stat calls
const entries = readdirSync(dir);
for (const entry of entries) {
const s = statSync(join(dir, entry)); // N blocking calls
}
// GOOD - parallel async checks
const checkPath = async (p: string) => {
try {
const s = await stat(p);
return s.isDirectory() ? p : null;
} catch { return null; }
};
const results = await Promise.all(paths.map(checkPath));
Common APIs
Clipboard
import { Clipboard } from "@raycast/api";
await Clipboard.copy("text");
await Clipboard.paste("text");
const text = await Clipboard.readText();
Notifications
import { showHUD, showToast, Toast } from "@raycast/api";
// Quick notification (disappears)
await showHUD("Done!");
// Toast with progress
const toast = await showToast({
style: Toast.Style.Animated,
title: "Loading...",
});
toast.style = Toast.Style.Success;
toast.title = "Complete";
AppleScript (macOS Integration)
import { runAppleScript } from "@raycast/utils";
// Get Chrome active tab URL
const url = await runAppleScript(`
tell application "Google Chrome"
return URL of active tab of front window
end tell
`);
// Get Safari URL
const safariUrl = await runAppleScript(`
tell application "Safari"
return URL of current tab of front window
end tell
`);
// Get frontmost app
const app = await runAppleScript(`
tell application "System Events"
return name of first application process whose frontmost is true
end tell
`);
Fetch Data
// Native fetch works
const response = await fetch("https://api.example.com/data");
const data = await response.json();
Preferences
In package.json:
"preferences": [
{
"name": "apiKey",
"type": "password",
"required": true,
"title": "API Key",
"description": "Your API key"
}
]
In code:
import { getPreferenceValues } from "@raycast/api";
interface Preferences {
apiKey: string;
}
const { apiKey } = getPreferenceValues<Preferences>();
Creating Extension Icon
Use ImageMagick:
convert -size 512x512 xc:'#6366F1' -fill white -gravity center \
-font Helvetica-Bold -pointsize 280 -annotate +0+20 'M' \
assets/extension-icon.png
Development Workflow
# Install dependencies
npm install
# Start dev server (hot reload)
npm run dev
# Lint and fix
npm run fix-lint
# Build for production
npm run build
Raycast Deeplinks
Trigger Raycast commands programmatically via URL scheme:
# Reload all extensions
open "raycast://extensions/raycast/raycast/reload-extensions"
# Open Raycast
open "raycast://focus"
# Run any extension command
open "raycast://extensions/{author}/{extension}/{command}"
Auto-reload after build
Add to package.json scripts:
"build": "ray build --skip-types -e dist -o dist && open raycast://extensions/raycast/raycast/reload-extensions"
Or create a reload script:
#!/bin/bash
npm run build && open "raycast://extensions/raycast/raycast/reload-extensions"
Testing in Raycast
- Run
npm run dev(provides hot reload) - Open Raycast
- Search for your command name
- Press Enter to run
Without dev server running, use deeplink to reload after changes:
npm run build && open "raycast://extensions/raycast/raycast/reload-extensions"
Publishing
npm run publish
Submits to Raycast Store for review.
FAQ
What does the raycast-extension skill do?
Build Raycast extensions with React and TypeScript. Use when the user asks to create a Raycast extension, command, or tool.
How do I install raycast-extension?
Run npx skills add https://github.com/johnlindquist/claude --skill raycast-extension in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/raycast-extension/ (personal) or .claude/skills/raycast-extension/ (project).
Which AI agents can use raycast-extension?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is raycast-extension safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains raycast-extension?
It is published by johnlindquist in johnlindquist/claude. It has 322 recorded installs.
More from johnlindquist/claude
gemini-image
johnlindquist/claude
Analyze images using Gemini's vision capabilities. Use for image analysis, text extraction from screenshots, and visual content understanding.
memory
johnlindquist/claude
Persistent knowledge storage using basic-memory CLI. Use to save notes, search memories semantically, and build context for topics across sessions.
brainstorm
johnlindquist/claude
Generate ideas and explore possibilities with AI. Use for creative problem solving, generating alternatives, and expanding on concepts.
deepwiki
johnlindquist/claude
Query DeepWiki for repository documentation and structure. Use to understand open source projects, find API docs, and explore codebases.
think
johnlindquist/claude
Deep multi-framework reasoning using Gemini. Use for complex problem analysis, challenging ideas, and evaluating multiple options with structured thinking.
Popular frontend & design skills
setup-matt-pocock-skills
mattpocock/skills
Configure this repo for the engineering skills: set up its issue tracker, triage label vocabulary, and domain doc layout. Run once before first use of the other engineering skills.
frontend-design
anthropics/skills
Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one. Helps with aesthetic direction, typography, and making choices that don't read as templated defaults.
hyperframes-animation
heygen-com/hyperframes
All animation knowledge for HyperFrames — atomic motion rules, multi-phase scene blueprints, scene transitions, broader motion-design techniques, AND the seven runtime adapters (GSAP default, plus Lottie, Three.js, Anime.js, CSS keyframes, Web Animations API, TypeGPU). Use for any motion or animation task: pick 2-4 rules and compose, or load a blueprint, or look up runtime-specific API (e.g. GSAP eases / Lottie player / Three.js mixer). Also covers auditing an existing composition's choreography (animation map) and 24 named text-animation effects. HyperFrames-native: single paused timeline, seek-safe, deterministic.
prototype
mattpocock/skills
Build a throwaway prototype to answer a design question. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like.
hyperframes-core
heygen-com/hyperframes
The HyperFrames composition contract — build one renderable project. Use for composition structure, the data- timing attributes, class="clip", tracks, sub-compositions, variables, framework-owned media playback, deterministic-render rules, and validation. Read before writing composition HTML.
vercel-react-best-practices
vercel-labs/agent-skills
React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.