From linear-pack
Installs @linear/sdk and configures Linear GraphQL API authentication via personal API keys or OAuth 2.0 (PKCE) for JS/TS Node.js projects.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin linear-packThis skill is limited to using the following tools:
Install the `@linear/sdk` TypeScript SDK and configure authentication for the Linear GraphQL API at `https://api.linear.app/graphql`. Supports personal API keys for scripts and OAuth 2.0 (with PKCE) for user-facing apps.
Guides secure Linear API integrations: API key env storage/validation, OAuth 2.0 PKCE flows, token refresh, and webhook HMAC verification.
Troubleshoots Linear MCP integration with fixes for OAuth failures, unauthorized access, token expiration, and setup via /mcp commands.
Runs Linear CLI to manage issues, comments, teams, projects: create/update/delete/view/comment. Handles auth, git/jj integration for terminal automation.
Share bugs, ideas, or general feedback.
Install the @linear/sdk TypeScript SDK and configure authentication for the Linear GraphQL API at https://api.linear.app/graphql. Supports personal API keys for scripts and OAuth 2.0 (with PKCE) for user-facing apps.
set -euo pipefail
npm install @linear/sdk
# or: pnpm add @linear/sdk
# or: yarn add @linear/sdk
The SDK exposes LinearClient, typed models for every entity (Issue, Project, Cycle, Team), and error classes (LinearError, InvalidInputLinearError).
Generate a Personal API key at Linear Settings > Account > API > Personal API keys. Keys start with lin_api_ and are shown only once.
import { LinearClient } from "@linear/sdk";
// Environment variable (recommended)
const client = new LinearClient({
apiKey: process.env.LINEAR_API_KEY,
});
// Verify connection
const me = await client.viewer;
console.log(`Authenticated as: ${me.name} (${me.email})`);
Environment setup:
# .env (never commit)
LINEAR_API_KEY=lin_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
# .gitignore
echo '.env' >> .gitignore
Linear supports the standard Authorization Code flow with optional PKCE. As of October 2025, newly created OAuth apps issue refresh tokens by default.
import crypto from "crypto";
// 1. Build authorization URL
const SCOPES = ["read", "write", "issues:create"];
const state = crypto.randomBytes(16).toString("hex");
const authUrl = new URL("https://linear.app/oauth/authorize");
authUrl.searchParams.set("client_id", process.env.LINEAR_CLIENT_ID!);
authUrl.searchParams.set("redirect_uri", process.env.LINEAR_REDIRECT_URI!);
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("scope", SCOPES.join(","));
authUrl.searchParams.set("state", state);
// Optional PKCE:
// authUrl.searchParams.set("code_challenge", challenge);
// authUrl.searchParams.set("code_challenge_method", "S256");
// 2. Exchange authorization code for tokens
const tokenResponse = 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: authorizationCode,
client_id: process.env.LINEAR_CLIENT_ID!,
client_secret: process.env.LINEAR_CLIENT_SECRET!,
redirect_uri: process.env.LINEAR_REDIRECT_URI!,
}),
});
const { access_token, refresh_token, expires_in } = await tokenResponse.json();
// 3. Create client with OAuth token
const client = new LinearClient({ accessToken: access_token });
Available OAuth scopes: read, write, issues:create, admin, initiative:read, initiative:write, customer:read, customer:write.
async function refreshAccessToken(refreshToken: string): Promise<string> {
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: refreshToken,
client_id: process.env.LINEAR_CLIENT_ID!,
client_secret: process.env.LINEAR_CLIENT_SECRET!,
}),
});
if (!response.ok) {
throw new Error(`Token refresh failed: ${response.status}`);
}
const tokens = await response.json();
// Store new refresh_token — Linear rotates it on each refresh
await saveTokens(tokens.access_token, tokens.refresh_token);
return tokens.access_token;
}
function validateLinearConfig(): void {
const key = process.env.LINEAR_API_KEY;
if (!key) throw new Error("LINEAR_API_KEY environment variable is required");
if (!key.startsWith("lin_api_")) throw new Error("LINEAR_API_KEY must start with lin_api_");
if (key.length < 30) throw new Error("LINEAR_API_KEY appears truncated");
}
// Call before creating client
validateLinearConfig();
| Error | Cause | Solution |
|---|---|---|
Authentication required | Invalid, expired, or missing API key | Regenerate at Settings > Account > API |
Invalid API key format | Key doesn't start with lin_api_ | Copy full key from Linear (shown once) |
Forbidden | Token lacks required OAuth scope | Re-authorize with correct scopes |
invalid_grant on token exchange | Code expired or PKCE verifier mismatch | Restart OAuth flow; codes expire quickly |
Module not found: @linear/sdk | SDK not installed | Run npm install @linear/sdk |
ENOTFOUND api.linear.app | DNS/firewall issue | Ensure outbound HTTPS to api.linear.app |
const response = await fetch("https://api.linear.app/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": process.env.LINEAR_API_KEY!,
},
body: JSON.stringify({
query: `{ viewer { id name email } }`,
}),
});
const { data, errors } = await response.json();
if (errors) console.error("GraphQL errors:", errors);
else console.log("Viewer:", data.viewer);
// For apps without user interaction — uses client_credentials grant
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: "client_credentials",
client_id: process.env.LINEAR_CLIENT_ID!,
client_secret: process.env.LINEAR_CLIENT_SECRET!,
scope: "read,write",
}),
});
const { access_token } = await response.json();
const client = new LinearClient({ accessToken: access_token });