Configure and handle Linear webhooks for real-time event processing. Use when setting up webhooks, handling Linear events, or building real-time integrations. Trigger with phrases like "linear webhooks", "linear events", "linear real-time", "handle linear webhook", "linear webhook setup".
/plugin marketplace add jeremylongshore/claude-code-plugins-plus-skills/plugin install linear-pack@claude-code-plugins-plusThis skill is limited to using the following tools:
Set up and handle Linear webhooks for real-time event notifications.
| Event Type | Description |
|---|---|
Issue | Issue created, updated, or removed |
IssueComment | Comment added or updated |
Project | Project changes |
Cycle | Cycle (sprint) changes |
Label | Label changes |
Reaction | Emoji reactions |
// api/webhooks/linear.ts (Vercel/Next.js style)
import crypto from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
export const config = {
api: {
bodyParser: false, // Need raw body for signature
},
};
async function getRawBody(req: NextApiRequest): Promise<string> {
const chunks: Buffer[] = [];
for await (const chunk of req) {
chunks.push(chunk);
}
return Buffer.concat(chunks).toString("utf8");
}
function verifySignature(payload: string, signature: string): boolean {
const secret = process.env.LINEAR_WEBHOOK_SECRET!;
const hmac = crypto.createHmac("sha256", secret);
const expectedSignature = hmac.update(payload).digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
const rawBody = await getRawBody(req);
const signature = req.headers["linear-signature"] as string;
if (!signature || !verifySignature(rawBody, signature)) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(rawBody);
// Process event
await processLinearEvent(event);
return res.status(200).json({ received: true });
}
// lib/webhook-handlers.ts
interface LinearWebhookPayload {
action: "create" | "update" | "remove";
type: string;
data: Record<string, unknown>;
createdAt: string;
organizationId: string;
webhookTimestamp: number;
webhookId: string;
}
type EventHandler = (data: Record<string, unknown>, action: string) => Promise<void>;
const handlers: Record<string, EventHandler> = {
Issue: handleIssueEvent,
IssueComment: handleCommentEvent,
Project: handleProjectEvent,
Cycle: handleCycleEvent,
};
export async function processLinearEvent(payload: LinearWebhookPayload) {
const handler = handlers[payload.type];
if (!handler) {
console.log(`No handler for event type: ${payload.type}`);
return;
}
try {
await handler(payload.data, payload.action);
} catch (error) {
console.error(`Error processing ${payload.type} event:`, error);
throw error;
}
}
async function handleIssueEvent(data: Record<string, unknown>, action: string) {
const issue = data as {
id: string;
identifier: string;
title: string;
state: { name: string };
priority: number;
team: { key: string };
};
console.log(`Issue ${action}: ${issue.identifier} - ${issue.title}`);
switch (action) {
case "create":
await onIssueCreated(issue);
break;
case "update":
await onIssueUpdated(issue);
break;
case "remove":
await onIssueRemoved(issue.id);
break;
}
}
async function handleCommentEvent(data: Record<string, unknown>, action: string) {
const comment = data as {
id: string;
body: string;
issue: { identifier: string };
user: { name: string };
};
console.log(`Comment ${action} on ${comment.issue.identifier} by ${comment.user.name}`);
}
async function handleProjectEvent(data: Record<string, unknown>, action: string) {
console.log(`Project ${action}:`, data);
}
async function handleCycleEvent(data: Record<string, unknown>, action: string) {
console.log(`Cycle ${action}:`, data);
}
// lib/linear-handlers.ts
import { sendSlackNotification } from "./slack";
import { syncToDatabase } from "./database";
async function onIssueCreated(issue: any) {
// Sync to local database
await syncToDatabase("issues", issue.id, issue);
// Notify Slack for high-priority issues
if (issue.priority <= 2) {
await sendSlackNotification({
channel: "#engineering-alerts",
text: `New high-priority issue: ${issue.identifier} - ${issue.title}`,
});
}
}
async function onIssueUpdated(issue: any) {
// Update local cache
await syncToDatabase("issues", issue.id, issue);
// Check for state changes
if (issue.state?.name === "Done") {
await celebrateCompletion(issue);
}
}
async function onIssueRemoved(issueId: string) {
await syncToDatabase("issues", issueId, null); // Soft delete
}
async function celebrateCompletion(issue: any) {
console.log(`Issue completed: ${issue.identifier}`);
}
# Using Linear UI:
# 1. Go to Settings > API > Webhooks
# 2. Click "Create webhook"
# 3. Enter your endpoint URL
# 4. Select events to receive
# 5. Save and copy the signing secret
// Or via API
import { LinearClient } from "@linear/sdk";
async function createWebhook() {
const client = new LinearClient({
apiKey: process.env.LINEAR_API_KEY!,
});
const result = await client.createWebhook({
url: "https://your-domain.com/api/webhooks/linear",
label: "My Integration Webhook",
teamId: "your-team-id", // Optional: limit to specific team
resourceTypes: ["Issue", "IssueComment", "Project"],
});
if (result.success) {
const webhook = await result.webhook;
console.log("Webhook created:", webhook?.id);
console.log("Secret (save this!):", webhook?.secret);
}
}
# Start your local server
npm run dev # Runs on localhost:3000
# In another terminal, start ngrok
ngrok http 3000
# Copy the https URL and add to Linear webhook settings
# Example: https://abc123.ngrok.io/api/webhooks/linear
// lib/idempotency.ts
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
export async function processIdempotent(
webhookId: string,
processor: () => Promise<void>
): Promise<boolean> {
const key = `webhook:${webhookId}`;
// Check if already processed
const exists = await redis.exists(key);
if (exists) {
console.log(`Webhook ${webhookId} already processed, skipping`);
return false;
}
// Mark as processing
await redis.setex(key, 86400, "processing"); // 24 hour TTL
try {
await processor();
await redis.setex(key, 86400, "completed");
return true;
} catch (error) {
await redis.del(key); // Allow retry
throw error;
}
}
// Usage in webhook handler
await processIdempotent(payload.webhookId, async () => {
await processLinearEvent(payload);
});
| Error | Cause | Solution |
|---|---|---|
Invalid signature | Wrong secret or tampering | Verify webhook secret |
Timeout | Processing too slow | Use async queue |
Duplicate events | Webhook retry | Implement idempotency |
Missing data | Partial event | Handle gracefully |
Optimize performance with linear-performance-tuning.
This skill should be used when the user asks to "create a hookify rule", "write a hook rule", "configure hookify", "add a hookify rule", or needs guidance on hookify rule syntax and patterns.
Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.