
Linear
- 1 installs
- 2 repo stars
- Updated July 6, 2026
- etusdigital/bhono
Manages Linear issues, teams, projects, statuses, labels, and comments via TypeScript CLI scripts that return JSON.
About
Executes Linear operations (create, update, list, filter issues and comments) through TypeScript CLI scripts with JSON output and auto-loaded config and credentials. A developer uses it when they want to manage Linear issues and projects from Claude Code.
- Named-flag and positional CLI scripts for issue and team operations
- Auto-loads teamId config and Linear API key from env, file, or .env
Linear by the numbers
- 1 all-time installs (skills.sh)
- Ranked #2,479 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Data as of Jul 8, 2026 (Skillselion catalog sync)
npx skills add https://github.com/etusdigital/bhono --skill linearAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 2 |
| Last updated | July 6, 2026 |
| Repository | etusdigital/bhono ↗ |
What it does
Manages Linear issues, teams, projects, statuses, labels, and comments via TypeScript CLI scripts that return JSON.
Files
Linear
Execute Linear operations via TypeScript CLI scripts with JSON output.
When to Use
Activate when user requests:
- Creating, updating, or archiving issues
- Listing or filtering teams, projects, or issues
- Managing status, priority, labels, or assignments
- Adding or viewing comments
Trigger keywords: Linear, issue, team, project, status, label, comment, priority
Quick Start
cd .claude/skills/linear
# Create issue (named flags - recommended)
npx tsx scripts/issues/create.ts --title "Bug fix" --description "Details here" --json
# List issues
npx tsx scripts/issues/list.ts --json
# Update issue
npx tsx scripts/issues/update.ts --issue ABC-123 --title "New title" --priority 2 --jsonArgument Styles
Scripts support two argument styles:
Named Flags (Recommended)
More readable, self-documenting, order doesn't matter:
npx tsx scripts/issues/create.ts --title "My Issue" --description "Details" --priority 2 --json
npx tsx scripts/issues/update.ts --issue ABC-123 --title "Updated" --priority 1 --jsonPositional (Legacy)
Shorter but order-dependent:
npx tsx scripts/issues/create.ts "My Issue" "team-uuid" --description "Details" --json
npx tsx scripts/issues/update.ts ABC-123 --title "Updated" --jsonAlways use named flags for complex operations with descriptions.
Configuration
Scripts auto-load defaults from .claude/linear-config.json:
# With config: teamId auto-filled
npx tsx scripts/issues/create.ts --title "Bug fix" --json
# Without config: must specify teamId
npx tsx scripts/issues/create.ts --title "Bug fix" --teamId "uuid" --jsonSetup config: Run /linear:setup to create defaults.
Authentication
Credentials loaded automatically (priority order): 1. LINEAR_API_KEY environment variable 2. ~/.linear/credentials file 3. .env file in project root
Setup credentials:
cd .claude/skills/linear && npx tsx scripts/setup/setup-credentials.tsCore Commands
Issues
Create issue:
npx tsx scripts/issues/create.ts --title "Issue title" [options] --jsonOptions:
| Option | Description |
|---|---|
--title <text> | Issue title (required) |
--teamId <id> | Team ID (optional if configured) |
--description <text> | Issue description (markdown) |
--state <id> | Initial workflow state ID |
--priority <0-4> | 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low |
--assignee <id> | User ID to assign |
--labels <ids> | Comma-separated label IDs |
--project <id> | Project ID |
--estimate <points> | Point estimate |
--due <YYYY-MM-DD> | Due date |
--parent <id> | Parent issue ID or identifier (e.g., AA-123) for sub-issues |
--cycle <id> | Cycle/sprint ID |
List issues:
npx tsx scripts/issues/list.ts [teamId] --jsonGet issue details:
npx tsx scripts/issues/get.ts ABC-123 --jsonUpdate issue:
npx tsx scripts/issues/update.ts --issue ABC-123 [options] --jsonOptions:
| Option | Description |
|---|---|
--issue <id> | Issue ID or identifier (required) |
--title <text> | New title |
--description <text> | New description (markdown) |
--state <id> | Workflow state ID |
--priority <0-4> | Priority level |
--assignee <id> | User ID (none to unassign) |
--labels <ids> | Label IDs (replaces existing) |
--project <id> | Project ID (none to remove) |
--estimate <points> | Point estimate (none to clear) |
--due <YYYY-MM-DD> | Due date (none to clear) |
--parent <id> | Parent issue ID or identifier (e.g., AA-123) (none to remove) |
--cycle <id> | Cycle/sprint ID (none to remove) |
Archive issue:
npx tsx scripts/issues/archive.ts ABC-123 --jsonTeams & Projects
npx tsx scripts/list-teams.ts --jsonStatus
List workflow states:
npx tsx scripts/status/list.ts [teamId] --jsonUpdate status by name (RECOMMENDED):
npx tsx scripts/status/set-by-name.ts ABC-123 "Done" --json
npx tsx scripts/status/set-by-name.ts ABC-123 "In Progress" --jsonUpdate status by ID (legacy):
npx tsx scripts/status/update.ts ABC-123 <stateId> --jsonComments
Add comment:
npx tsx scripts/comments/create.ts ABC-123 "Comment text" --jsonList comments:
npx tsx scripts/comments/list.ts ABC-123 --jsonLabels
List labels:
npx tsx scripts/labels/list.ts [teamId] --jsonCreate label:
npx tsx scripts/labels/create.ts "bug" [teamId] [color] --jsonAdd to issue:
npx tsx scripts/labels/add-to-issue.ts ABC-123 <labelId> --jsonUsers
npx tsx scripts/users/me.ts --json
npx tsx scripts/users/list.ts [teamId] --jsonCommon Workflows
Create Issue with Full Details
cd .claude/skills/linear
npx tsx scripts/issues/create.ts \
--title "Epic: New Feature" \
--description "## Overview
Description with markdown support.
## Tasks
- [ ] Task 1
- [ ] Task 2" \
--priority 2 \
--jsonCreate and Label Issue
cd .claude/skills/linear
# Create issue
ISSUE=$(npx tsx scripts/issues/create.ts --title "Bug: Login broken" --json)
ISSUE_ID=$(echo "$ISSUE" | jq -r '.id')
# Get label ID
LABELS=$(npx tsx scripts/labels/list.ts --json)
LABEL_ID=$(echo "$LABELS" | jq -r '.labels[] | select(.name == "bug") | .id')
# Add label
npx tsx scripts/labels/add-to-issue.ts $ISSUE_ID $LABEL_ID --jsonUpdate Status
cd .claude/skills/linear
# Update status by name (RECOMMENDED - no jq needed)
npx tsx scripts/status/set-by-name.ts ABC-123 "In Progress" --json
npx tsx scripts/status/set-by-name.ts ABC-123 "Done" --json
# Legacy: Get state ID and update (requires jq)
# STATES=$(npx tsx scripts/status/list.ts --json)
# IN_PROGRESS=$(echo "$STATES" | jq -r '.statuses[] | select(.name == "In Progress") | .id')
# npx tsx scripts/status/update.ts ABC-123 $IN_PROGRESS --jsonIssue Workflow
CRITICAL: When working on Linear issues, follow this lifecycle to maintain consistency.
Starting Work on an Issue
1. Update status to "In Progress":
cd .claude/skills/linear
npx tsx scripts/status/set-by-name.ts ABC-123 "In Progress" --json2. Assign yourself (if unassigned):
cd .claude/skills/linear
ME=$(npx tsx scripts/users/me.ts --json | jq -r '.id')
npx tsx scripts/issues/update.ts --issue ABC-123 --assignee $ME --jsonMoving to Review
When work is ready for review:
cd .claude/skills/linear
npx tsx scripts/status/set-by-name.ts ABC-123 "In Review" --jsonMoving to Icebox
For issues to defer indefinitely:
cd .claude/skills/linear
npx tsx scripts/status/set-by-name.ts ABC-123 "Icebox" --jsonCompleting an Issue
Before marking any issue done:
1. Check related issues:
- Parent issues (if working on subtasks)
- Sibling issues (other subtasks in same parent)
- Child issues (if completing a parent task)
2. Add completion comment with:
- Summary of what was done
- Files created/modified with descriptions
- Features implemented
- Next steps or configuration requirements
3. Update status to "Done":
cd .claude/skills/linear
npx tsx scripts/status/set-by-name.ts ABC-123 "Done" --jsonDocumentation Standards
Every completed issue must have a comment like:
## Completion Summary
**Status**: ✅ COMPLETED
### Changes Made
- `src/feature.ts`: Added new feature implementation
- `tests/feature.test.ts`: Added unit tests
### Features Implemented
- Feature 1: Description
- Feature 2: Description
### Next Steps (if any)
- Follow-up task 1
- Configuration neededParent Issue Updates
When all subtasks of a parent issue are complete: 1. Mark all acceptance criteria as completed [x] 2. Add summary referencing all completed subtask IDs 3. Update parent status to "Done"
Documentation Quality
| Quality | Example |
|---|---|
| ✅ Good | Detailed completion with files, features, implementation notes |
| ❌ Poor | Just marked Done without explanation |
See also: workflows/issue-lifecycle.md for detailed workflow documentation.
Output Format
- With `--json`: Pure JSON, ready for parsing
- Without `--json`: Formatted with colors (human-readable)
Always use `--json` for agent operations.
Best Practices
1. Use named flags for create/update operations 2. Always append `--json` for parseable output 3. Run `/linear:setup` once to configure defaults 4. Quote arguments with spaces or special characters 5. Parse with jq for reliable extraction
Mentions & Embeds
Linear automatically converts URLs in descriptions to mentions and embeds.
User Mentions
Use the profile URL format (not @username):
npx tsx scripts/issues/create.ts \
--title "Review needed" \
--description "https://linear.app/your-workspace/profiles/username please review" \
--jsonLinear converts https://linear.app/.../profiles/username → @username
Issue Mentions
Use the issue URL:
npx tsx scripts/issues/create.ts \
--title "Follow-up" \
--description "Related to https://linear.app/your-workspace/issue/AA-123" \
--jsonLinear converts the URL → clickable AA-123 link
Embeds
Paste URLs directly - Linear auto-embeds supported platforms:
| Platform | Example |
|---|---|
| YouTube | https://www.youtube.com/watch?v=... |
| Figma | https://www.figma.com/file/... |
| Loom | https://www.loom.com/share/... |
| Descript | https://share.descript.com/... |
npx tsx scripts/issues/create.ts \
--title "Design review" \
--description "Review mockup: https://www.figma.com/file/abc123" \
--jsonSlash Commands
- `/linear:setup` - Configure project defaults (team, project, auto-assign)
- `/linear:create-issue` - Interactive issue creation
Setup
cd .claude/skills/linear
npm install
npx tsx scripts/setup/setup-credentials.ts
npx tsx scripts/list-teams.ts --json # Test connectionAdditional Resources
reference.md- API reference, JSON schemas, troubleshootingexamples.md- Real-world workflow examplestemplates/- Issue template guide and examples:README.md- Templates overview and best practicesbug-report.md- Bug report template with severity guidelinesfeature-request.md- Feature request template with user storiestech-debt.md- Technical debt template with migration patternssecurity-issue.md- Security vulnerability template (CVSS, disclosure)sprint-task.md- Sprint task template with estimation guidesapi-reference.md- Using templates via the Linear API
Linear Operations - Examples
Example 1: Create Issue and Move to In Progress
User request: "Create a bug issue and move it to In Progress"
Execution:
# Get team ID
TEAM_ID=$(npx tsx list-teams.ts --json | jq -r '.teams[] | select(.name == "Engineering") | .id')
# Create issue
RESULT=$(npx tsx scripts/issues/create.ts "Bug: Login broken" "$TEAM_ID" "Users cannot authenticate" --json)
ISSUE_ID=$(echo "$RESULT" | jq -r '.id')
# Get "In Progress" status ID
STATUSES=$(npx tsx scripts/status/list.ts "$TEAM_ID" --json)
STATE_ID=$(echo "$STATUSES" | jq -r '.statuses[] | select(.name == "In Progress") | .id')
# Update issue status
npx tsx scripts/status/update.ts "$ISSUE_ID" "$STATE_ID" --jsonResponse: "Created issue ENG-124 and moved to In Progress"
---
Example 2: List Urgent Issues
User request: "Show me all urgent issues"
Execution:
# Get team ID
TEAM_ID=$(npx tsx list-teams.ts --json | jq -r '.teams[0].id')
# Get all issues
ISSUES=$(npx tsx scripts/issues/list.ts "$TEAM_ID" --json)
# Filter urgent (priority 1)
echo "$ISSUES" | jq '.issues[] | select(.priority == 1)'Response: Display filtered list with identifiers, titles, and status
---
Example 3: Add Comments to Multiple Issues
User request: "Add status update comment to all issues in Todo"
Execution:
# Get team and issues
TEAM_ID=$(npx tsx list-teams.ts --json | jq -r '.teams[0].id')
ISSUES=$(npx tsx scripts/issues/list.ts "$TEAM_ID" --json)
# Get Todo issue IDs
TODO_IDS=$(echo "$ISSUES" | jq -r '.issues[] | select(.status == "Todo") | .id')
# Add comment to each
for ISSUE_ID in $TODO_IDS; do
npx tsx scripts/comments/create.ts "$ISSUE_ID" "Status update: reviewing next sprint" --json
doneResponse: "Added comments to 5 Todo issues"
---
Example 4: Create Issue with Label
User request: "Create a feature request and tag it"
Execution:
# Get team
TEAM_ID=$(npx tsx list-teams.ts --json | jq -r '.teams[0].id')
# Create issue
RESULT=$(npx tsx scripts/issues/create.ts "Feature: Dark mode" "$TEAM_ID" "Add dark mode support" --json)
ISSUE_ID=$(echo "$RESULT" | jq -r '.id')
# Get or create "feature" label
LABELS=$(npx tsx scripts/labels/list.ts "$TEAM_ID" --json)
LABEL_ID=$(echo "$LABELS" | jq -r '.labels[] | select(.name == "feature") | .id')
# If label doesn't exist, create it
if [ -z "$LABEL_ID" ]; then
LABEL_RESULT=$(npx tsx scripts/labels/create.ts "feature" "$TEAM_ID" "#0000ff" --json)
LABEL_ID=$(echo "$LABEL_RESULT" | jq -r '.id')
fi
# Add label to issue
npx tsx scripts/labels/add-to-issue.ts "$ISSUE_ID" "$LABEL_ID" --jsonResponse: "Created issue ENG-125 with 'feature' label"
---
Example 5: Bulk Status Update
User request: "Move all 'Backlog' issues to 'Todo'"
Execution:
# Get team and statuses
TEAM_ID=$(npx tsx list-teams.ts --json | jq -r '.teams[0].id')
STATUSES=$(npx tsx scripts/status/list.ts "$TEAM_ID" --json)
TODO_STATE=$(echo "$STATUSES" | jq -r '.statuses[] | select(.name == "Todo") | .id')
# Get backlog issues
ISSUES=$(npx tsx scripts/issues/list.ts "$TEAM_ID" --json)
BACKLOG_IDS=$(echo "$ISSUES" | jq -r '.issues[] | select(.status == "Backlog") | .id')
# Update each
COUNT=0
for ISSUE_ID in $BACKLOG_IDS; do
npx tsx scripts/status/update.ts "$ISSUE_ID" "$TODO_STATE" --json
COUNT=$((COUNT + 1))
done
echo "Updated $COUNT issues from Backlog to Todo"Response: "Updated 12 issues from Backlog to Todo"
---
Example 6: Issue Report
User request: "Give me a summary of issues by status"
Execution:
# Get issues
TEAM_ID=$(npx tsx list-teams.ts --json | jq -r '.teams[0].id')
ISSUES=$(npx tsx scripts/issues/list.ts "$TEAM_ID" --json)
# Count by status
TODO=$(echo "$ISSUES" | jq '[.issues[] | select(.status == "Todo")] | length')
IN_PROGRESS=$(echo "$ISSUES" | jq '[.issues[] | select(.status == "In Progress")] | length')
DONE=$(echo "$ISSUES" | jq '[.issues[] | select(.status == "Done")] | length')
echo "Issue Summary:"
echo "- Todo: $TODO"
echo "- In Progress: $IN_PROGRESS"
echo "- Done: $DONE"Response:
Issue Summary:
- Todo: 15
- In Progress: 8
- Done: 42---
Example 7: Get Issue Details and Comments
User request: "Show me details and comments for AA-123"
Execution:
# Get issue details
ISSUE=$(npx tsx scripts/issues/get.ts AA-123 --json)
# Get comments
COMMENTS=$(npx tsx scripts/comments/list.ts "$(echo "$ISSUE" | jq -r '.id')" --json)
# Format output
echo "Issue: $(echo "$ISSUE" | jq -r '.title')"
echo "Status: $(echo "$ISSUE" | jq -r '.status')"
echo "Priority: $(echo "$ISSUE" | jq -r '.priorityLabel')"
echo ""
echo "Comments:"
echo "$COMMENTS" | jq -r '.comments[] | "- \(.user.name): \(.body)"'Response:
Issue: Fix login bug
Status: In Progress
Priority: Urgent
Comments:
- John Doe: Working on this now
- Jane Smith: Found the root cause---
Example 8: Create Related Issues
User request: "Create a parent issue and 3 sub-tasks"
Execution:
# Get team
TEAM_ID=$(npx tsx list-teams.ts --json | jq -r '.teams[0].id')
# Create parent issue
PARENT=$(npx tsx scripts/issues/create.ts "Implement user dashboard" "$TEAM_ID" "Main dashboard feature" --json)
PARENT_ID=$(echo "$PARENT" | jq -r '.identifier')
# Create sub-tasks with reference to parent
npx tsx scripts/issues/create.ts "Design dashboard layout (subtask of $PARENT_ID)" "$TEAM_ID" --json
npx tsx scripts/issues/create.ts "Implement data fetching (subtask of $PARENT_ID)" "$TEAM_ID" --json
npx tsx scripts/issues/create.ts "Add user settings (subtask of $PARENT_ID)" "$TEAM_ID" --jsonResponse: "Created parent issue ENG-126 and 3 related subtasks"
---
Example 9: Find and Update Issue
User request: "Find the login bug issue and mark it as urgent"
Execution:
# Get team and issues
TEAM_ID=$(npx tsx list-teams.ts --json | jq -r '.teams[0].id')
ISSUES=$(npx tsx scripts/issues/list.ts "$TEAM_ID" --json)
# Find issue with "login" in title
ISSUE_ID=$(echo "$ISSUES" | jq -r '.issues[] | select(.title | contains("login")) | .id' | head -1)
# Update priority to 1 (Urgent)
npx tsx scripts/issues/update.ts "$ISSUE_ID" --priority 1 --jsonResponse: "Found and marked issue ENG-115 as urgent"
---
Example 10: Weekly Status Update
User request: "Add weekly status comment to all In Progress issues"
Execution:
# Get current date
DATE=$(date +"%Y-%m-%d")
# Get team and issues
TEAM_ID=$(npx tsx list-teams.ts --json | jq -r '.teams[0].id')
ISSUES=$(npx tsx scripts/issues/list.ts "$TEAM_ID" --json)
# Get In Progress issue IDs
IN_PROGRESS_IDS=$(echo "$ISSUES" | jq -r '.issues[] | select(.status == "In Progress") | .id')
# Add weekly update comment
for ISSUE_ID in $IN_PROGRESS_IDS; do
npx tsx scripts/comments/create.ts "$ISSUE_ID" "Weekly update ($DATE): Still in progress" --json
doneResponse: "Added weekly status to 8 In Progress issues"
import { LinearClient } from "@linear/sdk";
import dotenv from "dotenv";
import { readFileSync, existsSync } from "fs";
import { homedir } from "os";
import { join } from "path";
/**
* Load Linear API key from multiple sources in order of priority:
* 1. Environment variable LINEAR_API_KEY (system-wide)
* 2. ~/.linear/credentials file
* 3. .env file in project root
*/
function loadLinearApiKey(): string {
// Priority 1: System environment variable
if (process.env.LINEAR_API_KEY) {
return process.env.LINEAR_API_KEY;
}
// Priority 2: ~/.linear/credentials file
const credentialsPath = join(homedir(), ".linear", "credentials");
if (existsSync(credentialsPath)) {
try {
const credentials = readFileSync(credentialsPath, "utf-8").trim();
if (credentials) {
return credentials;
}
} catch (error) {
// Ignore read errors and continue to next source
}
}
// Priority 3: .env file in project root
dotenv.config();
if (process.env.LINEAR_API_KEY) {
return process.env.LINEAR_API_KEY;
}
// No API key found in any source
throw new Error(
"LINEAR_API_KEY not found. Please set it via:\n" +
" 1. Environment variable: export LINEAR_API_KEY=your_key\n" +
" 2. Credentials file: ~/.linear/credentials\n" +
" 3. Project .env file\n\n" +
"Run 'npx tsx scripts/setup-credentials.ts' to configure."
);
}
const LINEAR_API_KEY = loadLinearApiKey();
// Create and export a shared Linear client instance
export const client = new LinearClient({ apiKey: LINEAR_API_KEY });
import { readFileSync, existsSync } from "fs";
import { resolve, dirname } from "path";
import { fileURLToPath } from "url";
export interface LinearConfig {
user: {
id: string;
name: string;
email: string;
};
defaults: {
teamId: string;
teamName: string;
teamKey: string;
projectId: string | null;
projectName: string | null;
autoAssign: boolean;
};
setupDate: string;
}
/**
* Load Linear configuration from .claude/linear-config.json
* Returns null if file doesn't exist or is invalid
*/
export function loadConfigIfExists(): LinearConfig | null {
try {
// Get the project root by going up from .claude/skills/linear/lib/
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Go up: lib/ -> linear/ -> skills/ -> .claude/ -> project root
const projectRoot = resolve(__dirname, "../../../..");
const configPath = resolve(projectRoot, ".claude/linear-config.json");
if (!existsSync(configPath)) {
return null;
}
const configContent = readFileSync(configPath, "utf-8");
const config = JSON.parse(configContent) as LinearConfig;
// Validate required fields
if (!config.defaults?.teamId) {
console.warn(
"Warning: Invalid config file - missing defaults.teamId"
);
return null;
}
return config;
} catch (error) {
// Silently return null on any error (file doesn't exist, invalid JSON, etc.)
return null;
}
}
/**
* Load Linear configuration or exit with error
* Use this when config is required
*/
export function loadConfigOrExit(): LinearConfig {
const config = loadConfigIfExists();
if (!config) {
console.error("Error: Linear configuration not found");
console.error("");
console.error("Please run the setup first:");
console.error(" /linear:setup");
console.error("");
console.error("Or use explicit parameters (see --help)");
process.exit(1);
}
return config;
}
/**
* Get team ID from arguments or config
* @param argIndex - The process.argv index where teamId should be
* @returns teamId or exits with error
*/
export function getTeamIdOrExit(argIndex: number): string {
let teamId = process.argv[argIndex];
// If argument is a flag (starts with --) or undefined, try to use config
if (!teamId || teamId.startsWith("--")) {
const config = loadConfigIfExists();
teamId = config?.defaults?.teamId;
if (!teamId) {
console.error("Error: No team ID provided");
console.error("");
console.error("Option 1: Provide team ID explicitly");
console.error(" Example: npx tsx script.ts <teamId>");
console.error("");
console.error("Option 2: Run setup to configure defaults");
console.error(" /linear:setup");
process.exit(1);
}
console.error(`ℹ️ Using team from config: ${config.defaults.teamName}`);
}
return teamId;
}
import { spawn } from "child_process";
/**
* Check if --json flag is present in command line arguments
*/
export function hasJsonFlag(): boolean {
return process.argv.includes("--json");
}
/**
* Output data with jq formatting if available and --json flag is not set.
* If --json flag is present, outputs clean JSON without jq or messages.
*/
export function outputWithJq(data: unknown): void {
const jsonString = JSON.stringify(data, null, 2);
// If --json flag is present, output clean JSON and exit
if (hasJsonFlag()) {
console.log(jsonString);
return;
}
// Check if jq is available
const jq = spawn("jq", ["."], { stdio: ["pipe", "inherit", "inherit"] });
jq.on("error", () => {
// jq not available, fallback to plain JSON
console.log(jsonString);
console.error("\nNote: Install jq for colorized output: brew install jq");
});
jq.stdin.write(jsonString);
jq.stdin.end();
}
{
"name": "linear-issues-scripts",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "linear-issues-scripts",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@linear/sdk": "^68.1.0",
"dotenv": "^17.2.3"
},
"devDependencies": {
"@types/node": "^25.0.3",
"tsx": "^4.21.0",
"typescript": "^5.9.3"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
"integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
"integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
"integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
"integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
"integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
"integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
"integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
"integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
"integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
"integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
"integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
"integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
"integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
"integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
"integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
"integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
"integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
"integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
"integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
"integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
"integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
"integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
"integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
"integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
"integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
"integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@graphql-typed-document-node/core": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
"integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==",
"license": "MIT",
"peerDependencies": {
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
}
},
"node_modules/@linear/sdk": {
"version": "68.1.0",
"resolved": "https://registry.npmjs.org/@linear/sdk/-/sdk-68.1.0.tgz",
"integrity": "sha512-I6MMle0hXR8SC2KVaDHt+vQvGDSQQwhfA9eq17hAL1y1Sf+7aGpaCXzMx3S4wcD9fLS5LZxd7aY8nAiTYHjoEg==",
"license": "MIT",
"dependencies": {
"@graphql-typed-document-node/core": "^3.1.0",
"graphql": "^15.4.0"
},
"engines": {
"node": ">=18.x"
}
},
"node_modules/@types/node": {
"version": "25.0.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/dotenv": {
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/esbuild": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.2",
"@esbuild/android-arm": "0.27.2",
"@esbuild/android-arm64": "0.27.2",
"@esbuild/android-x64": "0.27.2",
"@esbuild/darwin-arm64": "0.27.2",
"@esbuild/darwin-x64": "0.27.2",
"@esbuild/freebsd-arm64": "0.27.2",
"@esbuild/freebsd-x64": "0.27.2",
"@esbuild/linux-arm": "0.27.2",
"@esbuild/linux-arm64": "0.27.2",
"@esbuild/linux-ia32": "0.27.2",
"@esbuild/linux-loong64": "0.27.2",
"@esbuild/linux-mips64el": "0.27.2",
"@esbuild/linux-ppc64": "0.27.2",
"@esbuild/linux-riscv64": "0.27.2",
"@esbuild/linux-s390x": "0.27.2",
"@esbuild/linux-x64": "0.27.2",
"@esbuild/netbsd-arm64": "0.27.2",
"@esbuild/netbsd-x64": "0.27.2",
"@esbuild/openbsd-arm64": "0.27.2",
"@esbuild/openbsd-x64": "0.27.2",
"@esbuild/openharmony-arm64": "0.27.2",
"@esbuild/sunos-x64": "0.27.2",
"@esbuild/win32-arm64": "0.27.2",
"@esbuild/win32-ia32": "0.27.2",
"@esbuild/win32-x64": "0.27.2"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/get-tsconfig": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
"funding": {
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/graphql": {
"version": "15.10.1",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-15.10.1.tgz",
"integrity": "sha512-BL/Xd/T9baO6NFzoMpiMD7YUZ62R6viR5tp/MULVEnbYJXZA//kRNW7J0j1w/wXArgL0sCxhDfK5dczSKn3+cg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 10.x"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/tsx": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT"
}
}
}
{
"name": "linear-issues-scripts",
"version": "1.0.0",
"description": "Scripts to retrieve Linear issues using the TypeScript SDK",
"type": "module",
"scripts": {
"setup": "tsx scripts/setup-credentials.ts",
"list-teams": "tsx list-teams.ts",
"get-issues": "tsx get-issues.ts"
},
"keywords": [
"linear",
"issues",
"sdk"
],
"author": "",
"license": "ISC",
"dependencies": {
"@linear/sdk": "^68.1.0",
"dotenv": "^17.2.3"
},
"devDependencies": {
"@types/node": "^25.0.3",
"tsx": "^4.21.0",
"typescript": "^5.9.3"
}
}
Linear Operations - Quick Reference
Command Summary
| Operation | Command (Named Flags - Recommended) |
|---|---|
| Create issue | npx tsx scripts/issues/create.ts --title "title" [options] --json |
| Update issue | npx tsx scripts/issues/update.ts --issue ABC-123 [options] --json |
| List issues | npx tsx scripts/issues/list.ts [teamId] --json |
| Get issue | npx tsx scripts/issues/get.ts ABC-123 --json |
| Archive issue | npx tsx scripts/issues/archive.ts ABC-123 --json |
| List teams | npx tsx scripts/list-teams.ts --json |
| List statuses | npx tsx scripts/status/list.ts [teamId] --json |
| Update status | npx tsx scripts/status/update.ts ABC-123 <stateId> --json |
| Create comment | npx tsx scripts/comments/create.ts ABC-123 "text" --json |
| List comments | npx tsx scripts/comments/list.ts ABC-123 --json |
| List labels | npx tsx scripts/labels/list.ts [teamId] --json |
| Create label | npx tsx scripts/labels/create.ts "name" [teamId] --json |
| Add label | npx tsx scripts/labels/add-to-issue.ts ABC-123 <labelId> --json |
| Current user | npx tsx scripts/users/me.ts --json |
| List users | npx tsx scripts/users/list.ts [teamId] --json |
Create Issue Options
# Named flags (recommended)
npx tsx scripts/issues/create.ts --title "Issue title" [options] --json
# Legacy positional (backwards compatible)
npx tsx scripts/issues/create.ts "Issue title" [teamId] [options] --json| Option | Type | Description |
|---|---|---|
--title <text> | String | Issue title (required) |
--teamId <id> | ID | Team ID (optional if configured) |
--description <text> | String | Issue description (markdown supported) |
--state <id> | ID | Initial workflow state ID |
--priority <0-4> | Int | Priority level |
--assignee <id> | ID | User ID to assign |
--labels <ids> | IDs | Comma-separated label IDs |
--project <id> | ID | Project ID |
--estimate <points> | Int | Point estimate |
--due <YYYY-MM-DD> | Date | Due date |
--parent <id> | ID | Parent issue ID or identifier (e.g., AA-123) for sub-issues |
--cycle <id> | ID | Cycle/sprint ID |
Update Issue Options
# Named flags (recommended)
npx tsx scripts/issues/update.ts --issue ABC-123 [options] --json
# Legacy positional (backwards compatible)
npx tsx scripts/issues/update.ts ABC-123 [options] --json| Option | Type | Description |
|---|---|---|
--issue <id> | ID | Issue ID or identifier (required) |
--title <text> | String | New title |
--description <text> | String | New description (markdown) |
--state <id> | ID | Workflow state ID |
--priority <0-4> | Int | Priority level |
--assignee <id> | ID/null | User ID (none to unassign) |
--labels <ids> | IDs | Label IDs (replaces existing) |
--project <id> | ID/null | Project ID (none to remove) |
--estimate <points> | Int/null | Point estimate (none to clear) |
--due <YYYY-MM-DD> | Date/null | Due date (none to clear) |
--parent <id> | ID/null | Parent issue ID or identifier (e.g., AA-123) (none to remove) |
--cycle <id> | ID/null | Cycle/sprint ID (none to remove) |
Priority Levels
0- No priority1- Urgent (🔴)2- High (🟠)3- Medium (🟡)4- Low (🔵)
Mentions & Embeds
Use URLs in description fields - Linear auto-converts them:
| Type | Format | Result |
|---|---|---|
| User mention | https://linear.app/workspace/profiles/username | @username |
| Issue mention | https://linear.app/workspace/issue/AA-123 | Clickable AA-123 |
| YouTube | https://www.youtube.com/watch?v=... | Embedded video |
| Figma | https://www.figma.com/file/... | Embedded design |
| Loom | https://www.loom.com/share/... | Embedded video |
Note: @username syntax only works in UI, not API. Use profile URLs.
Common JQ Patterns
# Extract team ID by name
jq -r '.teams[] | select(.name == "Engineering") | .id'
# Extract issue IDs
jq -r '.issues[].id'
# Filter urgent issues
jq '.issues[] | select(.priority == 1)'
# Count issues
jq '.issues | length'
# Get first team
jq -r '.teams[0].id'
# Extract identifier (AA-123)
jq -r '.identifier'
# Get status name
jq -r '.status'Bash Patterns
# Store result in variable
RESULT=$(npx tsx scripts/issues/list.ts --json)
# Extract field
TEAM_ID=$(echo $RESULT | jq -r '.teams[0].id')
# Loop through items
for ID in $(echo $RESULT | jq -r '.issues[].id'); do
# do something with $ID
done
# Conditional execution (named flags)
if npx tsx scripts/issues/create.ts --title "Bug fix" --json; then
echo "Success"
fi
# Chain commands
TEAM_ID=$(npx tsx scripts/list-teams.ts --json | jq -r '.teams[0].id') && \
npx tsx scripts/issues/list.ts $TEAM_ID --json
# Create and update issue
ISSUE=$(npx tsx scripts/issues/create.ts --title "New task" --json)
ISSUE_ID=$(echo $ISSUE | jq -r '.identifier')
npx tsx scripts/issues/update.ts --issue $ISSUE_ID --priority 2 --jsonTypical Response Structures
Teams:
{
"teams": [
{"id": "uuid", "name": "Engineering", "key": "ENG"}
],
"projects": [
{"id": "uuid", "name": "Q1 Project", "teamId": "uuid"}
]
}Issues:
{
"issues": [
{
"id": "uuid",
"identifier": "ENG-123",
"title": "Bug fix",
"status": "In Progress",
"priority": 1,
"priorityLabel": "Urgent",
"url": "https://linear.app/..."
}
]
}Issue Details:
{
"id": "uuid",
"identifier": "ENG-123",
"title": "Bug fix",
"description": "Full description...",
"status": "In Progress",
"priority": 1,
"priorityLabel": "Urgent",
"assignee": {"id": "...", "name": "...", "email": "..."},
"creator": {"id": "...", "name": "...", "email": "..."},
"url": "https://linear.app/...",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-02T00:00:00.000Z"
}Statuses:
{
"statuses": [
{
"id": "uuid",
"name": "In Progress",
"type": "started",
"color": "#f2c94c",
"position": 2
}
]
}Comments:
{
"comments": [
{
"id": "uuid",
"body": "Comment text",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z",
"user": {"id": "...", "name": "...", "email": "..."}
}
]
}Error Messages
| Error | Cause | Solution |
|---|---|---|
| LINEAR_API_KEY not found | No credentials | Run npx tsx scripts/setup-credentials.ts |
| Module not found | Missing dependencies | Run npm install |
| Issue not found | Invalid ID | Check issue ID/identifier |
| Team not found | Invalid team ID | Use list-teams.ts to get valid IDs |
| Permission denied | File permissions | Run chmod 600 ~/.linear/credentials |
Authentication Locations
Priority order (first found is used):
1. Environment variable: $LINEAR_API_KEY 2. User credentials: ~/.linear/credentials 3. Project .env: .env in project root
File Structure
.
├── scripts/
│ ├── issues/ # Issue operations
│ ├── comments/ # Comment operations
│ ├── status/ # Status operations
│ ├── labels/ # Label operations
│ └── users/ # User operations
├── scripts/
│ └── setup-credentials.ts # Setup wizard
├── lib/
│ ├── client.ts # Linear client
│ └── output.ts # Output formatting
├── list-teams.ts # List teams/projects
└── get-issues.ts # Get issues (legacy)import { client } from "../../lib/client.js";
import { outputWithJq } from "../../lib/output.js";
async function createComment() {
const issueId = process.argv[2];
const body = process.argv[3];
if (!issueId || !body) {
console.error("Error: Please provide issue ID and comment body");
console.error(
"Usage: npx tsx scripts/comments/create.ts <issueId> <body>"
);
process.exit(1);
}
const commentPayload = await client.createComment({
issueId,
body,
});
const comment = await commentPayload.comment;
if (!comment) {
console.error("Failed to create comment");
process.exit(1);
}
const user = await comment.user;
outputWithJq({
id: comment.id,
body: comment.body,
createdAt: comment.createdAt,
user: user
? {
id: user.id,
name: user.name,
email: user.email,
}
: null,
});
}
createComment().catch((error) => {
console.error("Error creating comment:", error.message);
process.exit(1);
});
import { client } from "../../lib/client.js";
import { outputWithJq } from "../../lib/output.js";
async function listComments() {
const issueId = process.argv[2];
if (!issueId) {
console.error("Error: Please provide issue ID");
console.error("Usage: npx tsx scripts/comments/list.ts <issueId>");
process.exit(1);
}
const issue = await client.issue(issueId);
if (!issue) {
console.error("Issue not found");
process.exit(1);
}
const commentsResponse = await issue.comments();
const comments = await Promise.all(
commentsResponse.nodes.map(async (comment) => {
const user = await comment.user;
return {
id: comment.id,
body: comment.body,
createdAt: comment.createdAt,
updatedAt: comment.updatedAt,
user: user
? {
id: user.id,
name: user.name,
email: user.email,
}
: null,
};
})
);
outputWithJq({ comments });
}
listComments().catch((error) => {
console.error("Error listing comments:", error.message);
process.exit(1);
});
import { client } from "../../lib/client.js";
import { outputWithJq } from "../../lib/output.js";
async function archiveIssue() {
const issueId = process.argv[2];
if (!issueId) {
console.error("Error: Please provide issue ID");
console.error("Usage: npx tsx scripts/issues/archive.ts <issueId>");
process.exit(1);
}
const issuePayload = await client.archiveIssue(issueId);
const success = issuePayload.success;
if (!success) {
console.error("Failed to archive issue");
process.exit(1);
}
outputWithJq({
success: true,
message: "Issue archived successfully",
});
}
archiveIssue().catch((error) => {
console.error("Error archiving issue:", error.message);
process.exit(1);
});
import { client } from "../../lib/client.js";
import { outputWithJq } from "../../lib/output.js";
import { getTeamIdOrExit } from "../../lib/config.js";
/**
* Check if a string looks like an issue identifier (e.g., AA-123, ENG-456)
*/
function isIssueIdentifier(str: string): boolean {
return /^[A-Z]+-\d+$/i.test(str);
}
/**
* Resolve an issue identifier (AA-123) to its UUID.
* If already a UUID, returns as-is.
*/
async function resolveIssueId(idOrIdentifier: string): Promise<string> {
if (!isIssueIdentifier(idOrIdentifier)) {
// Assume it's already a UUID
return idOrIdentifier;
}
// Look up the issue by identifier
const issue = await client.issue(idOrIdentifier);
if (!issue) {
throw new Error(`Issue not found: ${idOrIdentifier}`);
}
return issue.id;
}
interface CreateOptions {
title: string;
teamId: string;
description?: string;
stateId?: string;
priority?: number;
assigneeId?: string;
labelIds?: string[];
projectId?: string;
estimate?: number;
dueDate?: string;
parentId?: string;
cycleId?: string;
}
function printUsage() {
console.error("Usage: npx tsx scripts/issues/create.ts --title <title> [options] --json");
console.error(" npx tsx scripts/issues/create.ts <title> [teamId] [options] --json (legacy)");
console.error("");
console.error("Options:");
console.error(" --title <text> Issue title (required)");
console.error(" --teamId <id> Team ID (optional if configured)");
console.error(" --description <text> Issue description (markdown supported)");
console.error(" --state <id> Initial workflow state ID");
console.error(" --priority <0-4> Priority: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low");
console.error(" --assignee <id> User ID to assign the issue to");
console.error(" --labels <ids> Comma-separated label IDs");
console.error(" --project <id> Project ID to associate with");
console.error(" --estimate <points> Point estimate");
console.error(" --due <YYYY-MM-DD> Due date");
console.error(" --parent <id> Parent issue ID or identifier (e.g., AA-123) for sub-issues");
console.error(" --cycle <id> Cycle/sprint ID");
console.error(" --json Output as JSON (recommended for scripting)");
console.error("");
console.error("Examples:");
console.error(" npx tsx scripts/issues/create.ts --title \"Fix login bug\" --json");
console.error(" npx tsx scripts/issues/create.ts --title \"New feature\" --priority 2 --assignee user-123 --json");
console.error(" npx tsx scripts/issues/create.ts \"Fix login bug\" --json (legacy positional)");
}
function getArgValue(args: string[], flag: string): string | undefined {
const index = args.indexOf(flag);
if (index !== -1 && args[index + 1] && !args[index + 1].startsWith("--")) {
return args[index + 1];
}
return undefined;
}
function parseArgs(): CreateOptions | null {
const args = process.argv.slice(2);
if (args.length === 0) {
console.error("Error: Please provide issue title");
console.error("Use --title \"title\" or provide as first argument");
console.error("");
printUsage();
return null;
}
let title: string | undefined;
let teamIdArg: string | undefined;
let optionsStartIndex = 0;
// Check for --title flag (new named pattern - recommended)
title = getArgValue(args, "--title");
// Check for --teamId flag
teamIdArg = getArgValue(args, "--teamId");
// If no --title flag, check for positional title (legacy backwards compatibility)
if (!title && args[0] && !args[0].startsWith("--")) {
title = args[0];
optionsStartIndex = 1;
// Check for positional teamId (legacy)
if (!teamIdArg && args[1] && !args[1].startsWith("--")) {
teamIdArg = args[1];
optionsStartIndex = 2;
}
}
if (!title) {
console.error("Error: Please provide issue title");
console.error("Use --title \"title\" or provide as first argument");
console.error("");
printUsage();
return null;
}
// Get teamId from arg or config
const teamId = teamIdArg || getTeamIdOrExit(undefined);
const options: CreateOptions = { title, teamId };
// Parse named options
for (let i = optionsStartIndex; i < args.length; i++) {
const arg = args[i];
const nextArg = args[i + 1];
// Skip already-processed flags
if (arg === "--title" || arg === "--teamId") {
if (nextArg && !nextArg.startsWith("--")) {
i++; // Skip the value too
}
continue;
}
switch (arg) {
case "--description":
if (nextArg && !nextArg.startsWith("--")) {
options.description = nextArg;
i++;
}
break;
case "--state":
if (nextArg && !nextArg.startsWith("--")) {
options.stateId = nextArg;
i++;
}
break;
case "--priority":
if (nextArg && !nextArg.startsWith("--")) {
const priority = parseInt(nextArg);
if (priority >= 0 && priority <= 4) {
options.priority = priority;
} else {
console.error("Error: Priority must be 0-4");
return null;
}
i++;
}
break;
case "--assignee":
if (nextArg && !nextArg.startsWith("--")) {
options.assigneeId = nextArg;
i++;
}
break;
case "--labels":
if (nextArg && !nextArg.startsWith("--")) {
options.labelIds = nextArg.split(",").map(id => id.trim());
i++;
}
break;
case "--project":
if (nextArg && !nextArg.startsWith("--")) {
options.projectId = nextArg;
i++;
}
break;
case "--estimate":
if (nextArg && !nextArg.startsWith("--")) {
options.estimate = parseInt(nextArg);
i++;
}
break;
case "--due":
if (nextArg && !nextArg.startsWith("--")) {
options.dueDate = nextArg;
i++;
}
break;
case "--parent":
if (nextArg && !nextArg.startsWith("--")) {
options.parentId = nextArg;
i++;
}
break;
case "--cycle":
if (nextArg && !nextArg.startsWith("--")) {
options.cycleId = nextArg;
i++;
}
break;
case "--json":
// Handled by outputWithJq
break;
default:
// Ignore unknown flags
break;
}
}
return options;
}
async function createIssue() {
const options = parseArgs();
if (!options) {
process.exit(1);
}
// Resolve parentId if it's an identifier (AA-123) to UUID
let resolvedParentId: string | undefined;
if (options.parentId) {
resolvedParentId = await resolveIssueId(options.parentId);
}
// Build the input object with only defined fields
const input = {
title: options.title,
teamId: options.teamId,
...(options.description !== undefined && { description: options.description }),
...(options.stateId !== undefined && { stateId: options.stateId }),
...(options.priority !== undefined && { priority: options.priority }),
...(options.assigneeId !== undefined && { assigneeId: options.assigneeId }),
...(options.labelIds !== undefined && { labelIds: options.labelIds }),
...(options.projectId !== undefined && { projectId: options.projectId }),
...(options.estimate !== undefined && { estimate: options.estimate }),
...(options.dueDate !== undefined && { dueDate: options.dueDate }),
...(resolvedParentId !== undefined && { parentId: resolvedParentId }),
...(options.cycleId !== undefined && { cycleId: options.cycleId }),
};
const issuePayload = await client.createIssue(input);
const issue = await issuePayload.issue;
if (!issue) {
console.error("Failed to create issue");
process.exit(1);
}
// Fetch additional details for output
const state = await issue.state;
const assignee = await issue.assignee;
const project = await issue.project;
const parent = await issue.parent;
outputWithJq({
id: issue.id,
identifier: issue.identifier,
title: issue.title,
description: issue.description,
url: issue.url,
priority: issue.priority,
priorityLabel: issue.priorityLabel,
estimate: issue.estimate,
dueDate: issue.dueDate,
state: state ? { id: state.id, name: state.name } : null,
assignee: assignee ? { id: assignee.id, name: assignee.name, email: assignee.email } : null,
project: project ? { id: project.id, name: project.name } : null,
parent: parent ? { id: parent.id, identifier: parent.identifier } : null,
createdAt: issue.createdAt,
});
}
createIssue().catch((error) => {
console.error("Error creating issue:", error.message);
process.exit(1);
});
import { client } from "../../lib/client.js";
import { outputWithJq } from "../../lib/output.js";
async function getIssue() {
const issueId = process.argv[2];
if (!issueId) {
console.error("Error: Please provide issue ID or identifier");
console.error(
"Usage: npx tsx scripts/issues/get.ts <issueId or identifier>"
);
process.exit(1);
}
// Check if it's an identifier (e.g., AA-123) or UUID
let issue;
if (issueId.includes("-") && issueId.split("-")[0].match(/^[A-Z]+$/)) {
// It's an identifier like AA-123
const issueResult = await client.issue(issueId);
issue = issueResult;
} else {
// It's a UUID
const issueResult = await client.issue(issueId);
issue = issueResult;
}
if (!issue) {
console.error("Issue not found");
process.exit(1);
}
const state = await issue.state;
const assignee = await issue.assignee;
const creator = await issue.creator;
outputWithJq({
id: issue.id,
identifier: issue.identifier,
title: issue.title,
description: issue.description,
status: state?.name || "Unknown",
priority: issue.priority,
priorityLabel: issue.priorityLabel,
assignee: assignee
? {
id: assignee.id,
name: assignee.name,
email: assignee.email,
}
: null,
creator: creator
? {
id: creator.id,
name: creator.name,
email: creator.email,
}
: null,
url: issue.url,
createdAt: issue.createdAt,
updatedAt: issue.updatedAt,
});
}
getIssue().catch((error) => {
console.error("Error fetching issue:", error.message);
process.exit(1);
});
import { client } from "../../lib/client.js";
import { outputWithJq } from "../../lib/output.js";
import { getTeamIdOrExit } from "../../lib/config.js";
interface ListOptions {
mine: boolean;
unassigned: boolean;
status: string | null;
}
function parseArgs(): ListOptions {
const args = process.argv.slice(2);
return {
mine: args.includes("--mine"),
unassigned: args.includes("--unassigned"),
status: args.find((a) => a.startsWith("--status="))?.split("=")[1] || null,
};
}
async function listIssues() {
const teamId = getTeamIdOrExit(2);
const options = parseArgs();
const viewer = await client.viewer;
// Build filter based on options
const filter: Record<string, unknown> = {
team: { id: { eq: teamId } },
};
// Only filter by assignee if --mine flag is passed
if (options.mine) {
filter.assignee = { id: { eq: viewer.id } };
} else if (options.unassigned) {
filter.assignee = { null: true };
}
// Filter by status name if provided
if (options.status) {
filter.state = { name: { eqIgnoreCase: options.status } };
}
const issuesResponse = await client.issues({
filter,
first: 50,
});
const issues = await Promise.all(
issuesResponse.nodes.map(async (issue) => ({
id: issue.id,
identifier: issue.identifier,
title: issue.title,
status: (await issue.state)?.name || "Unknown",
priority: issue.priority,
priorityLabel: issue.priorityLabel,
assignee: (await issue.assignee)?.name || null,
url: issue.url,
}))
);
outputWithJq({ issues });
}
listIssues().catch((error) => {
console.error("Error listing issues:", error.message);
process.exit(1);
});
import { client } from "../../lib/client.js";
import { outputWithJq } from "../../lib/output.js";
/**
* Check if a string looks like an issue identifier (e.g., AA-123, ENG-456)
*/
function isIssueIdentifier(str: string): boolean {
return /^[A-Z]+-\d+$/i.test(str);
}
/**
* Resolve an issue identifier (AA-123) to its UUID.
* If already a UUID, returns as-is.
*/
async function resolveIssueId(idOrIdentifier: string): Promise<string> {
if (!isIssueIdentifier(idOrIdentifier)) {
// Assume it's already a UUID
return idOrIdentifier;
}
// Look up the issue by identifier
const issue = await client.issue(idOrIdentifier);
if (!issue) {
throw new Error(`Issue not found: ${idOrIdentifier}`);
}
return issue.id;
}
interface UpdateOptions {
title?: string;
description?: string;
stateId?: string;
priority?: number;
assigneeId?: string | null;
labelIds?: string[];
projectId?: string | null;
estimate?: number | null;
dueDate?: string | null;
parentId?: string | null;
cycleId?: string | null;
}
function printUsage() {
console.error("Usage: npx tsx scripts/issues/update.ts --issue <issueId> [options] --json");
console.error(" npx tsx scripts/issues/update.ts <issueId> [options] --json (legacy)");
console.error("");
console.error("Options:");
console.error(" --issue <id> Issue ID or identifier (e.g., ABC-123) (required)");
console.error(" --title <text> New title");
console.error(" --description <text> New description (markdown supported)");
console.error(" --state <id> Workflow state ID");
console.error(" --priority <0-4> Priority: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low");
console.error(" --assignee <id> User ID (use 'none' to unassign)");
console.error(" --labels <ids> Comma-separated label IDs (replaces existing)");
console.error(" --project <id> Project ID (use 'none' to remove)");
console.error(" --estimate <points> Point estimate (use 'none' to clear)");
console.error(" --due <YYYY-MM-DD> Due date (use 'none' to clear)");
console.error(" --parent <id> Parent issue ID or identifier (e.g., AA-123) (use 'none' to remove)");
console.error(" --cycle <id> Cycle/sprint ID (use 'none' to remove)");
console.error(" --json Output as JSON (recommended for scripting)");
console.error("");
console.error("Examples:");
console.error(" npx tsx scripts/issues/update.ts --issue ABC-123 --priority 1 --json");
console.error(" npx tsx scripts/issues/update.ts --issue ABC-123 --state state-id --assignee user-id --json");
console.error(" npx tsx scripts/issues/update.ts ABC-123 --assignee none --json (legacy positional)");
}
function getArgValue(args: string[], flag: string): string | undefined {
const index = args.indexOf(flag);
if (index !== -1 && args[index + 1] && !args[index + 1].startsWith("--")) {
return args[index + 1];
}
return undefined;
}
function parseArgs(): { issueId: string; updates: UpdateOptions } | null {
const args = process.argv.slice(2);
if (args.length === 0) {
console.error("Error: Please provide issue ID");
console.error("Use --issue ABC-123 or provide as first argument");
console.error("");
printUsage();
return null;
}
let issueId: string | undefined;
let optionsStartIndex = 0;
// Check for --issue flag (new named pattern - recommended)
issueId = getArgValue(args, "--issue");
// If no --issue flag, check for positional issueId (legacy backwards compatibility)
if (!issueId && args[0] && !args[0].startsWith("--")) {
issueId = args[0];
optionsStartIndex = 1;
}
if (!issueId) {
console.error("Error: Please provide issue ID");
console.error("Use --issue ABC-123 or provide as first argument");
console.error("");
printUsage();
return null;
}
const updates: UpdateOptions = {};
// Parse named options
for (let i = optionsStartIndex; i < args.length; i++) {
const arg = args[i];
const nextArg = args[i + 1];
// Skip already-processed --issue flag
if (arg === "--issue") {
if (nextArg && !nextArg.startsWith("--")) {
i++; // Skip the value too
}
continue;
}
switch (arg) {
case "--title":
if (nextArg && !nextArg.startsWith("--")) {
updates.title = nextArg;
i++;
}
break;
case "--description":
if (nextArg && !nextArg.startsWith("--")) {
updates.description = nextArg;
i++;
}
break;
case "--state":
if (nextArg && !nextArg.startsWith("--")) {
updates.stateId = nextArg;
i++;
}
break;
case "--priority":
if (nextArg && !nextArg.startsWith("--")) {
const priority = parseInt(nextArg);
if (priority >= 0 && priority <= 4) {
updates.priority = priority;
} else {
console.error("Error: Priority must be 0-4");
return null;
}
i++;
}
break;
case "--assignee":
if (nextArg && !nextArg.startsWith("--")) {
updates.assigneeId = nextArg === "none" || nextArg === "null" ? null : nextArg;
i++;
}
break;
case "--labels":
if (nextArg && !nextArg.startsWith("--")) {
updates.labelIds = nextArg.split(",").map(id => id.trim());
i++;
}
break;
case "--project":
if (nextArg && !nextArg.startsWith("--")) {
updates.projectId = nextArg === "none" || nextArg === "null" ? null : nextArg;
i++;
}
break;
case "--estimate":
if (nextArg && !nextArg.startsWith("--")) {
updates.estimate = nextArg === "none" || nextArg === "null" ? null : parseInt(nextArg);
i++;
}
break;
case "--due":
if (nextArg && !nextArg.startsWith("--")) {
updates.dueDate = nextArg === "none" || nextArg === "null" ? null : nextArg;
i++;
}
break;
case "--parent":
if (nextArg && !nextArg.startsWith("--")) {
updates.parentId = nextArg === "none" || nextArg === "null" ? null : nextArg;
i++;
}
break;
case "--cycle":
if (nextArg && !nextArg.startsWith("--")) {
updates.cycleId = nextArg === "none" || nextArg === "null" ? null : nextArg;
i++;
}
break;
case "--json":
// Handled by outputWithJq
break;
default:
// Ignore unknown flags
break;
}
}
if (Object.keys(updates).length === 0) {
console.error("Error: No updates provided");
console.error("");
printUsage();
return null;
}
return { issueId, updates };
}
async function updateIssue() {
const parsed = parseArgs();
if (!parsed) {
process.exit(1);
}
const { issueId, updates } = parsed;
// Resolve parentId if it's an identifier (AA-123) to UUID
// Skip resolution if null (removing parent)
let resolvedParentId: string | null | undefined = updates.parentId;
if (updates.parentId !== undefined && updates.parentId !== null) {
resolvedParentId = await resolveIssueId(updates.parentId);
}
// Build the input object with only defined fields
const input = {
...(updates.title !== undefined && { title: updates.title }),
...(updates.description !== undefined && { description: updates.description }),
...(updates.stateId !== undefined && { stateId: updates.stateId }),
...(updates.priority !== undefined && { priority: updates.priority }),
...(updates.assigneeId !== undefined && { assigneeId: updates.assigneeId }),
...(updates.labelIds !== undefined && { labelIds: updates.labelIds }),
...(updates.projectId !== undefined && { projectId: updates.projectId }),
...(updates.estimate !== undefined && { estimate: updates.estimate }),
...(updates.dueDate !== undefined && { dueDate: updates.dueDate }),
...(resolvedParentId !== undefined && { parentId: resolvedParentId }),
...(updates.cycleId !== undefined && { cycleId: updates.cycleId }),
};
const issuePayload = await client.updateIssue(issueId, input);
const issue = await issuePayload.issue;
if (!issue) {
console.error("Failed to update issue");
process.exit(1);
}
// Fetch additional details for output
const state = await issue.state;
const assignee = await issue.assignee;
const project = await issue.project;
const parent = await issue.parent;
const labels = await issue.labels();
outputWithJq({
id: issue.id,
identifier: issue.identifier,
title: issue.title,
description: issue.description,
url: issue.url,
priority: issue.priority,
priorityLabel: issue.priorityLabel,
estimate: issue.estimate,
dueDate: issue.dueDate,
state: state ? { id: state.id, name: state.name } : null,
assignee: assignee ? { id: assignee.id, name: assignee.name, email: assignee.email } : null,
project: project ? { id: project.id, name: project.name } : null,
parent: parent ? { id: parent.id, identifier: parent.identifier } : null,
labels: labels.nodes.map(l => ({ id: l.id, name: l.name })),
updatedAt: issue.updatedAt,
});
}
updateIssue().catch((error) => {
console.error("Error updating issue:", error.message);
process.exit(1);
});
import { client } from "../../lib/client.js";
import { outputWithJq } from "../../lib/output.js";
async function addLabelToIssue() {
const issueId = process.argv[2];
const labelId = process.argv[3];
if (!issueId || !labelId) {
console.error("Error: Please provide issue ID and label ID");
console.error(
"Usage: npx tsx scripts/labels/add-to-issue.ts <issueId> <labelId>"
);
console.error("Tip: Use scripts/labels/list.ts to see available labels");
process.exit(1);
}
// Get current labels
const issue = await client.issue(issueId);
if (!issue) {
console.error("Issue not found");
process.exit(1);
}
const currentLabels = await issue.labels();
const currentLabelIds = currentLabels.nodes.map((label) => label.id);
// Add new label if not already present
if (currentLabelIds.includes(labelId)) {
console.error("Label already attached to issue");
process.exit(1);
}
const updatedLabelIds = [...currentLabelIds, labelId];
const issuePayload = await client.updateIssue(issueId, {
labelIds: updatedLabelIds,
});
const updatedIssue = await issuePayload.issue;
if (!updatedIssue) {
console.error("Failed to add label to issue");
process.exit(1);
}
const labels = await updatedIssue.labels();
outputWithJq({
id: updatedIssue.id,
identifier: updatedIssue.identifier,
title: updatedIssue.title,
labels: labels.nodes.map((label) => ({
id: label.id,
name: label.name,
color: label.color,
})),
});
}
addLabelToIssue().catch((error) => {
console.error("Error adding label to issue:", error.message);
process.exit(1);
});
import { client } from "../../lib/client.js";
import { outputWithJq } from "../../lib/output.js";
import { getTeamIdOrExit } from "../../lib/config.js";
async function createLabel() {
const name = process.argv[2];
if (!name) {
console.error("Error: Please provide label name");
console.error("");
console.error("Usage: npx tsx scripts/labels/create.ts <name> [teamId] [color]");
console.error("");
console.error("If teamId is omitted, uses default from .claude/linear-config.json");
process.exit(1);
}
const teamId = getTeamIdOrExit(3);
const color = process.argv[4];
const labelPayload = await client.createIssueLabel({
name,
teamId,
...(color && { color }),
});
const label = await labelPayload.issueLabel;
if (!label) {
console.error("Failed to create label");
process.exit(1);
}
outputWithJq({
id: label.id,
name: label.name,
color: label.color,
description: label.description,
});
}
createLabel().catch((error) => {
console.error("Error creating label:", error.message);
process.exit(1);
});
import { client } from "../../lib/client.js";
import { outputWithJq } from "../../lib/output.js";
import { getTeamIdOrExit } from "../../lib/config.js";
async function listLabels() {
const teamId = getTeamIdOrExit(2);
const team = await client.team(teamId);
if (!team) {
console.error("Team not found");
process.exit(1);
}
const labelsResponse = await team.labels();
const labels = labelsResponse.nodes.map((label) => ({
id: label.id,
name: label.name,
color: label.color,
description: label.description,
}));
outputWithJq({ labels });
}
listLabels().catch((error) => {
console.error("Error listing labels:", error.message);
process.exit(1);
});
import { client } from "../lib/client.js";
import { outputWithJq } from "../lib/output.js";
interface TeamOutput {
id: string;
name: string;
key: string;
}
interface ProjectOutput {
id: string;
name: string;
teamId: string | null;
}
interface Output {
teams: TeamOutput[];
projects: ProjectOutput[];
}
async function listTeamsAndProjects(): Promise<Output> {
// Fetch teams
const teamsResponse = await client.teams();
const teams: TeamOutput[] = teamsResponse.nodes.map((team) => ({
id: team.id,
name: team.name,
key: team.key,
}));
// Fetch projects
const projectsResponse = await client.projects();
const projects: ProjectOutput[] = await Promise.all(
projectsResponse.nodes.map(async (project) => {
const teams = await project.teams();
return {
id: project.id,
name: project.name,
teamId: teams.nodes[0]?.id || null,
};
})
);
return { teams, projects };
}
// Main execution
listTeamsAndProjects()
.then(outputWithJq)
.catch((error) => {
console.error("Error fetching teams and projects:", error.message);
process.exit(1);
});
#!/usr/bin/env node
import { writeFileSync, mkdirSync, existsSync } from "fs";
import { homedir } from "os";
import { join } from "path";
import * as readline from "readline";
async function promptForApiKey(): Promise<string> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(
"Enter your Linear API key (get it from https://linear.app/settings/api): ",
(answer) => {
rl.close();
resolve(answer.trim());
}
);
});
}
async function main() {
console.log("🔧 Linear Credentials Setup\n");
// Check if credentials already exist
const credentialsDir = join(homedir(), ".linear");
const credentialsPath = join(credentialsDir, "credentials");
if (existsSync(credentialsPath)) {
console.log("⚠️ Credentials file already exists at:");
console.log(` ${credentialsPath}\n`);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const overwrite = await new Promise<string>((resolve) => {
rl.question("Do you want to overwrite it? (yes/no): ", (answer) => {
rl.close();
resolve(answer.trim().toLowerCase());
});
});
if (overwrite !== "yes" && overwrite !== "y") {
console.log("\n✅ Setup cancelled. Existing credentials preserved.");
return;
}
}
// Get API key from user
const apiKey = await promptForApiKey();
if (!apiKey) {
console.error("\n❌ Error: API key cannot be empty");
process.exit(1);
}
// Validate API key format (Linear keys start with lin_api_)
if (!apiKey.startsWith("lin_api_")) {
console.warn(
"\n⚠️ Warning: Linear API keys typically start with 'lin_api_'"
);
console.warn(" Make sure you copied the correct key.\n");
}
// Create .linear directory if it doesn't exist
if (!existsSync(credentialsDir)) {
mkdirSync(credentialsDir, { recursive: true, mode: 0o700 });
}
// Write credentials file
try {
writeFileSync(credentialsPath, apiKey, { mode: 0o600 });
console.log("\n✅ Credentials saved successfully!");
console.log(` Location: ${credentialsPath}`);
console.log(` Permissions: 600 (read/write for owner only)\n`);
console.log("🎉 You can now use the Linear scripts!\n");
console.log("Try:");
console.log(" npx tsx list-teams.ts");
console.log(" npx tsx scripts/issues/list.ts <teamId>");
} catch (error) {
console.error(
`\n❌ Error writing credentials: ${error instanceof Error ? error.message : String(error)}`
);
process.exit(1);
}
}
main().catch((error) => {
console.error(`\n❌ Setup failed: ${error.message}`);
process.exit(1);
});
import { client } from "../../lib/client.js";
import { outputWithJq } from "../../lib/output.js";
import { getTeamIdOrExit } from "../../lib/config.js";
async function listStatuses() {
const teamId = getTeamIdOrExit(2);
const team = await client.team(teamId);
if (!team) {
console.error("Team not found");
process.exit(1);
}
const statesResponse = await team.states();
const statuses = statesResponse.nodes.map((state) => ({
id: state.id,
name: state.name,
type: state.type,
color: state.color,
position: state.position,
}));
outputWithJq({ statuses });
}
listStatuses().catch((error) => {
console.error("Error listing statuses:", error.message);
process.exit(1);
});
import { client } from "../../lib/client.js";
import { outputWithJq } from "../../lib/output.js";
import { getTeamIdOrExit } from "../../lib/config.js";
/**
* Update issue status by status name (e.g., "Done", "In Progress")
*
* Usage:
* npx tsx scripts/status/set-by-name.ts ABC-123 "Done" --json
* npx tsx scripts/status/set-by-name.ts ABC-123 "In Progress" --json
*
* This avoids the need for jq chaining to find status IDs.
*/
async function setStatusByName() {
const issueId = process.argv[2];
const statusName = process.argv[3];
if (!issueId || !statusName) {
console.error("Error: Please provide issue ID and status name");
console.error(
"Usage: npx tsx scripts/status/set-by-name.ts <issueId> <statusName> [--json]"
);
console.error("Example: npx tsx scripts/status/set-by-name.ts ABC-123 \"Done\" --json");
console.error("\nCommon status names: Backlog, Todo, In Progress, In Review, Done, Canceled");
process.exit(1);
}
// Get team states
const teamId = getTeamIdOrExit(4); // Skip 4 args: node, script, issueId, statusName
const team = await client.team(teamId);
if (!team) {
console.error("Team not found");
process.exit(1);
}
const statesResponse = await team.states();
// Find status by name (case-insensitive)
const targetState = statesResponse.nodes.find(
(state) => state.name.toLowerCase() === statusName.toLowerCase()
);
if (!targetState) {
const availableStates = statesResponse.nodes.map((s) => s.name).join(", ");
console.error(`Error: Status "${statusName}" not found`);
console.error(`Available statuses: ${availableStates}`);
process.exit(1);
}
// Update the issue
const issuePayload = await client.updateIssue(issueId, {
stateId: targetState.id,
});
const issue = await issuePayload.issue;
if (!issue) {
console.error("Failed to update issue status");
process.exit(1);
}
const state = await issue.state;
outputWithJq({
id: issue.id,
identifier: issue.identifier,
title: issue.title,
status: state?.name || "Unknown",
statusId: state?.id,
updatedAt: issue.updatedAt,
});
}
setStatusByName().catch((error) => {
console.error("Error updating issue status:", error.message);
process.exit(1);
});
import { client } from "../../lib/client.js";
import { outputWithJq } from "../../lib/output.js";
async function updateIssueStatus() {
const issueId = process.argv[2];
const stateId = process.argv[3];
if (!issueId || !stateId) {
console.error("Error: Please provide issue ID and state ID");
console.error(
"Usage: npx tsx scripts/status/update.ts <issueId> <stateId>"
);
console.error(
"Tip: Use scripts/status/list.ts to see available states"
);
process.exit(1);
}
const issuePayload = await client.updateIssue(issueId, {
stateId,
});
const issue = await issuePayload.issue;
if (!issue) {
console.error("Failed to update issue status");
process.exit(1);
}
const state = await issue.state;
outputWithJq({
id: issue.id,
identifier: issue.identifier,
title: issue.title,
status: state?.name || "Unknown",
updatedAt: issue.updatedAt,
});
}
updateIssueStatus().catch((error) => {
console.error("Error updating issue status:", error.message);
process.exit(1);
});
import { client } from "../../lib/client.js";
import { outputWithJq } from "../../lib/output.js";
import { loadConfigIfExists } from "../../lib/config.js";
async function listUsers() {
let teamId = process.argv[2];
// If no teamId provided, try to use default from config
if (!teamId) {
const config = loadConfigIfExists();
teamId = config?.defaults?.teamId;
if (teamId) {
console.error(`ℹ️ Using team from config: ${config.defaults.teamName}`);
}
}
if (teamId) {
// List users for a specific team
const team = await client.team(teamId);
if (!team) {
console.error("Team not found");
process.exit(1);
}
const membersResponse = await team.members();
const users = membersResponse.nodes.map((user) => ({
id: user.id,
name: user.name,
email: user.email,
displayName: user.displayName,
admin: user.admin,
active: user.active,
}));
outputWithJq({ users });
} else {
// List all users in the organization
const usersResponse = await client.users();
const users = usersResponse.nodes.map((user) => ({
id: user.id,
name: user.name,
email: user.email,
displayName: user.displayName,
admin: user.admin,
active: user.active,
}));
outputWithJq({ users });
}
}
listUsers().catch((error) => {
console.error("Error listing users:", error.message);
process.exit(1);
});
import { client } from "../../lib/client.js";
import { outputWithJq } from "../../lib/output.js";
async function getCurrentUser() {
const viewer = await client.viewer;
outputWithJq({
id: viewer.id,
name: viewer.name,
email: viewer.email,
displayName: viewer.displayName,
admin: viewer.admin,
createdAt: viewer.createdAt,
});
}
getCurrentUser().catch((error) => {
console.error("Error fetching current user:", error.message);
process.exit(1);
});
Templates API Reference
This document covers how to work with Linear templates programmatically via the API.
Creating Issues from Templates
Using templateId
When creating an issue, you can specify a templateId to apply a template's defaults:
npx tsx scripts/issues/create.ts \
--title "Bug: Login broken" \
--templateId "template-uuid-here" \
--jsonNote: Values provided in the input override template defaults. So if you specify --priority 1 but the template has priority 3, the issue will have priority 1.
Using Default Template
To use the team's default template:
mutation {
issueCreate(input: {
title: "New issue"
teamId: "team-uuid"
useDefaultTemplate: true
}) {
success
issue { id identifier }
}
}Querying Templates
List All Templates
query {
templates {
nodes {
id
name
description
type
templateData
team {
id
name
}
creator {
id
name
}
createdAt
updatedAt
}
}
}Get Team-Specific Templates
query {
team(id: "team-uuid") {
templates {
nodes {
id
name
description
}
}
defaultTemplateForMembers {
id
name
}
defaultTemplateForNonMembers {
id
name
}
}
}Get Single Template
query {
template(id: "template-uuid") {
id
name
description
type
templateData
team {
id
name
}
}
}Template Types
| Type | Description |
|---|---|
issue | Standard issue template |
project | Project template with milestones |
Template Fields
The templateData field contains a JSON object with the template's default values:
{
"title": "Bug: ",
"description": "## Summary\n\n## Steps to Reproduce\n\n## Expected Behavior\n\n## Actual Behavior",
"priority": 2,
"labelIds": ["label-uuid-1", "label-uuid-2"],
"stateId": "state-uuid",
"assigneeId": "user-uuid",
"projectId": "project-uuid",
"estimate": 3
}Creating Templates via API
Create Issue Template
mutation {
templateCreate(input: {
name: "Bug Report"
type: issue
teamId: "team-uuid"
description: "Template for bug reports"
templateData: {
description: "## Summary\n\n## Steps to Reproduce\n\n"
priority: 2
labelIds: ["bug-label-uuid"]
}
}) {
success
template {
id
name
}
}
}Update Template
mutation {
templateUpdate(
id: "template-uuid"
input: {
name: "Updated Bug Report"
templateData: {
priority: 1
}
}
) {
success
template {
id
name
}
}
}Delete Template
mutation {
templateDelete(id: "template-uuid") {
success
}
}Workflow: Get Template ID for Issue Creation
# 1. List templates to find the ID
TEMPLATES=$(npx tsx scripts/list-teams.ts --json)
# Or query directly via GraphQL
curl -X POST https://api.linear.app/graphql \
-H "Authorization: $LINEAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "{ templates { nodes { id name team { key } } } }"
}' | jq '.data.templates.nodes[] | select(.name == "Bug Report")'
# 2. Create issue using template ID
npx tsx scripts/issues/create.ts \
--title "Bug: Specific issue" \
--templateId "found-template-uuid" \
--jsonTemplate + URL Creation (linear.new)
Create pre-filled issues via URL:
https://linear.app/workspace/team/KEY/new?template=template-uuid
https://linear.app/workspace/team/KEY/new?template=template-uuid&title=Override+TitleURL parameters override template values:
title- Issue titledescription- Issue descriptionpriority- Priority (0-4)labelIds- Comma-separated label UUIDsassigneeId- Assignee UUIDprojectId- Project UUID
Best Practices
1. Template Inheritance: Values you provide always override template defaults
2. Team vs Workspace Templates:
- Team templates: Only available to that team
- Workspace templates: Available to all teams (
teamId: null)
3. Default Templates: Teams can set default templates for:
- Members (internal issue creation)
- Non-members (external/Asks issue creation)
4. Template Versioning: Templates don't version - changes apply immediately
5. Finding Template IDs:
- Use GraphQL explorer: https://studio.apollographql.com/public/Linear-API
- Query templates endpoint
- Check URL when editing template in UI
Error Handling
| Error | Cause | Solution |
|---|---|---|
Template not found | Invalid templateId | Query templates to get valid IDs |
Permission denied | Template belongs to different team | Use team-specific template or workspace template |
Invalid template type | Using project template for issue | Check template type field |
Sources
Bug Report Template
Template Configuration
| Property | Value |
|---|---|
| Name | Bug Report |
| Labels | bug |
| Priority | 2 (High) or use triage |
| Default Status | Triage |
Description Template
## Summary
<!-- Brief description of the bug -->
## Environment
- **Browser/App**:
- **OS**:
- **Device**:
- **User Account**:
## Steps to Reproduce
1.
2.
3.
## Expected Behavior
<!-- What should have happened -->
## Actual Behavior
<!-- What actually happened -->
## Screenshots/Videos
<!-- Attach visual evidence if applicable -->
## Console Errors
<!-- Paste any relevant error messages -->
## Additional Context
<!-- Any other relevant information -->CLI Usage
# Create bug report
npx tsx scripts/issues/create.ts \
--title "Bug: [Component] - Brief description" \
--description "## Summary
Login button does not respond on mobile Safari.
## Environment
- **Browser/App**: Safari 17.2
- **OS**: iOS 17.1
- **Device**: iPhone 15 Pro
## Steps to Reproduce
1. Open app on mobile Safari
2. Navigate to login page
3. Tap login button
## Expected Behavior
Login form should submit and redirect to dashboard.
## Actual Behavior
Nothing happens when tapping the button.
## Console Errors
\`\`\`
TypeError: Cannot read property 'submit' of null
\`\`\`" \
--priority 2 \
--jsonBest Practices
1. Title Format: Bug: [Component/Feature] - Brief description
Bug: Cart - Cannot add items on mobileBug: Auth - Session expires unexpectedly
2. Include Visual Evidence: Screenshots or screen recordings significantly speed up debugging
3. Capture Console Logs: JavaScript errors help developers reproduce issues
4. Specify Environment: Bugs often only appear in specific browsers/devices
5. Be Specific: "Button doesn't work" → "Login button doesn't respond to tap events on iOS Safari"
Severity Guidelines
| Severity | Description | Example |
|---|---|---|
| Urgent (1) | App unusable, data loss | Payment processing fails |
| High (2) | Major feature broken | Cannot login |
| Medium (3) | Feature degraded | Slow page load |
| Low (4) | Minor issue | Typo in UI |
Feature Request Template
Template Configuration
| Property | Value |
|---|---|
| Name | Feature Request |
| Labels | enhancement |
| Priority | 3 (Medium) or use triage |
| Default Status | Backlog |
Description Template
## Problem Statement
<!-- What problem does this solve? Who is affected? -->
## Proposed Solution
<!-- Describe the feature you'd like -->
## User Stories
<!-- As a [user type], I want [feature] so that [benefit] -->
- As a user, I want to...
- As an admin, I want to...
## Acceptance Criteria
- [ ] Criterion 1
- [ ] Criterion 2
- [ ] Criterion 3
## Design/Mockups
<!-- Link to designs or describe UI expectations -->
## Technical Considerations
<!-- Any known technical constraints or dependencies -->
## Alternatives Considered
<!-- What other solutions were explored? -->
## Additional Context
<!-- Business value, customer requests, competitive analysis -->CLI Usage
# Create feature request
npx tsx scripts/issues/create.ts \
--title "Feature: Add dark mode toggle" \
--description "## Problem Statement
Users have requested dark mode support, especially for nighttime usage. Currently, the app only supports light theme which causes eye strain in low-light environments.
## Proposed Solution
Add a dark mode toggle in Settings that switches the app theme. The preference should persist across sessions.
## User Stories
- As a user, I want to switch to dark mode so that I can use the app comfortably at night
- As a user, I want my theme preference saved so that I don't have to toggle it each session
## Acceptance Criteria
- [ ] Toggle switch in Settings page
- [ ] Dark theme applied to all components
- [ ] Preference persisted in localStorage
- [ ] Respects system preference by default
- [ ] Smooth transition animation
## Design/Mockups
https://www.figma.com/file/abc123
## Technical Considerations
- Use CSS variables for theme colors
- Consider prefers-color-scheme media query
- Test with all existing components
## Alternatives Considered
- Auto-detect only (no manual toggle) - rejected: users want control
- Scheduled dark mode - future enhancement" \
--priority 3 \
--jsonBest Practices
1. Title Format: Feature: Brief description
Feature: Add export to CSV functionalityFeature: Enable two-factor authentication
2. Focus on the Problem: Explain the "why" before the "what"
3. Include User Stories: Helps team understand user perspective
4. Define Done: Clear acceptance criteria prevent scope creep
5. Link Designs: Visual references accelerate development
6. Mention Alternatives: Shows you've thought through options
Priority Guidelines
| Priority | Criteria |
|---|---|
| Urgent (1) | Critical for launch, blocking revenue |
| High (2) | High customer demand, competitive feature |
| Medium (3) | Nice to have, improves UX |
| Low (4) | Future consideration, minor enhancement |
Linking Related Issues
Reference related issues in the description:
## Related Issues
- Blocked by: https://linear.app/workspace/issue/AA-100
- Related: https://linear.app/workspace/issue/AA-101
- Parent epic: https://linear.app/workspace/issue/AA-50Linear will automatically convert these URLs to clickable issue links.
Linear Issue Templates Guide
This guide covers best practices for creating and using issue templates in Linear, both via the UI and the API.
Overview
Templates in Linear:
- Speed up issue creation with pre-filled fields
- Ensure consistent information capture across teams
- Enable filtering and reporting by template type
- Work across Linear UI, Slack, Asks, and API
Template Types
1. Standard Templates
Pre-defined issue formats with:
- Title prefix/format
- Description (markdown with placeholder text)
- Default labels, priority, project, assignee
- Estimate and due date defaults
2. Form Templates (Business/Enterprise)
Structured forms with custom fields:
- Text inputs, dropdowns, checkboxes
- Required field validation
- Issue property fields (priority, customer, labels)
- Field descriptions and instructions
Creating Templates
Via Linear UI
1. Navigate to Settings → Team → Templates 2. Click New Template 3. Configure:
- Template name
- Description with placeholder text
- Default properties (labels, priority, project)
4. Save template
Via API
# Create issue from template
npx tsx scripts/issues/create.ts \
--title "Bug: Login broken" \
--templateId "template-uuid" \
--jsonThe API's IssueCreateInput accepts:
templateId- UUID of the template to useuseDefaultTemplate- Boolean to use team's default template
Note: Values provided in the input override template defaults.
Best Practices
1. Naming Conventions
Use clear, action-oriented titles:
✅ [Verb] [What] [Context]
"Fix broken scroll in mobile navbar"
"Add dark mode toggle to settings"
❌ Vague titles
"Bug"
"Feature"2. Structured Descriptions
Include consistent sections:
## Background
Why are we doing this? What's the context?
## Requirements
- [ ] Requirement 1
- [ ] Requirement 2
## Acceptance Criteria
What defines "done"?
## Links
- Design: [Figma link]
- Spec: [Doc link]3. Use Placeholder Text
In Linear UI, format text as placeholder (select text → click Aa icon):
## Steps to Reproduce
<Describe the steps you took>
## Expected Behavior
<What should have happened>
## Actual Behavior
<What actually happened>4. Set Appropriate Defaults
Pre-fill fields that are always the same:
- Bug reports → "bug" label, Triage status
- Feature requests → "enhancement" label
- Technical debt → "tech-debt" label, Low priority
5. Keep Templates Minimal
Only include fields that are truly necessary. Too many required fields = friction.
Template Examples
See the following files for ready-to-use templates:
| Template | File | Use Case |
|---|---|---|
| Bug Report | bug-report.md | User-reported bugs, QA findings |
| Feature Request | feature-request.md | New features, enhancements |
| Technical Debt | tech-debt.md | Refactoring, code cleanup |
| Security Issue | security-issue.md | Vulnerabilities, security fixes |
| Sprint Task | sprint-task.md | General sprint work items |
Filtering by Template
Issues created from templates are filterable:
template:Bug Report
template:Feature RequestUse Insights to analyze:
- Bug reports vs feature requests ratio
- Resolution time by template type
- Template usage by team
Integration with Slack/Asks
Templates can be used in:
- Slack: Up to 5 templates available via
/linearcommand - Asks: Form templates for external stakeholders
- Email: Create issues from templates via intake email
API Reference
Query Templates
query {
templates {
nodes {
id
name
description
type
team { id name }
}
}
}Create Issue from Template
mutation {
issueCreate(input: {
title: "Issue title"
teamId: "team-uuid"
templateId: "template-uuid"
}) {
success
issue { id identifier url }
}
}Using Default Template
mutation {
issueCreate(input: {
title: "Issue title"
teamId: "team-uuid"
useDefaultTemplate: true
}) {
success
issue { id identifier url }
}
}Sources
Security Issue Template
Template Configuration
| Property | Value |
|---|---|
| Name | Security Issue |
| Labels | security, vulnerability |
| Priority | 1 (Urgent) or 2 (High) |
| Default Status | Triage |
Description Template
## Vulnerability Summary
<!-- Brief description of the security issue -->
## Severity
<!-- Critical / High / Medium / Low -->
## CVSS Score (if applicable)
<!-- e.g., 7.5 (High) -->
## Affected Components
<!-- Which systems/services are affected? -->
-
-
## Attack Vector
<!-- How could this be exploited? -->
## Impact
<!-- What's the potential damage? -->
- **Confidentiality**:
- **Integrity**:
- **Availability**:
## Steps to Reproduce
<!-- How to verify the vulnerability exists -->
1.
2.
3.
## Proof of Concept
<!-- Code or demonstration if safe to share -->
## Recommended Fix
<!-- Proposed remediation -->
## References
<!-- CVE IDs, security advisories, documentation -->
-
## Discovery
- **Discovered by**:
- **Discovered date**:
- **Disclosure deadline**:CLI Usage
# Create security issue (CONFIDENTIAL - be careful with details)
npx tsx scripts/issues/create.ts \
--title "Security: SQL Injection in search endpoint" \
--description "## Vulnerability Summary
SQL injection vulnerability in the /api/search endpoint allows unauthorized database access.
## Severity
High
## CVSS Score
8.6 (High)
## Affected Components
- \`/api/search\` endpoint
- \`src/services/search.ts\`
## Attack Vector
User-supplied search query is concatenated directly into SQL without parameterization.
## Impact
- **Confidentiality**: High - Attacker can read any database records
- **Integrity**: High - Attacker can modify database records
- **Availability**: Medium - Potential for data deletion
## Steps to Reproduce
1. Navigate to search page
2. Enter: \`'; DROP TABLE users; --\`
3. Observe SQL error in response
## Recommended Fix
Use parameterized queries:
\`\`\`typescript
// Before (vulnerable)
const query = \\\`SELECT * FROM items WHERE name = '\${searchTerm}'\\\`;
// After (safe)
const query = 'SELECT * FROM items WHERE name = $1';
const result = await db.query(query, [searchTerm]);
\`\`\`
## References
- OWASP SQL Injection: https://owasp.org/www-community/attacks/SQL_Injection
- CWE-89: https://cwe.mitre.org/data/definitions/89.html
## Discovery
- **Discovered by**: Security audit
- **Discovered date**: 2025-01-15
- **Disclosure deadline**: 2025-01-22" \
--priority 1 \
--jsonBest Practices
1. Title Format: Security: Brief description (no exploit details)
Security: Authentication bypass in admin panelSecurity: XSS vulnerability in comments
2. Handle Sensitively:
- Don't include full exploit code in public channels
- Use private issues if available
- Follow responsible disclosure practices
3. Include Severity: Use standard severity ratings (Critical/High/Medium/Low)
4. Provide Remediation: Security issues should include fix recommendations
5. Set Deadline: Security issues often have disclosure timelines
Severity Guidelines
| Severity | CVSS | Response Time | Examples |
|---|---|---|---|
| Critical | 9.0-10.0 | Immediate | RCE, auth bypass, data breach |
| High | 7.0-8.9 | 24-48 hours | SQL injection, privilege escalation |
| Medium | 4.0-6.9 | 1 week | XSS, CSRF, info disclosure |
| Low | 0.1-3.9 | Next sprint | Missing headers, minor leaks |
Common Vulnerability Types
| Type | Label | Example |
|---|---|---|
| Injection | injection | SQL, NoSQL, Command, LDAP |
| Authentication | auth | Broken auth, session issues |
| XSS | xss | Reflected, Stored, DOM-based |
| Access Control | access-control | IDOR, privilege escalation |
| Cryptography | crypto | Weak encryption, key exposure |
| Configuration | misconfiguration | Default creds, open ports |
Responsible Disclosure
If external: 1. Acknowledge receipt within 24 hours 2. Provide timeline for fix 3. Credit reporter (if desired) 4. Coordinate disclosure date
Sprint Task Template
Template Configuration
| Property | Value |
|---|---|
| Name | Sprint Task |
| Labels | (varies by task type) |
| Priority | 3 (Medium) |
| Default Status | Todo |
| Estimate | (set per task) |
Description Template
## Objective
<!-- What needs to be accomplished? -->
## Background
<!-- Why are we doing this? Context and motivation -->
## Requirements
- [ ] Requirement 1
- [ ] Requirement 2
- [ ] Requirement 3
## Technical Approach
<!-- How will this be implemented? -->
## Acceptance Criteria
- [ ] Criterion 1
- [ ] Criterion 2
- [ ] Tests written and passing
- [ ] Code reviewed
## Dependencies
<!-- What needs to happen first? -->
## Out of Scope
<!-- What are we NOT doing? -->
## Links
- Design:
- Spec:
- Parent issue:CLI Usage
# Create sprint task
npx tsx scripts/issues/create.ts \
--title "Implement user avatar upload" \
--description "## Objective
Add the ability for users to upload and update their profile avatar.
## Background
Users have requested the ability to personalize their profiles. Currently, we only show initials.
## Requirements
- [ ] Upload button on profile settings page
- [ ] Support JPEG, PNG, WebP formats
- [ ] Max file size: 5MB
- [ ] Auto-resize to 256x256
- [ ] Store in S3 bucket
## Technical Approach
1. Add file input component to ProfileSettings
2. Create \`/api/users/avatar\` endpoint
3. Use sharp for image processing
4. Upload to S3 with user ID as key
5. Update user record with avatar URL
## Acceptance Criteria
- [ ] User can upload image from profile settings
- [ ] Image is resized and optimized
- [ ] Avatar displays throughout app
- [ ] Invalid files show error message
- [ ] Unit tests for upload service
- [ ] E2E test for upload flow
## Dependencies
- S3 bucket configured (already done)
- sharp package installed
## Out of Scope
- Avatar cropping UI (future enhancement)
- Gravatar integration
- Avatar history
## Links
- Design: https://www.figma.com/file/abc123
- Parent epic: https://linear.app/workspace/issue/AA-50" \
--priority 3 \
--estimate 3 \
--jsonBest Practices
1. Title Format: Imperative verb + what
Implement user avatar uploadAdd pagination to user listFix memory leak in WebSocket handler
2. Define "Done": Clear acceptance criteria prevent ambiguity
3. List Dependencies: Helps with sprint planning
4. Scope Boundaries: "Out of Scope" prevents creep
5. Link Related Issues: Reference parent epics, blockers, related work
Estimate Guidelines
Use Fibonacci or T-shirt sizing consistently:
| Points | Effort | Example |
|---|---|---|
| 1 | Few hours | Config change, copy update |
| 2 | Half day | Simple component, small fix |
| 3 | 1 day | Feature with tests |
| 5 | 2-3 days | Complex feature |
| 8 | 1 week | Large feature, multiple components |
| 13 | 2 weeks | Epic-level work (should be broken down) |
Task Breakdown Pattern
For larger tasks, create sub-issues:
# Parent task
npx tsx scripts/issues/create.ts \
--title "Epic: User profile system" \
--description "..." \
--json
# Sub-tasks (using parent ID)
npx tsx scripts/issues/create.ts \
--title "Add profile settings page" \
--parent AA-100 \
--estimate 3 \
--json
npx tsx scripts/issues/create.ts \
--title "Implement avatar upload" \
--parent AA-100 \
--estimate 5 \
--json
npx tsx scripts/issues/create.ts \
--title "Add profile API endpoints" \
--parent AA-100 \
--estimate 3 \
--jsonStatus Workflow
Typical sprint task flow:
Todo → In Progress → In Review → DoneUpdate status as you work:
# Start working
npx tsx scripts/status/update.ts AA-101 "in-progress-state-id" --json
# Submit for review
npx tsx scripts/status/update.ts AA-101 "in-review-state-id" --json
# Complete
npx tsx scripts/status/update.ts AA-101 "done-state-id" --jsonTechnical Debt Template
Template Configuration
| Property | Value |
|---|---|
| Name | Technical Debt |
| Labels | tech-debt, refactor |
| Priority | 4 (Low) or 3 (Medium) |
| Default Status | Backlog |
Description Template
## Summary
<!-- Brief description of the technical debt -->
## Current State
<!-- What's the problem with the current implementation? -->
## Impact
<!-- How does this affect development, performance, or maintainability? -->
- **Development velocity**:
- **Performance**:
- **Security risk**:
- **Test coverage**:
## Proposed Solution
<!-- How should this be refactored? -->
## Files/Components Affected
<!-- List the areas of code that need changes -->
- `src/components/...`
- `src/utils/...`
## Migration Strategy
<!-- If breaking changes, how will we migrate? -->
## Acceptance Criteria
- [ ] Criterion 1
- [ ] Criterion 2
- [ ] Tests updated
- [ ] Documentation updated
## Effort Estimate
<!-- T-shirt size: XS, S, M, L, XL -->
## Risk Assessment
<!-- What could go wrong? -->CLI Usage
# Create tech debt issue
npx tsx scripts/issues/create.ts \
--title "Tech Debt: Migrate from moment.js to date-fns" \
--description "## Summary
Replace moment.js with date-fns to reduce bundle size and improve tree-shaking.
## Current State
We're using moment.js (300KB gzipped) for date formatting throughout the app. It's imported wholesale and doesn't tree-shake.
## Impact
- **Development velocity**: Neutral
- **Performance**: -300KB bundle size potential savings
- **Security risk**: moment.js is in maintenance mode
- **Test coverage**: Need to update date-related tests
## Proposed Solution
1. Install date-fns
2. Create adapter layer with same API
3. Migrate components incrementally
4. Remove moment.js when complete
## Files/Components Affected
- \`src/utils/date.ts\`
- \`src/components/DatePicker/\`
- \`src/components/Timeline/\`
- \`src/hooks/useFormatDate.ts\`
## Migration Strategy
1. Add date-fns alongside moment.js
2. Create \`src/utils/date-adapter.ts\` with unified API
3. Migrate one component at a time
4. Remove moment.js after all migrations complete
## Acceptance Criteria
- [ ] All date operations use date-fns
- [ ] Bundle size reduced by >200KB
- [ ] All existing tests pass
- [ ] No moment.js imports remain
## Effort Estimate
Medium (M) - 2-3 days
## Risk Assessment
- Date format inconsistencies during migration
- Timezone handling differences between libraries" \
--priority 4 \
--jsonBest Practices
1. Title Format: Tech Debt: Brief description
Tech Debt: Remove deprecated API callsTech Debt: Add TypeScript to utils folder
2. Quantify Impact: "Reduces build time by 30%" is better than "Makes build faster"
3. Include Migration Path: Especially for breaking changes
4. List Affected Files: Helps estimate scope
5. Link to Related Issues: Tech debt often blocks feature work
Priority Guidelines
| Priority | When to Use |
|---|---|
| High (2) | Blocks feature development, security risk |
| Medium (3) | Significant performance/DX improvement |
| Low (4) | Nice to have, long-term improvement |
Common Tech Debt Categories
| Category | Examples |
|---|---|
| Dependencies | Outdated packages, deprecated libraries |
| Code Quality | Missing types, inconsistent patterns |
| Performance | Bundle size, slow queries, memory leaks |
| Testing | Missing tests, flaky tests, low coverage |
| Documentation | Outdated docs, missing API docs |
| Infrastructure | CI/CD improvements, build optimization |
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"outDir": "./dist",
"rootDir": "."
},
"include": ["**/*.ts"],
"exclude": ["node_modules", "dist"]
}
Issue Lifecycle Workflow
Complete workflow guide for managing Linear issues from triage to completion.
Status Lifecycle
┌─────────┐ ┌─────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌──────────┐
│ Triage │───▶│ Backlog │───▶│ Todo │───▶│In Progress│───▶│ In Review │───▶│ Done │
└─────────┘ └─────────┘ └───────────┘ └───────────┘ └───────────┘ └──────────┘
│ │ │ │ │ │
│ ▼ │ │ │ │
│ ┌─────────┐ │ │ │ │
│ │ Icebox │ │ │ │ │
│ └─────────┘ │ │ │ │
▼ ▼ ▼ ▼ ▼ │
┌────────────────────────────────────────────────────────────────────────────────┐ │
│ Canceled / Duplicate │◀───┘
└────────────────────────────────────────────────────────────────────────────────┘Workflow Stages
1. Triage (type: triage)
When: Issue is newly created and awaiting review.
Actions:
- Review issue description and requirements
- Assess priority and effort
- Assign to appropriate team/project
- Move to Backlog or Todo
2. Backlog (type: backlog)
When: Issue is prioritized but not scheduled.
Actions:
- Refine requirements if needed
- Estimate effort
- Schedule for upcoming sprint/cycle
2b. Icebox (type: backlog)
When: Issue is valid but deferred indefinitely.
Actions:
- Add comment explaining why it's being iceboxed
- Review periodically during planning sessions
ICEBOX=$(echo "$STATES" | jq -r '.statuses[] | select(.name == "Icebox") | .id')
npx tsx scripts/comments/create.ts ABC-123 "Moving to Icebox: Lower priority, will revisit in Q2" --json
npx tsx scripts/status/update.ts ABC-123 "$ICEBOX" --json3. Todo (type: unstarted)
When: Issue is ready to be picked up.
Actions:
- Self-assign or get assigned
- Review acceptance criteria
- Prepare to start work
4. In Progress (type: started)
When: Actively working on the issue.
Actions required upon starting:
cd .claude/skills/linear
# Get current user
ME=$(npx tsx scripts/users/me.ts --json | jq -r '.id')
# Get "In Progress" state (use name for precision)
STATES=$(npx tsx scripts/status/list.ts --json)
IN_PROGRESS=$(echo "$STATES" | jq -r '.statuses[] | select(.name == "In Progress") | .id')
# Update issue
npx tsx scripts/issues/update.ts --issue ABC-123 --assignee "$ME" --json
npx tsx scripts/status/update.ts ABC-123 "$IN_PROGRESS" --json5. In Review (type: started)
When: Work is complete and awaiting review.
Actions:
- Code review requested
- PR submitted
- Awaiting approval
IN_REVIEW=$(echo "$STATES" | jq -r '.statuses[] | select(.name == "In Review") | .id')
npx tsx scripts/status/update.ts ABC-123 "$IN_REVIEW" --json6. Done (type: completed)
When: Work is complete and verified.
Actions required before marking done: 1. Verify all acceptance criteria met 2. Check related issues 3. Add completion documentation 4. Update status
Completion Checklist
Before marking ANY issue as Done, verify:
- [ ] Code complete: All implementation finished
- [ ] Tests passing: Unit/integration tests green
- [ ] Documentation: Code comments where needed
- [ ] Related issues checked: Parent, siblings, children reviewed
- [ ] Completion comment added: Summary of changes
Completion Comment Template
## Completion Summary
**Status**: ✅ COMPLETED
### Changes Made
- `path/to/file.ts`: Brief description of changes
- `path/to/another.ts`: Brief description of changes
### Features Implemented
- Feature 1: What it does
- Feature 2: What it does
### Testing
- [ ] Unit tests added/updated
- [ ] Manual testing completed
- [ ] Edge cases verified
### Next Steps (if any)
- Follow-up task description
- Configuration required
- Related work to schedule
### Related Issues
- Parent: ABC-100 (if applicable)
- Subtasks: ABC-101, ABC-102 (if applicable)Related Issue Management
Working on Subtasks
When completing a subtask:
1. Update the subtask with completion comment 2. Check sibling subtasks: Are others complete? 3. Update parent issue:
- If all subtasks done → Mark parent Done
- If partial → Update parent with progress comment
# Get parent issue details
npx tsx scripts/issues/get.ts ABC-100 --json
# Add progress comment to parent
npx tsx scripts/comments/create.ts ABC-100 "Subtask ABC-101 completed. 2/3 subtasks done." --jsonCompleting Parent Issues
When all subtasks are complete:
# Add summary comment
npx tsx scripts/comments/create.ts ABC-100 "$(cat <<'EOF'
## All Subtasks Completed
**Status**: ✅ COMPLETED
### Completed Subtasks
- ABC-101: Feature A implementation
- ABC-102: Feature B implementation
- ABC-103: Testing and documentation
### Summary
All acceptance criteria met. Feature ready for release.
EOF
)" --json
# Update status
DONE=$(npx tsx scripts/status/list.ts --json | jq -r '.statuses[] | select(.type == "completed") | .id' | head -1)
npx tsx scripts/status/update.ts ABC-100 $DONE --jsonDocumentation Quality Examples
Good Documentation
## Completion Summary
**Status**: ✅ COMPLETED
### Changes Made
- `src/auth/oauth.ts`: Implemented OAuth 2.1 PKCE flow with state validation
- `src/auth/tokens.ts`: Added secure token storage with encryption
- `tests/auth/oauth.test.ts`: 12 unit tests covering happy path and edge cases
### Features Implemented
- OAuth authorization with PKCE challenge
- Automatic token refresh before expiry
- Secure token storage in encrypted format
- Error handling with user-friendly messages
### Testing
- [x] Unit tests (12 passing)
- [x] Integration test with mock OAuth server
- [x] Manual testing with Google OAuth
### Configuration Required
Set environment variables:
- `OAUTH_CLIENT_ID`
- `OAUTH_REDIRECT_URI`Poor Documentation
Done.Fixed the bug.Implemented feature.Why this matters: Poor documentation creates confusion about what was delivered and makes it hard to debug issues later.
Edge Cases
Blocked Issues
If an issue is blocked:
1. Add comment explaining the blocker 2. Create linked blocking issue if needed 3. Keep status as In Progress or move to Backlog 4. Do NOT mark as Done
npx tsx scripts/comments/create.ts ABC-123 "Blocked by: Need API credentials from external team. Created follow-up ABC-124." --jsonReopened Issues
If a completed issue needs more work:
1. Move back to In Progress 2. Add comment explaining why reopened 3. Follow normal completion flow when done again
Canceled Issues
When canceling:
1. Add comment explaining why 2. Move to Canceled status 3. Update any parent issues
CANCELED=$(npx tsx scripts/status/list.ts --json | jq -r '.statuses[] | select(.type == "canceled") | .id' | head -1)
npx tsx scripts/comments/create.ts ABC-123 "Canceled: Requirements changed, no longer needed." --json
npx tsx scripts/status/update.ts ABC-123 $CANCELED --jsonQuick Reference Commands
cd .claude/skills/linear
# Get statuses (run once, reuse $STATES)
STATES=$(npx tsx scripts/status/list.ts --json)
# Start work on issue
ME=$(npx tsx scripts/users/me.ts --json | jq -r '.id')
IN_PROGRESS=$(echo "$STATES" | jq -r '.statuses[] | select(.name == "In Progress") | .id')
npx tsx scripts/issues/update.ts --issue ABC-123 --assignee "$ME" --json
npx tsx scripts/status/update.ts ABC-123 "$IN_PROGRESS" --json
# Move to review
IN_REVIEW=$(echo "$STATES" | jq -r '.statuses[] | select(.name == "In Review") | .id')
npx tsx scripts/status/update.ts ABC-123 "$IN_REVIEW" --json
# Complete issue
DONE=$(echo "$STATES" | jq -r '.statuses[] | select(.name == "Done") | .id')
npx tsx scripts/comments/create.ts ABC-123 "Completion summary here..." --json
npx tsx scripts/status/update.ts ABC-123 "$DONE" --json
# Move to icebox
ICEBOX=$(echo "$STATES" | jq -r '.statuses[] | select(.name == "Icebox") | .id')
npx tsx scripts/comments/create.ts ABC-123 "Moving to Icebox: reason..." --json
npx tsx scripts/status/update.ts ABC-123 "$ICEBOX" --json
# Cancel issue
CANCELED=$(echo "$STATES" | jq -r '.statuses[] | select(.name == "Canceled") | .id')
npx tsx scripts/comments/create.ts ABC-123 "Reason for cancellation..." --json
npx tsx scripts/status/update.ts ABC-123 "$CANCELED" --json
# Mark as duplicate
DUPLICATE=$(echo "$STATES" | jq -r '.statuses[] | select(.name == "Duplicate") | .id')
npx tsx scripts/comments/create.ts ABC-123 "Duplicate of ABC-100" --json
npx tsx scripts/status/update.ts ABC-123 "$DUPLICATE" --json