linear-remote
Project-scoped CLI that wraps the Linear GraphQL API into a read-first, JSON-output tool. Stdlib-only Python 3 (no pip, no venv). Designed for agent-driven Linear work — one call returns the full issue + comments + relations + state picture, name-resolved, with a first-class --from-markdown path for the "turn this design doc into an issue" workflow.
Lives at ${CLAUDE_SKILL_DIR}/../../bin/linear-remote; the plugin auto-adds bin/ to PATH, so just run linear-remote ....
Prerequisites
LINEAR_API_KEY set (project .env / .env.local or shell env). Get one at https://linear.app/settings/api. Personal API keys only — this plugin deliberately avoids the OAuth browser flow that the MCP requires.
- Optional:
LINEAR_TEAM_ID — a team UUID or key (e.g. LOA) to omit --team on every call.
The CLI bails with a clear missing-config message if LINEAR_API_KEY is absent.
When to reach for this
- User says "add this to linear as an issue" with a chunk of markdown → write the markdown to a file, then
issues create --from-markdown <path> --team <name>. YAML frontmatter (team:, project:, labels:, priority:, assignee:) is respected. First H1 becomes the title, rest becomes the body.
- User asks "what's LOA-229 about" → run
issues get LOA-229 --include-comments --include-relations --pretty. One call → title, body, state, assignee, labels, project, parent, comments, and blocking/blocked-by relations.
- User asks "list my in-progress work on " → run
issues list --project <name> --state 'In Progress' --assignee <email>.
- User wants a project status snapshot → run
projects overview '<name>' --pretty. State-bucketed issues (backlog / todo / inProgress / inReview / done / canceled), milestones, recent activity, completion %.
- User says "move LOA-229 to In Review" → run
issues move LOA-229 'In Review'. Add --wait when the transition is downstream of a GitHub PR merge (Linear's webhook auto-moves to "Done"). 60s default timeout, 3s poll, partial JSON on timeout.
- User wants to comment on an issue → run
comments add LOA-229 @./review-notes.md (the @path form reads body from a file).
Headline commands
linear-remote issues get <id-or-query> [--include-comments] [--include-relations]
linear-remote issues list [--project ...] [--state ...] [--assignee ...] [--team-filter ...] [--label ...] [--limit 25] [--cursor <c>]
linear-remote issues search <query> [--limit 25]
linear-remote issues create --title <t> --team <name> [--project <n>] [--body "..."|--body @file] [--labels a,b] [--priority 0-4] [--assignee <email>]
linear-remote issues create --from-markdown <path> [--team <name>] # killer feature
linear-remote issues update <id> [--title ...] [--state ...] [--assignee ...] [--labels a,b] [--priority N] [--project ...]
linear-remote issues move <id> <state-name> [--wait] [--timeout 60]
linear-remote issues assign <id> <assignee>
linear-remote comments add <issue-id> <body> # <body> accepts @path for file input
linear-remote comments list <issue-id>
linear-remote projects list [--team-filter <name>]
linear-remote projects overview <name> [--bucket-limit 25]
linear-remote teams list
linear-remote states <team-name>
linear-remote labels [--team-filter <name>]
linear-remote cycles <team-name>
All list/show commands emit JSON to stdout. Use --pretty for indentation.
Every payload carries a top-level tool: {name, version} field so you can self-diagnose version drift directly from the output. Run linear-remote --version to print the version without a command.
jq recommendation
Pipe through jq to slice out the bits you need instead of paging the full JSON into context. Examples:
# Just the open-issue identifiers from a project overview
linear-remote projects overview 'Agent Plus' | jq -r '.issuesByState.inProgress[].identifier'
# Title + state for one issue, no comments tree
linear-remote issues get LOA-229 | jq '{id: .identifier, title, state: .state.name}'
# Drop the tool meta when piping an issue payload to another tool
linear-remote issues get LOA-229 | jq 'del(.tool)'
Design rules (agent-plus patterns)
- Aggregate server-side.
issues get --include-comments --include-relations returns the whole context tree in one call. projects overview bundles project meta + milestones + state-bucketed issues + recent activity.
- Resolve by name. Pass
--team LOA, --project 'Agent Plus', --state 'In Progress', --assignee alice@example.com. The CLI resolves UUIDs internally, with lru_cache on enum lookups so update + move in one invocation doesn't re-query.
- Accept either ID format. Every issue argument accepts the human key (
LOA-229) or the UUID. Free-text falls through to search with ambiguity surfacing candidates.
--wait on webhook-driven transitions. issues move --wait polls issue.state.name every 3s until it matches the target, 60s default, override with --timeout. On timeout: non-zero exit with partial JSON. Instant mutations return immediately without polling.
--json is the default. No human-prose output paths.
- Zero API-key leakage. Every API response walks through
_scrub() before emission — masks apiKey, token, secret, webhookSecret, password, and similar. A canary-value test asserts a known secret substring never appears in any output.
--from-markdown contract
- YAML frontmatter (optional):
team, project, labels (list or inline), assignee, priority (0-4), title (overrides H1).
- First
# H1 → title. Everything after → description.
<!-- html comments --> stripped (Linear renders them as visible text).
--team flag overrides frontmatter; frontmatter overrides LINEAR_TEAM_ID.
Pagination
List commands return {nodes: [...], pageInfo: {hasNextPage, endCursor}}. Pass --cursor <endCursor> for the next page. --limit default 25, max 100.
Config precedence (highest first)
--api-key / --team CLI flags
--env-file <path>
.env.local / .env walked up from cwd (closest wins)
- Shell env
Only LINEAR_* prefixed vars are picked up.
Error message contract
Every error path emits problem + cause + fix + link:
- Missing key → "Set in project
.env or .env.local (keys prefixed LINEAR_), or ~/.claude/settings.json. Get one: https://linear.app/settings/api"
- 401 → "API key rejected. Check for whitespace/truncation. Rotate at https://linear.app/settings/api"
- 403 → "Key lacks access to this workspace or resource. Confirm you're a member and the key belongs to it."
- GraphQL field-level errors → the
path and message surfaced verbatim so you can see which field failed.
- 429 → "Rate-limited by Linear. Retry after s (Retry-After header). Linear's complexity budget is ~1500/hr."
When NOT to use this — fall back to the Linear GraphQL API directly
This wrapper's write surface is deliberately narrow (issues CRUD + move/assign, comments add/list, projects list/overview, read-only teams/states/labels/cycles). For anything outside that surface, don't try to bend linear-remote flags to fit — drop straight to curl https://api.linear.app/graphql -H "Authorization: $LINEAR_API_KEY" with a hand-written GraphQL document. The API key is already in scope; the only thing the wrapper adds is name resolution and scrubbing, and neither helps you when the operation isn't wrapped at all.
Specific cases where you should hit GraphQL directly, not linear-remote:
- Project milestone CRUD (create / update / delete milestones, reorder, set target dates).
projects overview reads milestones; there's no milestones subcommand for writes. Use projectMilestoneCreate / projectMilestoneUpdate / projectMilestoneDelete mutations.
- Workflow state CRUD (add a new state to a team, rename, change color, reorder, archive).
states <team> is read-only. Use workflowStateCreate / workflowStateUpdate / workflowStateArchive.
- Cycle management beyond listing (create a cycle, shift dates, close/uncomplete).
cycles <team> is read-only. Use cycleCreate / cycleUpdate.
- Team / workspace admin (create teams, change team settings, invite members, manage roles, org-level config). Not wrapped at all. Use
teamCreate / teamUpdate / teamMembershipCreate and the org mutations.
- Webhook management and integration config (register/rotate webhooks, configure GitHub/Slack/Intercom integrations, OAuth app settings). Not wrapped. Use
webhookCreate / webhookUpdate / integrationRequest and friends.
- Documents, initiatives-beyond-read, roadmaps, custom views, custom fields — none are wrapped. Use
documentCreate / initiativeCreate / initiativeUpdate / roadmap* / customView* mutations directly, and consult Linear's schema introspection (query { __schema { mutationType { fields { name } } } }) if you're unsure of the field name.
Don't get stuck in a loop. If linear-remote --help doesn't show a subcommand for the write you need, or a command returns "unknown subcommand" / "unsupported operation", stop retrying with different flags — write the GraphQL query/mutation yourself and curl it. The wrapper exists to make reading and common writes faster; it's not trying to be a full GraphQL client, and padding it out with one-off mutations defeats the point.
What it doesn't do
Deliberately out of scope for v1:
- Cycle / milestone CRUD (read-only).
- Initiative CRUD.
- Team / workspace management.
- Custom field mutations.
- Webhook configuration.
- Cross-issue relation mutations (
relate). Relations surface in issues get read-only.
- OAuth. Personal API key only — avoids the browser-auth wall that the MCP hits.
Use the Linear UI, the MCP, or raw GraphQL for those. This plugin is read-first Linear work plus the common write surface (issue create/update/move, comments add).
1---2name: linear-remote3description: Read-first wrapper around the Linear GraphQL API. Single-call issue context (comments + relations + state), name-resolved teams/projects/states/labels, `issues create --from-markdown` turns a design doc into a Linear issue without hand-stitching flags. Use whenever the user wants to read, triage, or write Linear issues — creating an issue from a markdown doc, fetching issue context (including comments and relations) without chaining 5+ MCP calls, listing project work by state, or moving an issue and waiting for webhook-driven transitions. Wraps a static personal API key, so it sidesteps the MCP OAuth browser-auth wall entirely.4---5
6# linear-remote
7
8Project-scoped CLI that wraps the Linear GraphQL API into a read-first, JSON-output tool. Stdlib-only Python 3 (no pip, no venv). Designed for agent-driven Linear work — one call returns the full issue + comments + relations + state picture, name-resolved, with a first-class `--from-markdown` path for the "turn this design doc into an issue" workflow.
9
10Lives at `${CLAUDE_SKILL_DIR}/../../bin/linear-remote`; the plugin auto-adds `bin/` to PATH, so just run `linear-remote ...`.
11
12## Prerequisites
13
14- **`LINEAR_API_KEY`** set (project `.env` / `.env.local` or shell env). Get one at https://linear.app/settings/api. Personal API keys only — this plugin deliberately avoids the OAuth browser flow that the MCP requires.
15- **Optional:** `LINEAR_TEAM_ID` — a team UUID or key (e.g. `LOA`) to omit `--team` on every call.
16
17The CLI bails with a clear missing-config message if `LINEAR_API_KEY` is absent.
18
19## When to reach for this
20
21- User says **"add this to linear as an issue"** with a chunk of markdown → write the markdown to a file, then `issues create --from-markdown <path> --team <name>`. YAML frontmatter (`team:`, `project:`, `labels:`, `priority:`, `assignee:`) is respected. First H1 becomes the title, rest becomes the body.
22- User asks **"what's LOA-229 about"** → run `issues get LOA-229 --include-comments --include-relations --pretty`. One call → title, body, state, assignee, labels, project, parent, comments, and blocking/blocked-by relations.
23- User asks **"list my in-progress work on <project>"** → run `issues list --project <name> --state 'In Progress' --assignee <email>`.
24- User wants a **project status snapshot** → run `projects overview '<name>' --pretty`. State-bucketed issues (backlog / todo / inProgress / inReview / done / canceled), milestones, recent activity, completion %.
25- User says **"move LOA-229 to In Review"** → run `issues move LOA-229 'In Review'`. Add `--wait` when the transition is downstream of a GitHub PR merge (Linear's webhook auto-moves to "Done"). 60s default timeout, 3s poll, partial JSON on timeout.
26- User wants to **comment on an issue** → run `comments add LOA-229 @./review-notes.md` (the `@path` form reads body from a file).
27
28## Headline commands
29
30```bash
31linear-remote issues get <id-or-query> [--include-comments] [--include-relations]
32linear-remote issues list [--project ...] [--state ...] [--assignee ...] [--team-filter ...] [--label ...] [--limit 25] [--cursor <c>]
33linear-remote issues search <query> [--limit 25]
34
35linear-remote issues create --title <t> --team <name> [--project <n>] [--body "..."|--body @file] [--labels a,b] [--priority 0-4] [--assignee <email>]
36linear-remote issues create --from-markdown <path> [--team <name>] # killer feature
37
38linear-remote issues update <id> [--title ...] [--state ...] [--assignee ...] [--labels a,b] [--priority N] [--project ...]
39linear-remote issues move <id> <state-name> [--wait] [--timeout 60]
40linear-remote issues assign <id> <assignee>
41
42linear-remote comments add <issue-id> <body> # <body> accepts @path for file input
43linear-remote comments list <issue-id>
44
45linear-remote projects list [--team-filter <name>]
46linear-remote projects overview <name> [--bucket-limit 25]
47
48linear-remote teams list
49linear-remote states <team-name>
50linear-remote labels [--team-filter <name>]
51linear-remote cycles <team-name>
52```
53
54All list/show commands emit JSON to stdout. Use `--pretty` for indentation.
55
56Every payload carries a top-level `tool: {name, version}` field so you can self-diagnose version drift directly from the output. Run `linear-remote --version` to print the version without a command.
57
58### jq recommendation
59
60Pipe through `jq` to slice out the bits you need instead of paging the full JSON into context. Examples:
61
62```bash
63# Just the open-issue identifiers from a project overview
64linear-remote projects overview 'Agent Plus' | jq -r '.issuesByState.inProgress[].identifier'
65
66# Title + state for one issue, no comments tree
67linear-remote issues get LOA-229 | jq '{id: .identifier, title, state: .state.name}'
68
69# Drop the tool meta when piping an issue payload to another tool
70linear-remote issues get LOA-229 | jq 'del(.tool)'
71```
72
73## Design rules (agent-plus patterns)
74
751. **Aggregate server-side.** `issues get --include-comments --include-relations` returns the whole context tree in one call. `projects overview` bundles project meta + milestones + state-bucketed issues + recent activity.
762. **Resolve by name.** Pass `--team LOA`, `--project 'Agent Plus'`, `--state 'In Progress'`, `--assignee alice@example.com`. The CLI resolves UUIDs internally, with `lru_cache` on enum lookups so `update + move` in one invocation doesn't re-query.
773. **Accept either ID format.** Every issue argument accepts the human key (`LOA-229`) or the UUID. Free-text falls through to search with ambiguity surfacing candidates.
784. **`--wait` on webhook-driven transitions.** `issues move --wait` polls `issue.state.name` every 3s until it matches the target, 60s default, override with `--timeout`. On timeout: non-zero exit with partial JSON. Instant mutations return immediately without polling.
795. **`--json` is the default.** No human-prose output paths.
806. **Zero API-key leakage.** Every API response walks through `_scrub()` before emission — masks `apiKey`, `token`, `secret`, `webhookSecret`, `password`, and similar. A canary-value test asserts a known secret substring never appears in any output.
81
82## `--from-markdown` contract
83
84- YAML frontmatter (optional): `team`, `project`, `labels` (list or inline), `assignee`, `priority` (0-4), `title` (overrides H1).
85- First `# H1` → title. Everything after → description.
86- `<!-- html comments -->` stripped (Linear renders them as visible text).
87- `--team` flag overrides frontmatter; frontmatter overrides `LINEAR_TEAM_ID`.
88
89## Pagination
90
91List commands return `{nodes: [...], pageInfo: {hasNextPage, endCursor}}`. Pass `--cursor <endCursor>` for the next page. `--limit` default 25, max 100.
92
93## Config precedence (highest first)
94
951. `--api-key` / `--team` CLI flags
962. `--env-file <path>`
973. `.env.local` / `.env` walked up from cwd (closest wins)
984. Shell env
99
100Only `LINEAR_*` prefixed vars are picked up.
101
102## Error message contract
103
104Every error path emits problem + cause + fix + link:
105
106- Missing key → "Set in project `.env` or `.env.local` (keys prefixed `LINEAR_`), or `~/.claude/settings.json`. Get one: https://linear.app/settings/api"
107- 401 → "API key rejected. Check for whitespace/truncation. Rotate at https://linear.app/settings/api"
108- 403 → "Key lacks access to this workspace or resource. Confirm you're a member and the key belongs to it."
109- GraphQL field-level errors → the `path` and `message` surfaced verbatim so you can see which field failed.
110- 429 → "Rate-limited by Linear. Retry after <N>s (Retry-After header). Linear's complexity budget is ~1500/hr."
111
112## When NOT to use this — fall back to the Linear GraphQL API directly
113
114**This wrapper's write surface is deliberately narrow** (issues CRUD + move/assign, comments add/list, projects list/overview, read-only teams/states/labels/cycles). For anything outside that surface, don't try to bend `linear-remote` flags to fit — drop straight to `curl https://api.linear.app/graphql -H "Authorization: $LINEAR_API_KEY"` with a hand-written GraphQL document. The API key is already in scope; the only thing the wrapper adds is name resolution and scrubbing, and neither helps you when the operation isn't wrapped at all.
115
116Specific cases where you should hit GraphQL directly, not `linear-remote`:
117
118- **Project milestone CRUD** (create / update / delete milestones, reorder, set target dates). `projects overview` reads milestones; there's no `milestones` subcommand for writes. Use `projectMilestoneCreate` / `projectMilestoneUpdate` / `projectMilestoneDelete` mutations.
119- **Workflow state CRUD** (add a new state to a team, rename, change color, reorder, archive). `states <team>` is read-only. Use `workflowStateCreate` / `workflowStateUpdate` / `workflowStateArchive`.
120- **Cycle management beyond listing** (create a cycle, shift dates, close/uncomplete). `cycles <team>` is read-only. Use `cycleCreate` / `cycleUpdate`.
121- **Team / workspace admin** (create teams, change team settings, invite members, manage roles, org-level config). Not wrapped at all. Use `teamCreate` / `teamUpdate` / `teamMembershipCreate` and the org mutations.
122- **Webhook management and integration config** (register/rotate webhooks, configure GitHub/Slack/Intercom integrations, OAuth app settings). Not wrapped. Use `webhookCreate` / `webhookUpdate` / `integrationRequest` and friends.
123- **Documents, initiatives-beyond-read, roadmaps, custom views, custom fields** — none are wrapped. Use `documentCreate` / `initiativeCreate` / `initiativeUpdate` / `roadmap*` / `customView*` mutations directly, and consult Linear's schema introspection (`query { __schema { mutationType { fields { name } } } }`) if you're unsure of the field name.
124
125**Don't get stuck in a loop.** If `linear-remote --help` doesn't show a subcommand for the write you need, or a command returns "unknown subcommand" / "unsupported operation", stop retrying with different flags — write the GraphQL query/mutation yourself and `curl` it. The wrapper exists to make *reading and common writes* faster; it's not trying to be a full GraphQL client, and padding it out with one-off mutations defeats the point.
126
127## What it doesn't do
128
129Deliberately out of scope for v1:
130
131- Cycle / milestone CRUD (read-only).
132- Initiative CRUD.
133- Team / workspace management.
134- Custom field mutations.
135- Webhook configuration.
136- Cross-issue relation mutations (`relate`). Relations surface in `issues get` read-only.
137- OAuth. Personal API key only — avoids the browser-auth wall that the MCP hits.
138
139Use the Linear UI, the MCP, or raw GraphQL for those. This plugin is read-first Linear work plus the common write surface (issue create/update/move, comments add).