Linear for you and your agents. Same queue, same CLI.
Linear for human-owned work and agent-executed handoff. Keep the human as assignee, delegate the issue to an agent with --delegate, and require proof before --done closes the loop.
Use the same shell contract whether you're typing or a subagent is: query the queue, delegate the work, ship progress notes, and close only with evidence. Single-file Python (stdlib only) keeps install and audit overhead low, but the moat is the delegation workflow.
Watch with audio (MP4, 1.4 MB)
Preferred (pinned tag + SHA-256 verify via install.sh):
curl -sSL https://raw.githubusercontent.com/phnx-labs/linear-cli/v0.31.0/install.sh | shManual pin (same tag; no checksum — prefer install.sh above):
curl -o ~/.local/bin/linear https://raw.githubusercontent.com/phnx-labs/linear-cli/v0.31.0/linear
chmod +x ~/.local/bin/linearCreate a Linear API key with Full access at linear.app/settings/account/security, then:
linear setup --api-key lin_api_... --agent claudeConfig is written to ~/.linear-cli/config.json. If you already have ~/.agents/linear.json from an earlier setup, linear-cli auto-migrates it on first run.
Security: treat the API key like a password. Do not paste it into chat with an LLM in plaintext. Pass it from your shell env (
linear setup --api-key "$LINEAR_API_KEY"), or let linear-cli read it from the macOS Keychain. Resolution order is: config file →LINEAR_API_KEYenv → Keychain servicelinear-api-key.
For a shared agent fleet, mint the Full-access key once with agents browser so the browser session drives linear.app/settings/account/security instead of exposing the key in chat. Store it in agents secrets / the OS keychain, then let each session run with LINEAR_API_KEY populated from that secret.
No code change is required for this path: linear-cli already resolves credentials from its config file, then LINEAR_API_KEY, then the macOS Keychain service linear-api-key.
linear tasks # your queue in the active cycle
linear tasks --board # whole team board
linear tasks ANT-42 # detail view
linear tasks --query "auth refresh" # search title + description
linear tasks --similar "OAuth token renewal" # related tickets, ranked (check before you create)
linear tasks --cycle all # whole team: every cycle + backlog
linear tasks --cycle none # the backlog (issues in no cycle)
linear tasks --cycle "Q2W11" # a specific cycle by name, number, or id
linear tasks --since 2026-06-01 # only issues created on/after a date
linear tasks --assignee me # by assignee: me | none | someone@x.com
linear tasks --project "Rush App" # scope to one project (name or UUID)
linear tasks --milestone "v1.0" # scope to one milestone (the whole deliverable)
linear tasks --project "Rush App" --by-milestone # group by milestone; unmatched fall in "No milestone"
linear tasks --all # whole team, ignoring the cwd project auto-scope
linear tasks --json | jq # machine-readable (adds "project": {id, name, auto})
linear update ANT-42 --pickup # claim (In Progress)
linear update ANT-42 --comment "..." # progress note
linear update ANT-42 --done --proof https://pr/123 --proof "deployed"
linear update ANT-42 --priority urgent --assign bisma # human by name or email
linear update ANT-42 --title "Renamed" --description "Rewritten body"
linear update ANT-42 --project "Foo" --milestone "v1.0"
linear update ANT-42 --blocked-by ANT-7 --blocks ANT-9 # relations
linear update ANT-42 --delegate claude # hand to an agent (delegate)
linear update ANT-42 --delegate none # clear the delegate
linear update ANT-1 ANT-2 ANT-3 --cycle none # bulk: many ids at once
linear tasks --cycle all --json | jq -r '.issues[].identifier' \
| linear update --stdin --label triage # bulk via stdin (xargs-style)
linear queue # list closes waiting on rate limits
linear queue list # same as bare queue
linear queue drain # apply queued closes with backoff
linear queue drain --once # apply one due intent, then stop
linear queue drain --dry-run # preview queued closes without applying
linear create "Fix auth bug" --priority urgent # horizon now: due today, reminder in 15m, next milestone
linear create "Billing page" --milestone "v1.0" # horizon milestone: due on v1.0's target date
linear create "Someday idea" --backlog # horizon backlog: Backlog, no cycle, no date, Q1 2027
linear create "Crash on login" --bug # label Bug, backlog, priority low
linear create "Prod is down" --bug --blocker # label Bug, horizon now, priority high
linear create --description "Paragraph dump — title is derived from this."
linear create "Sub-task" --parent ANT-42 # nested; prints a tip nudging a flat issue
linear create "Roadmap item" --project "Phoenix" # project outside the cwd's binding
linear create "Ship it" --delegate droid
linear create --from-file plan.jsonl # bulk: one JSON object per line
linear create "Unowned" --assign none --force # refused without --force: every issue needs an owner
linear create "No deliverable yet" --skip-milestone # escape hatch: warns, lands in "No milestone"
linear goals # company quarter, week, my week, my day + to-dos, team
linear goals --json # the same as one document (see Goals JSON)
linear goals set week "Ship checkout" --milestone "Ready to charge"
linear goals set day "Stripe webhook green" # parented to your week goal
linear goals set company-week "Launch gate green" # renames the active cycle
linear goals todo "Write the refund test" # a sub-issue of today's goal, due today
linear goals todo --done ANT-51
linear projects # list projects + progress + issue count
linear projects "Phoenix" # detail view: milestones with per-milestone % done
linear projects overview --json # one document: current cycle + projects → milestones → open issues
linear projects overview --project "Phoenix" # restrict (repeatable); compact tree without --json
linear projects create --name "Phoenix" --lead you@co.com --target 2026-09-30
linear projects update "Phoenix" --description "..." # description / lead / dates / state
linear projects update "Phoenix" --priority low # urgent | high | medium | low | none
linear projects archive "Old Project" # remove a project (moves to trash)
linear initiatives # workspace initiatives (list / show / create / link)
linear initiatives "Company goal" # detail + linked projects with progress
linear initiatives link "Q3" --project "Phoenix"
linear milestones list "Phoenix" # milestones in a project, each with % done (issues rolled up)
linear milestones create --project "Phoenix" --name "v1.0" --target 2026-08-15
linear milestones set-target-date "v1.0" 2026-08-20 --project "Phoenix"
linear milestones move "v1.0" --to "Phoenix CLI" # across projects
linear milestones delete "v1.0" --project "Phoenix"
linear labels # available labels
linear labels create triage --color "#ff8800" # label CRUD ...
linear labels update triage --name needs-triage # ... rename ...
linear labels delete needs-triage # ... delete
linear users # humans (--assign) + agents (--delegate), grouped
linear agents # agent members you can --delegate to (auto-detected)
linear agents --refresh # re-detect after installing/removing an agent app
linear states # the team's workflow states (valid --status values)
linear cycles # list cycles (add --ids for UUIDs)
linear cycles create --name "Jul W1" --starts 2026-07-01 --ends 2026-07-07
linear cycles update "Jul W1" --name "Jul Week 1" # by name or id
linear --team ENG tasks # one-shot override (multi-team workspace)Every list is fully paginated — no silent truncation at Linear's 50-issue page cap, so
linear tasksand--querysee the whole cycle/team before you create a duplicate.
Full help: linear <command> --help.
--query is a server-side substring match on title and description. --similar
is the check-before-create step: it ranks the whole team's tickets (every cycle,
done and canceled included) by relevance to a title or paragraph, fusing Linear's
own semantic search with a local token-overlap ranker.
linear tasks --similar "mid-run OAuth token renewal" # top 10, marks: S semantic, L lexical
linear tasks --similar "worktree cleanup" --limit 3 --json # {"rows": [...], "rankers": [...]}linear tasks --similar "<title or paragraph>" is the check: run it, and if a
close ticket already exists, enrich that one instead of filing a twin. linear create repeats the same ranking automatically (top 3, printed to stderr) and
then creates anyway — advisory, never a deny. --from-file bulk create skips
the lookup because searchIssues is rate-limited to 30/min.
A third ranker scores candidates by cosine similarity over text embeddings. It is off until you configure a backend:
linear setup --embeddings ollama # local, default model qwen3-embedding:0.6b
linear setup --embeddings gemini # gemini-embedding-001, key from GEMINI_API_KEY
linear setup --embeddings none # off againOllama is read from $OLLAMA_HOST (default http://localhost:11434);
--embeddings-model NAME picks another model (embeddinggemma, all-minilm,
…). The Gemini key comes from the environment only and is never written to
config.json. Vectors are cached in ~/.linear-cli/cache.sqlite keyed by
issue and model, so only new or edited issues are re-embedded. A hit is marked
E; if the backend is unreachable the ranker prints one embeddings: … skipped line and --similar answers from the other two.
The same CLI works whether you're typing or a subagent is. Driving Linear from either shouldn't require shelling out to @linear/sdk, hand-rolling GraphQL, or parsing HTML.
- Assignee-as-queue.
linear tasksreturns what you own in the active cycle. Widen with--cycle all(whole team) or--cycle none(backlog), or filter by--assignee me|none|<email>. No dashboards, no saved views. - Directory-aware scope. When
agents projectsbinds the current directory to a Linear project,linear tasks(and--board) auto-scope to that project — so an agent launched inside a project folder works that project's queue, not the whole workspace.--allshows every project,--project Xoverrides, andautoScope: falsein~/.linear-cli/config.jsondisables it. Fail-open: with noagentsCLI or no binding for the cwd, nothing changes. The--jsonoutput carriesproject: {id, name, auto}(null when unscoped). - Milestones as deliverables.
--milestonescopes to one deliverable across all cycles;--by-milestonegroups a project's issues by milestone (with a No milestone bucket for unmatched work), each row annotated with its cycle so you see which iteration a deliverable's work is scheduled in.linear projects/milestones listroll up per-milestone % done, so a deliverable's progress sits next to its target date. Scoping to--project/--milestonewidens to all cycles by default (the whole deliverable, not just this cycle's slice). - Every issue gets an owner.
createrefuses to make an unassigned issue and says why. The default already assigns the API key owner, so this only fires when the assignee would be empty —--assign none, an--assignvalue matching no human (which used to warn and create it unowned anyway), or an unresolvable API key owner (which used to create it unowned with no warning at all). Fix it with--assign <email|name>, or say--forcewhen it genuinely has no owner yet.--delegatedoesn't count: it sets the agent, not the owner, so pair it with--assign. Keeps the board's "No assignee" bucket from filling with tickets nobody picks up. - Every issue gets a date by rule.
createputs each issue in exactly one horizon and prints what it chose on one stderr line (horizon now · urgent → due 2026-10-08, reminder 13:45 · milestone: 0.1.0 (next for Rush) · …). See Horizons. The project comes from--projector the cwd'sagents projectsbinding; the milestone from--milestoneor the project's next open one. With neither,createrefuses and its error teaches the three horizons.--skip-milestoneis the one escape hatch and warns.--from-fileapplies the same rules per row (a row can setbacklog,bug,blocker). A project whose Linear status is paused, completed or canceled takes no new issues;createrefuses it unless--force, so setting that status closes a project to agents working in its directory. - Native agent delegation.
linear update ANT-42 --delegate claudesets Linear'sdelegateId: the human stays assignee, the agent becomes delegate, and review ownership stays clear. - One ownership model.
delegateis the only thing that owns an issue.linear tasks --agent claudefilters to issues delegated to Claude; the default view adds the issues nobody has been delegated (delegateis null).linear tasks --boardgroups its columns by delegate. There is no label lane — an unknown--agentaborts rather than printing an empty queue. - Proof-first completion.
--done --proof <file|url|text>uploads attachments, records links, and appends notes in one call — so reviewers see evidence without digging. - Durable closes. If a
--donecall hits a Linear rate limit or transient error, the close intent is persisted to~/.linear-cli/queue/and retried with exponential backoff (or serverRetry-After).linear queue list/linear queue draininspect and apply pending closes; concurrent drains on one machine are serialized with a file lock. The nextlinear update --donealso drains automatically so the board stays honest. Never tight-loop bulk closes — use the queue andlinear queue drain(or--once) instead of a barefor id in …; do linear update $id --done; donewithout sleep. - JSON everywhere.
--jsonon every read command. Pipe tojqor hand to a subagent. - One call for a menu.
linear projects overview --jsonreturns the current cycle plus every project → milestone → open issue in a single document, so a menubar or dashboard renders from one command with no follow-up queries. Shape in Overview JSON.
linear projects overview --json is built for menus and dashboards: one call,
one document, nothing to join afterwards.
{
"generatedAt": "2026-09-10T09:33:05.127Z",
"cycle": {"id": "5e73…", "number": 28, "name": "first design partner live",
"startsAt": "2026-09-08T07:00:00.000Z", "endsAt": "2026-09-15T07:00:00.000Z"},
"projects": [
{
"id": "…", "name": "Mobile app", "priority": "high", "state": "started", "targetDate": null,
"members": [
{"id": "…", "name": "Ada", "displayName": "ada",
"email": "ada@example.com", "avatarUrl": "https://public.linear.app/…", "role": "lead"}
],
"milestones": [
{"id": "ba96…", "name": "Fleet reliability", "targetDate": "2026-08-12",
"issues": {"total": 14, "open": 3, "done": 11, "canceled": 0},
"open": [
{"identifier": "PHNX-3999", "title": "…", "state": "Doing", "cycle": 28,
"assignee": "Muqsit", "priority": 2, "url": "https://linear.app/…"}
]}
],
"noMilestone": {"issues": {"total": 1367, "open": 24, "done": 1039, "canceled": 304},
"open": []}
}
],
"partial": false,
"partialReason": null
}cycleis the team's current cycle (startsAt <= now < endsAt), ornullbetween cycles. An issue row'scycleis the cycle number, so "this cycle" isrow.cycle == doc.cycle.number;nullmeans not scheduled.issuescounts every issue in the bucket.open= state typebacklog,unstarted,started(andtriage);done=completed;canceled=canceled;open + done + canceled == total. Only open issues are listed — done and canceled are counts only.noMilestoneis the same{issues, open}pair for the project's issues that sit in no milestone.- Ordering: projects by priority (
urgent→low, thennone) then name; milestones by Linear's sortOrder; open rows by cycle number ascending (no cycle last), then priority (urgent first), then identifier. membersis the project's people for an avatar stack: the lead (role: "lead"), then explicit project members by name ("member"). Linear grants access per team, so this is who the project names, not who can open it. When a project sets neither, it falls back to the humans assigned to its issues in any state, most issues first ("assignee"). Agents are left out (they appear as an issue row'sdelegate).emailandavatarUrlcan benull. The people ride the project query, so the document still costs four requests; past 50 members on one project the list is cut andpartialsays so.priorityon a project is a word (urgent|high|medium|low|none); on an issue it is Linear's integer (1 = urgent … 4 = low, 0 = none).- Everything is paginated to the end. If a safety rail stops a walk (25k
issues),
partialistrueandpartialReasonsays where — the document never silently undercounts. An auth or network error exits non-zero. --project <name|id>restricts the document; repeat it for several. Names resolve strictly — a typo aborts with suggestions.
Every issue linear create makes resolves to one horizon:
| Horizon | When | Due date | Cycle | Milestone | State |
|---|---|---|---|---|---|
| now | default | by priority (policy.sla) |
active | --milestone, else the project's next open one |
Todo |
| milestone | --milestone M |
M's target date; urgent/high keep an earlier SLA date | active only if the date falls in it | M | Todo |
| backlog | --backlog |
none | none | a future one, default policy.backlogMilestone |
Backlog |
- An explicit
--due-date,--cycleor--statusalways wins. - "Next open milestone" is the project's earliest
targetDatestill under 100%, never the backlog milestone. Undated milestones are skipped. --bugadds label Bug and, without--blocker, means backlog at priority low.--bug --blockermeans now at priority high.- With an agent identity in config (
linear setup --agent claude), new issues are delegated to it while the assignee stays the API-key owner. - Dates are in the team's timezone (Linear's team setting), so a box running in UTC files the same "today" as the people on the team.
linear milestones set-target-datecarries along every open issue in that milestone whose due date equalled the old target, and prints the count.
The SLA table and the backlog milestone live in ~/.linear-cli/config.json.
Every key is optional; anything left out keeps the default shown here:
{
"policy": {
"sla": {
"urgent": {"due": "today", "remind": "15m"},
"high": {"due": "today", "remind": "1h"},
"medium": {"due": "+1wd", "remind": "none"},
"low": {"due": "cycle_end", "remind": "none"}
},
"backlogMilestone": "Q1 2027"
}
}due is today, +Nwd (N working days, Monday to Friday), +Nd (N calendar
days) or cycle_end (the active cycle's last day). remind is Nm, Nh or
none, and sets Linear's issue reminder that long after creation. Priority
none is scheduled like medium.
linear goals reads four goal levels straight from Linear: the company quarter
(the Active initiative and its milestones), the company week (the active
cycle's name), your week (an issue labelled Weekly goal in the active cycle)
and your day (an issue labelled Daily goal due today, whose sub-issues are
the day's to-dos). --json prints one document:
{
"generatedAt": "2026-10-09T05:52:07Z",
"company": {
"initiative": {"id": "…", "name": "First paying customers", "url": "https://linear.app/…",
"startDate": "2026-08-02", "targetDate": "2026-12-31"},
"logoUrl": null,
"milestones": [{"id": "…", "name": "Ready to charge", "project": "Phoenix",
"targetDate": "2026-10-30", "progress": 0}]
},
"week": {
"cycle": {"id": "…", "number": 32, "name": "Launch gate green",
"startsAt": "2026-10-06T07:00:00.000Z", "endsAt": "2026-10-13T07:00:00.000Z"},
"goal": "Launch gate green"
},
"me": {
"user": {"id": "…", "name": "Ada", "avatarUrl": null},
"weekGoal": null,
"dayGoal": {"id": "…", "identifier": "ANT-50", "title": "Stripe webhook green",
"url": "https://linear.app/…", "dueDate": "2026-10-08",
"milestone": {"name": "Ready to charge", "targetDate": "2026-10-30"},
"done": 1, "total": 2,
"todos": [{"id": "…", "identifier": "ANT-51", "title": "Write the refund test",
"url": "https://linear.app/…", "completed": true}]}
},
"team": [{"user": {"id": "…", "name": "Grace", "avatarUrl": null},
"weekGoal": null, "dayGoal": null}]
}initiativeis the Active initiative (the one due first if there are several), ornull.startDateis its start date if set, else the day it was created.milestonesare the open milestones (under 100%) of the initiative's projects with a target date on or before the initiative's, earliest first, without the backlog milestone.progressis Linear's 0-100.week.goalis the cycle name, ornullwhen it is empty or still Linear's defaultCycle N.teamis every other active human (no agents, no guests).- A Goal's
todosare its sub-issues, canceled ones left out;done/totalcount them. A duplicate goal for the same person resolves to the newest. - Dates use the team's timezone.
The write commands (set week|day|company-week, todo, todo --done) take
--json and print the resulting Goal (set company-week prints week).
A new week goal is due on the cycle's last day, a new day goal today; both land
in --milestone or, by default, the company's next open milestone. Errors go to
stderr with exit 1.
Drop skill.md into your agent's skills directory (e.g. ~/.claude/skills/linear/skill.md) to teach Claude / Codex / Gemini how to use the CLI without you explaining it every session.
The Linear CLI space already has options. This one is built for human-owned queues where agents execute delegated work and must prove completion. The zero-dependency footprint is the supporting advantage: easy to install, easy to audit, and cheap for subagents to call repeatedly.
$ grep -E '^(import|from)' linear | sort -u
from __future__ import annotations
from pathlib import Path
from urllib.error import URLError
from urllib.request import Request, urlopen
import argparse
import json
import mimetypes
import os
import subprocess
import sys
Every symbol is in the Python standard library. No pip install. No npm install. No cargo build. No Deno. The whole tool is one ~118 KB file you can read top-to-bottom.
| Tool | Runtime | Deps | Install footprint | Last published |
|---|---|---|---|---|
| linear-cli (this) | Python 3.9+ stdlib | 0 | ~118 KB, 1 file | active |
@linear/cli (official) |
Node | 0 | 5 MB npm pkg | Nov 2021 (abandoned) |
| Linearis | Node | @linear/sdk + commander |
27 MB node_modules |
2025 |
| schpet/linear-cli | Deno | 25+ imports (cliffy, graphql-codegen, unified, valibot…) | Deno + codegen | active |
| Finesssee/linear-cli | Rust toolchain | 28 crates (tokio, reqwest, clap, keyring…) | compiled binary | active |
| Linear's hosted MCP server | remote | — | 0 local, but 13k+ tokens injected into every agent turn | active |
Numbers verified against each project's package.json / Cargo.toml / deno.json via the npm registry.
| Capability | linear-cli | @linear/cli |
Linearis | schpet | Finesssee | Linear MCP |
|---|---|---|---|---|---|---|
Native agent delegation (assignee stays human, delegateId becomes agent) |
yes | ? | ? | ? | ? | ? |
Proof-first completion (--done requires --proof) |
yes | ? | ? | ? | ? | ? |
Bulk issue updates (ANT-1 ANT-2, --stdin) |
yes | ? | ? | ? | ? | ? |
Bulk JSONL create (--from-file) |
yes | ? | ? | ? | ? | ? |
| Full pagination for list/search paths | yes | ? | ? | ? | ? | ? |
? means the competitor may support it, but this README is not claiming that without a source-level verification. The point is the positive contract this CLI exposes for agents: native delegation, proof-first close, bulk-safe commands, and no silent 50-item cap.
This is the boring superpower of having no dependencies.
Every notable CLI supply chain attack of the last few years — event-stream, colors/faker, ua-parser-js, node-ipc, the xz backdoor, polyfill.io, the dozens of npm typosquats caught monthly — happened through a compromised dependency, not the tool itself. linear-cli has none. The full audit surface is:
- The ~3,000 lines of Python in this repo (read it:
linear) - Python's standard library
- Linear's own GraphQL API at
api.linear.app
That's it. No npm install running 200 postinstall scripts. No transitive dependency eight layers deep that you've never heard of. No package-lock.json to audit, no Cargo.lock to chase, no Deno permissions matrix. If you trust this repo and Python's stdlib, you're done auditing.
It's probably the most boring CLI you'll ever security-review. That's the point.
Linear ships a first-party MCP server at mcp.linear.app/mcp. It's great for interactive chat. Less great for agents doing volume work:
- MCP injects the full tool catalog into every turn. Industry data: 40–50% of the context window is consumed by tool schemas before the agent does anything. The Linearis author cites ~13k tokens for Linear's MCP alone. That was the explicit reason he built a CLI.
- Benchmarks show CLI tools completing the same tasks ~33% more token-efficiently than equivalent MCP servers.
linear tasks --jsonreturns exactly the bytes you asked for. That's the whole point.
If you want the MCP, use it. If you want a subagent to burn through 50 tickets without blowing its context on schema chatter, use this.
- Python 3.9+ (ships with every macOS since 11, every Ubuntu since 20.04)
- A Linear API key with Full access
Yes. If your agent has its own Linear account (many teams provision one seat per agent — e.g. claude@yourcompany.com, codex@yourcompany.com), run linear setup --api-key <agent's own key> without --agent. Then linear tasks returns tickets assigned directly to the agent's user — the assignee IS the lane.
Use --agent <name> when your agents are installed as Linear app users and share a human's seat; skip it when each agent has its own account. To query any assignee on demand, linear tasks --assignee me|none|<email>.
Run linear migrate-agent-labels. It is a dry run by default and prints exactly what it would do: which issues get a delegate, which labels get stripped, and which labels get deleted once nothing carries them. --apply writes.
It never overwrites an existing delegate. An issue already delegated to someone other than its label claims — or carrying two agent:* labels that name different agents — is reported as a CONFLICT and left untouched. A label whose suffix is not a delegatable agent (a machine name, a workflow flag) is reported as UNRESOLVED and kept, along with any sibling label on the same issue: mixed state needs a human, and migrating half of it silently is worse.
Each issue gets at most one write, so two labels on one issue can't resurrect each other. A label is deleted only when nothing carries it workspace-wide (archived issues included), not merely nothing in the scanned team — and a label carried by more than 200 issues is kept outright, because a truncated carrier list can't prove a label is free.
--apply exits non-zero if anything was left behind — a conflict, an unresolved label, or a failed write. A dry run exits 0 and tells you what needs a human; it is an inspection, not a migration.
If you install agents as Linear app integrations (Settings → API → Applications, authorized with actor=app), they appear as first-class members. linear agents auto-detects them (cached, refreshed every 6h; --refresh to force). Hand work to one with linear update <ID> --delegate <name> — delegation is the supported path, since Linear silently ignores an app user set as a plain assigneeId. The human stays the assignee; the agent becomes the delegate.
Revoke the old key at linear.app/settings/account/security and run linear setup --api-key <new-key> again. The config file is overwritten in place.
Untested. The script is plain Python + urllib + subprocess, so it should run under WSL or Git Bash. The only macOS-specific bit is the Keychain fallback in get_api_key (via the security binary) — on other platforms it silently skips, so use the env var or config file.
Because every Mac and every modern Linux already ships it. Zero install, zero toolchain, zero npm audit churn. The tradeoff is startup isn't as fast as a compiled binary (~170 ms vs ~10 ms), but for a tool invoked a handful of times per ticket that's noise.
benchmark.py compares two CLI files against your configured Linear workspace.
It runs read-only task, board, overview and goals commands, plus a help startup
control. Each sample starts a fresh Python process and includes startup,
network, rendering and measurement overhead. Five paired runs alternate order;
the JSON lines include every sample, medians, ranges and GraphQL request counts.
python3 benchmark.py --before /path/to/base/linear --after ./linear --project "Your project" --runs 5Only timings, counts and SHA-256 output digests are printed. JSON comparisons
ignore generatedAt and object key order; changes to the workspace during the
run can still change the digest. response_bytes measures reserialized response
JSON, not wire traffic. Timings depend on workspace size and network conditions.
Overview overlaps independent reads with at most two requests in flight; goals uses at most three. Each cursor walk stays sequential. GraphQL has a 20-second socket timeout, not a deadline for the entire command or paginated walk. A stalled request returns an API error; mutations are not automatically retried by the GraphQL client.
MIT.