Secure API key management and OAuth best practices for Linear. Use when setting up authentication securely, implementing OAuth flows, or hardening Linear integrations. Trigger with phrases like "linear security", "linear API key security", "linear OAuth", "secure linear integration", "linear secrets management".
/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:
Implement secure authentication and API key management for Linear integrations.
Never hardcode API keys:
// BAD - Never do this!
const client = new LinearClient({
apiKey: "lin_api_xxxxxxxxxxxx" // Exposed in source code
});
// GOOD - Use environment variables
const client = new LinearClient({
apiKey: process.env.LINEAR_API_KEY!
});
Environment Setup:
# .env (never commit this file)
LINEAR_API_KEY=lin_api_xxxxxxxxxxxx
# .gitignore (commit this)
.env
.env.*
!.env.example
# .env.example (commit this for documentation)
LINEAR_API_KEY=lin_api_your_key_here
Validate on Startup:
// config/linear.ts
function validateConfig(): void {
const apiKey = process.env.LINEAR_API_KEY;
if (!apiKey) {
throw new Error("LINEAR_API_KEY environment variable is required");
}
if (!apiKey.startsWith("lin_api_")) {
throw new Error("LINEAR_API_KEY has invalid format");
}
if (apiKey.length < 30) {
throw new Error("LINEAR_API_KEY appears too short");
}
}
validateConfig();
// For user-facing applications
import express from "express";
import crypto from "crypto";
const app = express();
// OAuth configuration
const OAUTH_CONFIG = {
clientId: process.env.LINEAR_CLIENT_ID!,
clientSecret: process.env.LINEAR_CLIENT_SECRET!,
redirectUri: process.env.LINEAR_REDIRECT_URI!,
scope: ["read", "write", "issues:create"],
};
// Step 1: Initiate OAuth
app.get("/auth/linear", (req, res) => {
const state = crypto.randomBytes(16).toString("hex");
req.session!.oauthState = state;
const authUrl = new URL("https://linear.app/oauth/authorize");
authUrl.searchParams.set("client_id", OAUTH_CONFIG.clientId);
authUrl.searchParams.set("redirect_uri", OAUTH_CONFIG.redirectUri);
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("scope", OAUTH_CONFIG.scope.join(","));
authUrl.searchParams.set("state", state);
res.redirect(authUrl.toString());
});
// Step 2: Handle callback
app.get("/auth/linear/callback", async (req, res) => {
const { code, state } = req.query;
// Verify state to prevent CSRF
if (state !== req.session!.oauthState) {
return res.status(400).json({ error: "Invalid state parameter" });
}
// Exchange code for tokens
const response = await fetch("https://api.linear.app/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code: code as string,
client_id: OAUTH_CONFIG.clientId,
client_secret: OAUTH_CONFIG.clientSecret,
redirect_uri: OAUTH_CONFIG.redirectUri,
}),
});
const tokens = await response.json();
// Store tokens securely (encrypted in database)
await storeTokens(req.user!.id, {
accessToken: encrypt(tokens.access_token),
refreshToken: encrypt(tokens.refresh_token),
expiresAt: new Date(Date.now() + tokens.expires_in * 1000),
});
res.redirect("/dashboard");
});
async function getValidAccessToken(userId: string): Promise<string> {
const stored = await getStoredTokens(userId);
// Check if token is expired or expiring soon (5 min buffer)
if (stored.expiresAt.getTime() - Date.now() < 5 * 60 * 1000) {
const response = await fetch("https://api.linear.app/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: decrypt(stored.refreshToken),
client_id: process.env.LINEAR_CLIENT_ID!,
client_secret: process.env.LINEAR_CLIENT_SECRET!,
}),
});
const tokens = await response.json();
await storeTokens(userId, {
accessToken: encrypt(tokens.access_token),
refreshToken: encrypt(tokens.refresh_token),
expiresAt: new Date(Date.now() + tokens.expires_in * 1000),
});
return tokens.access_token;
}
return decrypt(stored.accessToken);
}
import crypto from "crypto";
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
// Use timing-safe comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express middleware
app.post("/webhooks/linear", express.raw({ type: "*/*" }), (req, res) => {
const signature = req.headers["linear-signature"] as string;
const payload = req.body.toString();
if (!verifyWebhookSignature(payload, signature, process.env.LINEAR_WEBHOOK_SECRET!)) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(payload);
// Process verified webhook...
res.status(200).json({ received: true });
});
// Support multiple API keys during rotation
const apiKeys = [
process.env.LINEAR_API_KEY_NEW,
process.env.LINEAR_API_KEY_OLD,
].filter(Boolean);
async function getWorkingClient(): Promise<LinearClient> {
for (const apiKey of apiKeys) {
try {
const client = new LinearClient({ apiKey: apiKey! });
await client.viewer; // Test the key
return client;
} catch {
continue;
}
}
throw new Error("No valid Linear API key found");
}
| Error | Cause | Solution |
|---|---|---|
Invalid signature | Webhook secret mismatch | Verify secret matches Linear settings |
Token expired | Refresh token expired | Re-authorize user |
Invalid scope | Missing permission | Request additional scopes |
Prepare for production with linear-prod-checklist.
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.