Single-binary Linear client built with Zig 0.16.0. Uses stdlib only, defaults to human-readable tables with a --json override, and stores auth securely at ~/.config/linear/config.json (0600). Use linear help <command> to see command-specific flags and examples.
- Build:
zig build -Drelease-safe(debug is default). Binary installs tozig-out/bin/linear. - Tests:
zig build test. Online suite runs withLINEAR_ONLINE_TESTS=1:LINEAR_ONLINE_TESTS=1 LINEAR_TEST_TEAM_ID=<TEAM_ID> zig build online(requiresLINEAR_API_KEY; optionalLINEAR_TEST_ISSUE_ID,LINEAR_TEST_PROJECT_ID,LINEAR_TEST_MILESTONE_ID; opt-in mutations withLINEAR_TEST_ALLOW_MUTATIONS=1).
- Quick start (uses a temp config):
export LINEAR_API_KEY=<paste key> export LINEAR_ONLINE_TESTS=1 export LINEAR_TEST_TEAM_ID=<team-id> # Optional for broader coverage: # export LINEAR_TEST_ISSUE_ID=<identifier like ENG-123> # export LINEAR_TEST_PROJECT_ID=<project id> # export LINEAR_TEST_MILESTONE_ID=<milestone id> # export LINEAR_TEST_ALLOW_MUTATIONS=1 # enables create/delete tests rm -f /tmp/linear-cli-qa.json echo "$LINEAR_API_KEY" | ./zig-out/bin/linear --config /tmp/linear-cli-qa.json auth set ./zig-out/bin/linear --config /tmp/linear-cli-qa.json auth test zig build test LINEAR_ONLINE_TESTS=1 LINEAR_TEST_TEAM_ID=$LINEAR_TEST_TEAM_ID zig build online - Finding IDs quickly:
- Team:
./zig-out/bin/linear --config /tmp/linear-cli-qa.json teams list --json | jq -r '.nodes[0].id' - Issue identifier:
./zig-out/bin/linear --config /tmp/linear-cli-qa.json issues list --limit 1 --quiet - Project/milestone:
./zig-out/bin/linear --config /tmp/linear-cli-qa.json issues list --include-projects --fields project,milestone --limit 1 --json - Label / assignee / workflow-state ids (for
--label,--assignee,--state-id):./zig-out/bin/linear --config /tmp/linear-cli-qa.json labels list --team <TEAM_KEY> --quiet,... users list --quiet,... states list --team <TEAM_KEY> --quiet - If no suitable issue exists and mutations are allowed:
LINEAR_TEST_ALLOW_MUTATIONS=1 ./zig-out/bin/linear --config /tmp/linear-cli-qa.json issue create --team <TEAM_KEY_OR_ID> --title "CLI QA seed" --quiet
- Team:
- Config path:
~/.config/linear/config.json(override with--config PATHor envLINEAR_CONFIG). - API keys must be 4-512 characters from
[A-Za-z0-9_-]; anything else is rejected at every ingestion point (config file,LINEAR_API_KEY,auth set, and every credential backend). - Keys:
api_key— plaintext, deprecated; see Credential backendscredential_helper— argv array for an external command that prints the keydefault_team_id(default ``)default_output(table|json, defaulttable)default_state_filter(default["completed","canceled"])team_cache(auto-populated key->id cache fromissue create)
- Manage defaults without editing JSON:
linear config show,linear config set default_team_id ENG,linear config set default_output json,linear config unset default_state_filter config set default_team_idverifies the team against the workspace before writing. An unknown team is refused and nothing is saved (team 'X' not found in workspace); a lookup that could not complete is reported separately (could not verify team 'X') so a timeout is never mistaken for a verdict. There is no--force: use a per-command--teamwhile offline.- Files are created with 0600 perms inside a 0700 directory; the CLI warns if permissions drift.
auth showprints a redacted key by default (--redactedis kept as a no-op alias);auth show --revealprints the full token and is refused unless stdout is a terminal.auth testpingsviewerto validate the current key.
Resolution order, highest precedence first:
LINEAR_API_KEY -> credential_helper -> keychain (macOS) -> config file (deprecated)
Run linear auth status to see which one is actually supplying the key. It never prints the key itself, so it is safe in a terminal recording or a bug report. It is also fully offline: format-valid (unverified) means the key matched the expected charset and length, not that Linear accepted it. linear auth test is the only command that round-trips the credential against the API.
Only the config file is ever written back to disk. A key that came from the environment, a helper, or the keychain is never persisted, and a key already stored in the config file survives saves made while any of them is supplying the effective value.
credential_helper — an external command whose stdout is the API key. This is the portable backend: it works on every platform, and it covers 1Password, pass, gopass, secret-tool, Vault, and CI secret managers without any provider-specific code.
- The array form is the documented one. It is argv, not a command line: no shell is spawned, so quotes, pipes,
;,$VAR, and globs are ordinary bytes inside an argument, never syntax. - A bare string (
"credential_helper": "pass show linear/api-key") is accepted for convenience and split on whitespace with the same no-shell rule. Quoting is not supported — use the array form when an argument contains a space. - Configure it from the CLI with
linear config set credential_helper "op read op://Private/Linear/api-key"— the string is split into argv by the same whitespace rule. The helper is run once before it is stored and must hand back a usable key; one that fails, prints nothing, or prints something that is not a key is refused and nothing is written. That check exists because a stored-but-broken helper clears the effective key instead of falling through, so saving one would lock you out. Bounds apply before anything is spawned: at most 16 arguments, 1024 bytes each. Nothing the helper writes to stdout is ever printed. - This is the bootstrap path that keeps the key off disk entirely: put the key in your secret manager, point a helper at it, done. Nothing else needs to happen — there is no separate move-it-off-disk step, because the key was never on disk.
- Trailing whitespace and the newline every secret manager emits are trimmed, and the result is validated before it can reach an
Authorizationheader. - Failures are reported by exit status and stderr. The helper's stdout is never logged, printed, or quoted in a diagnostic — that is where the secret is.
- A helper that fails does not fall through to the config file. That would silently reinstate the plaintext key the helper was configured to replace, so the chain stops and the CLI reports the failure instead.
linear config unset credential_helperstill works in that state. - Bounded: 30s timeout, 4 KiB of output. A hung helper is killed rather than hanging the CLI.
macOS keychain — used when no helper is configured. Write it with linear auth set --to keychain, which reads the key from piped stdin or a no-echo prompt and hands it to /usr/bin/security -i on stdin; the key never appears in an argv, where any process on the machine could read it out of the process table. The write is read back and compared before the command reports success. Reads run /usr/bin/security find-generic-password -w -s linear-cli -a api-key; the secret comes back on stdout, so nothing sensitive is in argv there either. A missing item is not an error, it just means the next backend gets its turn. On Linux and Windows this backend does not exist — --to keychain fails there rather than quietly writing the plaintext file.
op read op://Private/Linear/api-key | linear auth set --to keychain
linear auth status # source: keychainWhat the keychain actually buys you: encryption at rest and immunity to accidental disclosure — backups, synced folders, a stray cat ~/.config/linear/config.json, a screen share. It is not process isolation. The item is created through security, so it is ACL'd to /usr/bin/security, and any process running as you can read it back non-interactively without a prompt.
Getting off the plaintext file:
There is no migrate command. A key that has been in config.json has been on disk in cleartext, and no amount of overwriting beats copy-on-write filesystems, snapshots, Time Machine, or synced folders — so it has to be rotated regardless. Once you are rotating it anyway, moving the old key buys nothing over starting fresh, and starting fresh also clears the rest of the file's accumulated state.
# 1. Put the key in a secret manager, then rotate the old one in Linear. It has
# been on disk in cleartext: treat it as disclosed.
# 2. Delete the config file. It also holds default_team_id and team_cache, so
# those reset too.
rm ~/.config/linear/config.json
# 3. Set up again from scratch.
linear config set credential_helper "op read op://<vault>/<item>/<field>"
# or, on macOS: op read "op://<vault>/<item>/<field>" | linear auth set --to keychain
linear config set default_team_id TEAM_KEY
linear auth status && linear auth testDelete before you configure, not after: credential_helper is stored in config.json, so setting it first and deleting the file afterwards would throw the new helper away with the old key. --to keychain is the exception — it writes nothing to the config file — but the order above works for both.
Global flags:
--json— force JSON output (default followsconfig.default_output)--config PATHor envLINEAR_CONFIG— choose config file--endpoint URL— override the GraphQL endpoint (useful for QA/mocking). Must behttpson hostapi.linear.app; setLINEAR_ALLOW_INSECURE_ENDPOINT=1to point at a local mock. Rejected endpoints exit non-zero before any request (the Authorization header is never sent off-allowlist).--retries N— retry 5xx responses up to N times with a small backoff--timeout-ms MS— request timeout flag (plumbed for future enforcement)--no-keepalive— disable HTTP keep-alive reuse--help/--version— version includes git hash and build modelinear help <command>— show command-specific help with examples
Commands:
auth set [--to keychain|file]— store a key, read from piped stdin or a no-echo interactive prompt. There is no--api-keyflag; keys are never accepted on argv. Fails when no key is supplied instead of persistingLINEAR_API_KEY.--to keychainwrites the macOS keychain throughsecurity -i(stdin, never argv) and verifies a read-back; it fails on other platforms rather than falling back.--to fileis the default and is deprecated: it stores the key inconfig.jsonin plaintext and every run that reads it says so. Preferconfig set credential_helper, which keeps the key off disk entirely.auth status [--json]— report which backend supplied the key and whether it is well-formed, without ever printing the key and without touching the network.key: present (format-valid, unverified)is a charset/length check only — runauth testto verify the credential itself. JSON reportskey_present,key_format_valid, andkey_verified(alwaysnull; status never verifies). Exits non-zero when no well-formed key was found.auth test— pingviewerto validate the key.auth show [--reveal]— view the configured key; redacted unless--revealis passed on a terminal.config show|set|unset— view or update CLI defaults (team/output/state filter) without editing JSON.credential_helpercan be set, shown, and unset here; setting it runs the helper first and refuses to save one that does not return a usable key.me— show current user.teams list [--fields id,key,name] [--plain] [--no-truncate]— list teams with optional column and formatting controls.labels list [--team ID|KEY] [--limit N] [--max-items N] [--cursor CURSOR] [--pages N|--all] [--fields id,name,color,description,team] [--plain] [--no-truncate] [--quiet] [--data-only]— list issue labels; supplies the raw ids required byissues list --labelandissue create --labels.users list [--limit N] [--max-items N] [--cursor CURSOR] [--pages N|--all] [--include-inactive] [--fields id,name,display_name,email,active] [--plain] [--no-truncate] [--quiet] [--data-only]— list workspace members (active only unless--include-inactive); supplies the raw ids required byissues list --assigneeandissue create --assignee.states list [--team ID|KEY] [--limit N] [--max-items N] [--cursor CURSOR] [--pages N|--all] [--fields id,name,type,position,team] [--plain] [--no-truncate] [--quiet] [--data-only]— list workflow states; supplies the raw ids required byissues list --state-idandissue create --state. Workflow states are per-team and several teams commonly share state names, so theTeamcolumn is on by default to keep an unfiltered listing unambiguous.search <query> [--team ID|KEY] [--search-fields title,description,comments] [--fields identifier,title,state,assignee,priority,updated] [--state-type TYPES] [--assignee USER_ID|me] [--limit N] [--max-items N] [--cursor CURSOR] [--pages N|--all] [--case-sensitive] [--plain] [--no-truncate] [--quiet] [--data-only]— server-side search over titles/descriptions/comments or identifiers (identifier filter matches issue numbers). Paginates with cursor support plus page summaries, like the other list commands.- The two field flags are separate:
--search-fieldschooses what is searched (title,description,comments; defaulttitle,description),--fieldschooses which columns are printed, exactly as onissues list.--fieldsaccepts the six columnssearchfetches —identifier,title,state,assignee,priority,updated;parent,sub_issues,projectandmilestoneare not on a search response and are rejected with a pointer atissues listrather than printed as empty columns. --fieldsused to mean "which fields to search". That meaning moved to--search-fieldsoutright — there is no deprecated alias. A search-only value left in--fields(description,comments) is rejected locally, before any request, with a message naming--search-fields.titleis the one value both flags accept, so a bare--fields titleruns as a projection and prints a one-line note on stderr pointing at--search-fields.
- The two field flags are separate:
download <URL> [--output FILE]— fetch anuploads.linear.appattachment;--output -streams to stdout, otherwise the file is created 0600 likeissue view --attachment-dirwrites, since both carry private issue content.issues list [--team ID|KEY] [--state TYPES] [--created-since TS] [--updated-since TS] [--project ID] [--milestone ID] [--sort FIELD[:asc|desc]] [--sort-nulls first|last] [--limit N] [--max-items N] [--sub-limit N] [--cursor CURSOR] [--pages N|--all] [--fields ...] [--include-projects] [--plain] [--no-truncate] [--human-time]— defaults to the config team; excludes completed/canceled unless--stateis provided; project/milestone filters available; parent/sub-issue columns stay opt-in — thechildrensub-query is only sent whensub_issuesis in--fieldsor--sub-limitis passed explicitly, and--sub-limit 0disables it outright;--include-projects(or fields) adds project/milestone context;--max-itemsstops mid-page when needed; paginates with cursor support plus page summaries.-
--sorttakes any field Linear'sIssueSortInputaccepts, matched case-insensitively:priority,estimate,title,label,labelGroup,slaStatus,createdAt,updatedAt,completedAt,dueDate,accumulatedStateUpdatedAt,cycle,milestone,assignee,delegate,project,team,manual,workflowState,customer,customerRevenue,customerCount,customerImportantCount,rootIssue,linkCount,release.created/updatedremain aliases forcreatedAt/updatedAt, and the direction suffix defaults todesc. An unknown field is rejected locally — the error prints the accepted list and no request is sent — andlinear help issuesprints the same list. -
--sort-nulls first|lastdecides where issues with no value for the sort field land. It matters because most of the vocabulary is routinely null —dueDate,completedAt,cycle,milestone,slaStatus,delegate,release, and everycustomer*field on a non-customer issue — so a bounded page can fill up with nulls before the first real result:# Without it, a page of undated issues can arrive before the first deadline linear issues list --team ENG --sort dueDate:asc --limit 20 # Soonest deadline first, undated issues pushed to the end linear issues list --team ENG --sort dueDate:asc --sort-nulls last --limit 20
The value is matched case-insensitively (
LASTworks) and always sent lowercase, which is what Linear'sPaginationNullsexpects — note thatorderin the same object is capitalised (Ascending/Descending). Omitting the flag sends nonullsat all, leaving placement to the server default, so existing invocations are unchanged.--sort-nullswithout--sortis an error, and an unknown value is rejected locally with the accepted values printed — neither costs a request.
-
issue view [ID|IDENTIFIER] [--fields LIST] [--quiet] [--data-only] [--human-time] [--sub-limit N] [--attachment-dir DIR]— show a single issue;--fieldsfilters output (identifier,title,state,assignee,priority,url,created_at,updated_at,description,project,milestone,parent,sub_issues);--sub-limitcontrols sub-issue expansion when requested;--quietprints only the identifier,--data-onlyemits tab-separated fields or JSON. Attachment download is opt-in: nothing is written to disk unless--attachment-dir DIRis passed, and downloaded files are created 0600.issue create --team ID|KEY --title TITLE [--description TEXT|--description-file PATH] [--priority N] [--state STATE_ID] [--assignee USER_ID] [--labels ID,ID] [--yes] [--quiet] [--data-only]— resolves team key to id when needed, caches lookups, and returns identifier/url; requires--yes/--forceto proceed (otherwise exits with a message).issue update [ID|IDENTIFIER] [--assignee USER_ID|me] [--parent ID|IDENTIFIER] [--state STATE_ID|NAME] [--priority N] [--title TEXT] [--description TEXT|--description-file PATH] [--project PROJECT_ID] [--yes] [--quiet] [--data-only]— updates one or more fields; requires--yes/--force.issue delete <ID|IDENTIFIER> [--yes] [--dry-run] [--quiet] [--data-only]— archives an issue by id/identifier; requires--yes/--forceto proceed;--dry-runvalidates the target without sending the mutation and echoes the identifier/title. Accepts--bulk ID,ID,--bulk-file PATH, or--bulk-stdininstead of the positional identifier (see Bulk deletes). There is no--reason: Linear'sissueDeletetakes only(id, permanentlyDelete), so a reason could never be recorded on their side — record it yourself alongside the CLI's output.issue comment [ID|IDENTIFIER] --body TEXT|--body-file PATH [--parent COMMENT_ID] [--yes]— add a comment;--parentposts a threaded reply.issue comment list [ID|IDENTIFIER] [--limit N] [--max-items N] [--cursor CURSOR] [--pages N|--all] [--fields id,author,body,created_at,updated_at,parent,url] [--plain] [--no-truncate] [--quiet] [--data-only]— read an issue's comments (includingparentso thread structure survives a round trip). Paginates over the nestedIssue.commentsconnection, so--cursortakes anendCursorfrom.issue.comments.pageInfo.issue comment update <COMMENT_ID> --body TEXT|--body-file PATH [--yes] [--quiet] [--data-only]/issue comment delete <COMMENT_ID> [--yes] [--quiet] [--data-only]— edit or remove a comment; both require--yes/--force.project create --name NAME --team ID|KEY [--description TEXT] [--content TEXT|--content-file PATH] [--start-date DATE] [--target-date DATE] [--state STATE] [--yes]/project update <ID> [--name NAME] [--description TEXT] [--content TEXT|--content-file PATH] [--start-date DATE] [--target-date DATE] [--state STATE] [--yes]— Linear capsProject.descriptionat 255 characters, so long-form project text belongs in--content/--content-file, which map to the separateProject.contentfield.issue start [ID|IDENTIFIER] [--branch NAME] [--from-ref REF] [--yes] [--quiet] [--data-only]— checks out the issue's git branch and moves the issue into the team's firststartedworkflow state; requires--yes/--force.issue pr [ID|IDENTIFIER] [--base BRANCH] [--head BRANCH] [--draft] [--web] [--yes]— runsgh pr createwith<IDENTIFIER> <title>as the PR title and the Linear issue URL as the body; requires--yes/--force.issue id|url|title [ID|IDENTIFIER]— print one field on a single line, for shell substitution.issue idmakes no API request.issue describe [ID|IDENTIFIER] [--references]— print a commit message body withLinear-issue/Linear-issue-urltrailers, e.g.git commit -m "$(linear issue describe)".milestone list [--project ID|NAME] [--limit N] [--max-items N] [--cursor CURSOR] [--pages N|--all] [--fields id,name,target_date,sort_order,description,project] [--plain] [--no-truncate] [--quiet] [--data-only]— list milestones; supplies the raw ids required byissues list --milestone.--projectis optional: without it the listing spans every project in the workspace, with it the same query is filtered to that project.--projecttakes a project uuid, slug, or exact name; an ambiguous name is an error rather than a silent pick. Milestone names repeat across projects, so theProjectcolumn is on by default to keep an unfiltered listing unambiguous.milestone view <ID> [--quiet] [--data-only]— show one milestone with its project, target date, sort order, and description.milestone create --project ID|NAME --name NAME [--description TEXT|--description-file PATH] [--target-date DATE] [--sort-order N] [--yes] [--quiet] [--data-only]/milestone update <ID> [--name NAME] [--description TEXT|--description-file PATH] [--target-date DATE] [--sort-order N] [--yes] [--quiet] [--data-only]— create or edit a milestone; both require--yes/--force, andupdaterequires at least one field flag.--sort-orderis a float (lower sorts first).milestone delete <ID> [--yes] [--dry-run] [--quiet] [--data-only]— delete a milestone; requires--yes/--force. Also accepts--bulk/--bulk-file/--bulk-stdin(see Bulk deletes).gql [--query FILE] [--vars JSON|--vars-file FILE] [--operation-name NAME] [--fields LIST] [--data-only] [--paginate] [--max-pages N] [--yes] [--dry-run]— arbitrary GraphQL; non-zero on HTTP/GraphQL errors. Documents containing a mutation operation require--yes/--force, matching the other mutating commands;--dry-runreports the operation without sending it and works with no API key configured (it makes no request).--paginatewalks one connection to the end (see GraphQL auto-pagination).
issue view, issue update, issue comment, issue comment list, issue link, issue start, issue pr, issue id, issue url, issue title, and issue describe all take the issue identifier as an optional positional. When it is omitted the issue is inferred from the current branch: git symbolic-ref --short HEAD, then the leftmost \b([A-Za-z0-9]+)-([1-9][0-9]*)\b token in the branch name with the team key uppercased. A detached HEAD, a branch with no identifier, a missing git, and "not a git repository" each produce their own one-line diagnostic and a non-zero exit — the CLI never guesses.
issue start asks Linear for the issue's own branchName rather than slugifying the title locally, so branches match the ones Linear's "copy git branch name" button produces. --branch NAME overrides it, --from-ref REF bases a newly created branch on REF.
Everything this CLI spawns is built as an explicit argv array and executed without a shell: git symbolic-ref --short HEAD, git rev-parse --is-inside-work-tree, git rev-parse --verify <branch>, git checkout <branch>, git checkout -b <branch> [<from-ref>], and gh pr create --title <title> --body <url> [--base B] [--head H] [--draft] [--web]. Issue titles and branch names are attacker-influenceable, so they only ever reach a child process as one discrete argument, and ref arguments that start with - or contain whitespace/control characters are rejected before the spawn. gh inherits stdio so its interactive prompts keep working; git output is captured and forwarded to stderr so stdout stays machine-readable.
issue delete and milestone delete accept a batch instead of a positional id: --bulk ID,ID,..., --bulk-file PATH (one id per line or comma separated; - reads stdin), or --bulk-stdin. Only one source may be given, and combining a source with a positional id is an error.
The batch runs serially — one request at a time, in input order. These are destructive mutations, nothing else in this CLI runs concurrently, and a predictable order of deletes is worth more than the wall-clock saving a concurrent batcher would buy.
- Ids are deduplicated (first-seen order wins) so the same delete is never sent twice.
- A failed item is reported and the run continues; it does not abort the batch.
- A summary line (
issue delete: bulk complete; 2 succeeded, 1 failed) goes to stderr, and is suppressed under--jsonso stdout stays the only thing a consumer parses. - The exit code is non-zero when any item failed.
--dry-runcomposes: a bulk dry run resolves and validates every id and sends no mutation.--quietand--data-onlykeep their per-item meaning; under--jsonthe per-item documents are wrapped in a single JSON array.- A run is capped at 500 ids so a
--bulk-filepointed at the wrong file fails with a diagnostic instead of firing thousands of deletes.
linear issues list --team ENG --state canceled --quiet | linear issue delete --bulk-stdin --dry-run
linear issue delete --bulk ENG-1,ENG-2 --yes --quietlinear gql --paginate follows one connection's cursor to the end and merges every page's nodes into the first page's document, so the output has the same shape as a single-page response with a longer nodes array and the final page's pageInfo.
The query must declare an $after variable, pass it to the connection, and select pageInfo { hasNextPage endCursor }. All three are checked against the document before the first request, so a query that cannot be walked fails with a diagnostic instead of quietly returning one page. Mutations are rejected outright.
The connection is discovered in the response breadth-first, so the shallowest object selecting both nodes and pageInfo is the one that gets walked; a connection nested inside an array is not eligible (there is no single stable path to merge into). --max-pages N bounds the walk (default 20) so a malformed query cannot loop forever; hitting the bound prints a warning on stderr and still returns the pages it collected with hasNextPage: true intact.
linear gql --query issues.graphql --vars '{"teamId":"..."}' --paginate --max-pages 5 --data-onlyEvery text field that can carry multi-KB markdown accepts a companion --*-file PATH flag (--description-file, --body-file, --content-file). PATH may be - to read stdin. This avoids the shell-quoting corruption that argv-passed bodies are prone to (embedded quotes, newlines, $). Passing both the inline flag and its -file companion is an error rather than a silent precedence rule, and inputs above 1 MiB are rejected with an explicit diagnostic.
- Tables for lists; key/value blocks for detail views.
--jsonprints parsed JSON (gql honors--fieldswhen present).issues list --jsonadds top-levelpageInfoplus limit/sort metadata (andmaxItemswhen set);--data-only --jsonemits a nodes array with a siblingpageInfo.--plaindisables padding/truncation;--no-truncatekeeps full cell text.--human-timerenders issue timestamps relative to now.--data-onlyonissue view|createemits tab-separated fields (or JSON);--quietprints only the identifier. Ongql,--data-onlystrips the GraphQL envelope.issue comment listfolds newlines and tabs inside a comment body to spaces for table and tab-separated output, because both are single-line record formats;--json(with or without--data-only) returns bodies verbatim.- Bulk deletes wrap their per-item
--jsondocuments in one array and move the batch summary to stderr, so--jsonoutput stays a single parseable document.
- Endpoint:
https://api.linear.app/graphql. - Auth header:
Authorization: <key>(no Bearer). - Shared HTTP client with keep-alive (toggle with
--no-keepalive). - Retries 5xx responses with a small backoff; timeout flag is wired for future use.
- Surfaces HTTP status and first GraphQL error when available; 401s nudge to set
LINEAR_API_KEYor runauth set.
- Default team id: ``.
- Default output: table.
- Default state exclusion:
completed,canceled. - Pagination:
issues listandsearchfetch 25 items per page, the other list commands 50;--cursor,--pages, and--alldrive additional page fetches,--max-itemscaps the total (it may truncate mid-page), and stderr reports fetched counts plus the--cursorvalue to resume from. The summary is suppressed under--json. Every list command paginates, includingsearchandissue comment list; the latter walks a nested connection, so its cursors come from.issue.comments.pageInfo.
Install as a Claude Code plugin to let Claude manage Linear issues for you.
Install the linear binary first:
npm install -g @0xbigboss/linear-cli
linear auth set # configure your API key1. Add the marketplace:
/plugin marketplace add https://github.com/0xbigboss/linear-cli
2. Install the plugin:
/plugin install linear-cli@linear-cli-marketplace
3. Restart Claude Code to load the plugin.
The plugin provides a skill that teaches Claude how to use the Linear CLI. Once installed, Claude will automatically use the CLI when you ask about:
- Listing, viewing, or creating issues
- Managing teams and projects
- Linking issues, adding attachments, or comments
- Any Linear-related task
Example prompts:
- "List my Linear issues"
- "Create an issue in the ENG team titled 'Fix login bug'"
- "Show me issue ENG-123"
- "Link ENG-123 as blocking ENG-456"
{ "credential_helper": ["op", "read", "op://Private/Linear/api-key"] }