linear-local-dev-loop
Set up local Linear development environment and testing workflow. Use when configuring local dev, testing integrations, or setting up a development workflow with Linear webhooks. Trigger: "linear local development", "linear dev setup", "test linear locally", "linear development environment".
What this skill does
# Linear Local Dev Loop
## Overview
Set up an efficient local development workflow for building Linear integrations. Covers project scaffolding, environment config, test utilities, webhook tunneling with ngrok, and integration testing with vitest.
## Prerequisites
- Node.js 18+ with TypeScript
- `@linear/sdk` package
- Separate Linear workspace or team for development (recommended)
- ngrok or cloudflared for webhook tunnel testing
## Instructions
### Step 1: Project Scaffolding
```bash
set -euo pipefail
mkdir linear-integration && cd linear-integration
npm init -y
npm install @linear/sdk dotenv
npm install -D typescript @types/node vitest tsx
# TypeScript config
npx tsc --init --target ES2022 --module NodeNext --moduleResolution NodeNext --strict
```
### Step 2: Environment Configuration
```bash
# .env (never commit)
cat > .env << 'EOF'
LINEAR_API_KEY=lin_api_dev_xxxxxxxxxxxx
LINEAR_WEBHOOK_SECRET=whsec_dev_xxxxxxxxxxxx
LINEAR_DEV_TEAM_KEY=DEV
NODE_ENV=development
EOF
# .env.example (commit this for onboarding)
cat > .env.example << 'EOF'
LINEAR_API_KEY=lin_api_your_key_here
LINEAR_WEBHOOK_SECRET=
LINEAR_DEV_TEAM_KEY=DEV
NODE_ENV=development
EOF
echo -e ".env\n.env.local\n.env.*.local" >> .gitignore
```
### Step 3: Client Module with Connection Verification
```typescript
// src/client.ts
import { LinearClient } from "@linear/sdk";
import "dotenv/config";
let _client: LinearClient | null = null;
export function getClient(): LinearClient {
if (!_client) {
const apiKey = process.env.LINEAR_API_KEY;
if (!apiKey) throw new Error("LINEAR_API_KEY not set — copy .env.example to .env");
_client = new LinearClient({ apiKey });
}
return _client;
}
export async function verifyConnection(): Promise<void> {
const client = getClient();
const viewer = await client.viewer;
const teams = await client.teams();
console.log(`[Linear] Connected as ${viewer.name} (${viewer.email})`);
console.log(`[Linear] Teams: ${teams.nodes.map(t => t.key).join(", ")}`);
}
```
### Step 4: Test Data Utilities
```typescript
// src/test-utils.ts
import { getClient } from "./client";
const TEST_PREFIX = "[DEV-TEST]";
export async function getDevTeam() {
const client = getClient();
const teamKey = process.env.LINEAR_DEV_TEAM_KEY ?? "DEV";
const teams = await client.teams({ filter: { key: { eq: teamKey } } });
const team = teams.nodes[0];
if (!team) throw new Error(`Team ${teamKey} not found — set LINEAR_DEV_TEAM_KEY`);
return team;
}
export async function createTestIssue(title?: string) {
const client = getClient();
const team = await getDevTeam();
const result = await client.createIssue({
teamId: team.id,
title: `${TEST_PREFIX} ${title ?? new Date().toISOString()}`,
description: "Automated test issue — safe to delete",
priority: 4, // Low
});
return result.issue;
}
export async function cleanupTestIssues() {
const client = getClient();
const team = await getDevTeam();
const issues = await client.issues({
filter: {
team: { id: { eq: team.id } },
title: { startsWith: TEST_PREFIX },
},
first: 100,
});
let deleted = 0;
for (const issue of issues.nodes) {
await issue.delete();
deleted++;
}
console.log(`Cleaned up ${deleted} test issues`);
}
```
### Step 5: Integration Tests with Vitest
```typescript
// tests/linear.integration.test.ts
import { describe, it, expect, afterAll } from "vitest";
import { getClient } from "../src/client";
import { createTestIssue, cleanupTestIssues, getDevTeam } from "../src/test-utils";
describe("Linear Integration", () => {
afterAll(async () => {
await cleanupTestIssues();
});
it("authenticates successfully", async () => {
const client = getClient();
const viewer = await client.viewer;
expect(viewer.name).toBeDefined();
expect(viewer.email).toBeDefined();
});
it("creates and updates an issue", async () => {
const client = getClient();
const issue = await createTestIssue("vitest create");
expect(issue).toBeDefined();
expect(issue?.title).toContain("[DEV-TEST]");
// Update it
await client.updateIssue(issue!.id, { priority: 2 });
const updated = await client.issue(issue!.id);
expect(updated.priority).toBe(2);
});
it("queries workflow states", async () => {
const team = await getDevTeam();
const states = await team.states();
const types = states.nodes.map(s => s.type);
expect(types).toContain("unstarted");
expect(types).toContain("completed");
});
});
```
### Step 6: Package Scripts
```json
{
"scripts": {
"dev": "tsx watch src/index.ts",
"verify": "tsx src/verify-connection.ts",
"test": "vitest run",
"test:watch": "vitest --watch",
"cleanup": "tsx src/cleanup.ts"
}
}
```
### Step 7: Webhook Local Development with ngrok
```bash
# Terminal 1: Start your webhook server
npm run dev
# Terminal 2: Expose port 3000 via ngrok
ngrok http 3000
# Copy the https://xxxx.ngrok-free.app URL
# Register webhook in Linear:
# Settings > API > Webhooks > New webhook
# URL: https://xxxx.ngrok-free.app/webhooks/linear
# Select: Issues, Comments
```
Minimal webhook receiver for local testing:
```typescript
// src/webhook-dev.ts
import express from "express";
import crypto from "crypto";
const app = express();
app.post("/webhooks/linear", express.raw({ type: "*/*" }), (req, res) => {
const body = req.body.toString();
const sig = req.headers["linear-signature"] as string;
const secret = process.env.LINEAR_WEBHOOK_SECRET!;
if (secret && sig) {
const expected = crypto.createHmac("sha256", secret).update(body).digest("hex");
if (sig !== expected) {
console.warn("Signature mismatch — check LINEAR_WEBHOOK_SECRET");
}
}
const event = JSON.parse(body);
console.log(`[Webhook] ${event.type}.${event.action}:`, event.data?.identifier ?? event.data?.id);
res.json({ ok: true });
});
app.listen(3000, () => console.log("Webhook server on http://localhost:3000"));
```
## Error Handling
| Error | Cause | Solution |
|-------|-------|----------|
| `LINEAR_API_KEY not set` | Missing .env | Copy `.env.example` to `.env` and fill in values |
| `Team DEV not found` | Wrong team key | Set `LINEAR_DEV_TEAM_KEY` to a valid team key |
| `Cannot find module` | TypeScript path issue | Check `tsconfig.json` module resolution |
| Webhook not received | Tunnel not running | Start `ngrok http 3000` and register the URL |
| `Authentication required` | Expired dev API key | Regenerate in Linear Settings > Account > API |
## Examples
### Quick Connection Test Script
```typescript
// src/verify-connection.ts
import { verifyConnection } from "./client";
verifyConnection()
.then(() => console.log("Connection OK"))
.catch((err) => { console.error("Connection FAILED:", err.message); process.exit(1); });
```
## Resources
- [Linear SDK Documentation](https://linear.app/developers/sdk)
- [Linear Webhooks](https://linear.app/developers/webhooks)
- [Vitest Documentation](https://vitest.dev)
- [ngrok Quickstart](https://ngrok.com/docs/getting-started/)
Related in Code Review
gstack
IncludedFast headless browser for QA testing and site dogfooding. Navigate pages, interact with elements, verify state, diff before/after, take annotated screenshots, test responsive layouts, forms, uploads, dialogs, and capture bug evidence. Use when asked to open or test a site, verify a deployment, dogfood a user flow, or file a bug with screenshots. (gstack)
startup-due-diligence
IncludedLegal due diligence review for seed-stage and Series A startups (US, Delaware C-Corp focus). Supports both investor and founder perspectives. Capabilities include: (1) Interactive document review and issue spotting; (2) Document request list generation; (3) Cap table and SAFE/convertible note analysis; (4) Red flag identification with severity ratings; (5) Diligence report generation. TRIGGERS: due diligence, DD, startup investment, cap table review, Series A, seed round, investor diligence, legal review startup, SAFE analysis, convertible note, 409A, founder vesting.
interview-master
IncludedThis skill should be used when the user asks to "generate interview questions", "prepare for interview", "optimize resume", "conduct mock interview", "analyze git commits for resume", "generate resume from code", "review my resume", or mentions interview preparation, career assistance, or extracting project experience from git history. Provides comprehensive interview and career development guidance for both job seekers and interviewers.
fix-issue
IncludedFixes GitHub issues using parallel analysis agents for root cause investigation, code exploration, and regression detection. Reads issue context from gh CLI, searches codebase and memory for related patterns, generates a fix with tests, and links the resolution back to the issue via PR. Includes prevention analysis to avoid recurrence. Use when debugging errors, resolving regressions, fixing bugs, or triaging issues.
sf-apex
IncludedGenerates and reviews Salesforce Apex code with 150-point scoring. TRIGGER when: user writes, reviews, or fixes Apex classes, triggers, test classes, batch/queueable/schedulable jobs, or touches .cls/.trigger files. DO NOT TRIGGER when: LWC JavaScript (use sf-lwc), Flow XML (use sf-flow), SOQL-only queries (use sf-soql), or non-Salesforce code.
swift-development
IncludedComprehensive Swift development for building, testing, and deploying iOS/macOS applications. Use when Claude needs to: (1) Build Swift packages or Xcode projects from command line, (2) Run tests with XCTest or Swift Testing framework, (3) Manage iOS simulators with simctl, (4) Handle code signing, provisioning profiles, and app distribution, (5) Format or lint Swift code with SwiftFormat/SwiftLint, (6) Work with Swift Package Manager (SPM), (7) Implement Swift 6 concurrency patterns (async/await, actors, Sendable), (8) Create SwiftUI views with MVVM architecture, (9) Set up Core Data or SwiftData persistence, or any other Swift/iOS/macOS development tasks.