Why Use This
This skill provides specialized capabilities for jeremylongshore's codebase.
Use Cases
- Developing new features in the jeremylongshore repository
- Refactoring existing code to follow jeremylongshore standards
- Understanding and working with jeremylongshore's codebase structure
Install Guide
2 steps - 1
- 2
Install inside Ananke
Click Install Skill, paste the link below, then press Install.
https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/main/plugins/saas-packs/linear-pack/skills/linear-local-dev-loop
Skill Snapshot
Auto scan of skill assets. Informational only.
Valid SKILL.md
Checks against SKILL.md specification
Source & Community
Updated At May 23, 2026, 05:41 AM
Skill Stats
SKILL.md 279 Lines
Total Files 1
Total Size 7.6 KB
License MIT
---
name: linear-local-dev-loop
description: '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".
'
allowed-tools: Read, Write, Edit, Bash(npm:*), Bash(npx:*), Grep
version: 1.0.0
license: MIT
author: Jeremy Longshore <[email protected]>
tags:
- saas
- linear
- testing
- workflow
compatibility: Designed for Claude Code, also compatible with Codex and OpenClaw
---
# 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/)