# Use Linearis

> Use when running Linear.app operations from the command line — creating, updating, archiving, listing, or filtering issues, setting project milestones, or wiring blocked-by relations via the `linearis` CLI (binaries `linear` and `linearis`, JSON output) instead of an MCP or the web UI. Triggers on any Linear issue/project/milestone task in a terminal, and on syncing Kevin's Ogham roadmap with its shared-memory database. Covers generic install/auth setup, the CLI's sharp edges, and the local env.sh split that keeps workspace identifiers out of the repo. Not a full reference — that's `linear help` per command.

- Skill: `kevin-burns/use-linearis` (Agent Skill)
- Install (CLI): `npx skillmds@latest add kevin-burns/use-linearis`
- Raw SKILL.md: https://api.skillmd.com/api/skills/kevin-burns/use-linearis/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Product & Planning
- License: MIT
- Author: kevin-burns (https://skillmd.com/u/kevin-burns)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/kevin-burns/use-linearis

---


# use-linearis

`linearis` is a Node CLI for Linear.app — JSON output, smart ID resolution, cursor pagination, built for LLM agents. It ships two identical binaries, `linear` and `linearis`. It is **not** an MCP: no tool schemas land in context, so every fresh session pays a discovery tax. This skill pays that tax up front — generic setup, the CLI's sharp edges, and the Ogham dogfooding workflow. It carries **no real workspace identifiers** — those live in a local `~/.config/dotfiles/env.sh` you source (see below).

If Linear ships an official MCP with write support, migrate to it. Until then, `linearis` is the agent-shaped CLI.

## Setup (generic — works for any Linear workspace)

**Source:** <https://github.com/linearis-oss/linearis> · npm package `linearis` (MIT). Requires Node.

Install globally, then authenticate:

```bash
npm i -g linearis        # installs BOTH `linear` and `linearis` (same binary)
linear auth login        # browser OAuth; stores a token at ~/.linearis/token
```

One-liner to confirm it's installed and authed:

```bash
command -v linear >/dev/null && linear auth status || echo "install: npm i -g linearis && linear auth login"
```

`auth status` returns `{authenticated: true, user: {name, email}}` when a token is live at `~/.linearis/token`. If write operations fail with `Invalid scope: write required`, generate a Personal API Key in the Linear web UI (Settings → API → Personal API keys) with `admin` scope, then either `export LINEAR_API_TOKEN=<key>` or overwrite `~/.linearis/token`.

Anything not covered below, discover with `--help` (this skill is the sharp edges, not the full surface):

```bash
linear --help
linear issues --help          # per-subcommand flags
```

## Gotchas (the ones that cost time — re-verified against 2026.6.0 on 2026-08-05)

**1. Flag asymmetry between `issues create`/`update` and `issues list`.** Create/update take `--project-milestone <ms>`; list takes `--milestone <name>` (and requires `--project`). Same concept, two flag names. Likewise `--label` (singular, comma-separated) on list vs `--labels` on create/update.

**2. ~~Milestone create is broken.~~ Fixed in 2026.6.0** ([#223](https://github.com/linearis-oss/linearis/issues/223), [#228](https://github.com/linearis-oss/linearis/issues/228))**.** It used to return `Variable "$projectId" of required type "String!" was not provided` even with `--project` set, so the workaround was to create milestones in the web UI. The project id is now passed correctly — an invalid project yields a clean `Project "X" not found` instead of the variable error. `milestones` also gained `read` and `update`. Note there is still **no `milestones delete`**, so a mistyped milestone has to be cleaned up in the web UI; that is why the fix above was probed with a deliberately invalid project rather than by creating a throwaway.

**3. ~~Labels can't be created via CLI.~~ Fixed in 2026.6.0** ([#117](https://github.com/linearis-oss/linearis/issues/117))**.** `linear labels` now has `create`, `read`, `update` and `delete` alongside `list`. Still true, and still the expensive part: a nonexistent label name passed to `issues create` fails with `Label "X" not found` and **no issue is created** — so create the label first, or the whole call is a no-op.

**4. The stored token isn't a raw Personal API Key.** Copying `~/.linearis/token` into a `curl` `Authorization: Bearer …` header returns 401. Don't bypass the CLI by hitting GraphQL directly — fix `linearis` or stay on its surface.

**5. Project resolves by name or UUID, not slug — and the slug is the one you'll reach for.** `projects list` hands you both a `slugId` and a `url`, and the slug is what sits in every Linear project URL, so it is the natural thing to paste. Neither form is accepted:

```bash
linear issues list --project "Ogham"                                  # resolves
linear issues list --project "00000000-proj-0000-0000-000000000000"   # resolves
linear issues list --project "abcdef012345"                           # Project not found
linear issues list --project "myproject-abcdef012345"                  # Project not found
```

Same on `milestones list --project`. Use the display name or the full UUID.

**6. Query-complexity ceiling.** `linear projects list` with no filter returns `Query too complex — complexity 13950 / 10000`. Use `--limit 5` or filter down. Not a linearis bug as such — it is Linear's own GraphQL cost limit — but tracked upstream as [#276](https://github.com/linearis-oss/linearis/issues/276) (open), which reports it firing even on a one-project workspace.

**7. Fetching one issue is `read`, not `get` — and `get` is never coming.** `linear issues read <issue>` returns the full record including the description. Asking for `get` fails with `error: too many arguments for 'issues'. Expected 0 arguments but got 2`, which reads like a flag problem rather than a wrong verb and sends you hunting through `--help` for the wrong thing.

This is a well-worn trap, not a local quirk: upstream [#48](https://github.com/linearis-oss/linearis/issues/48) reports LLMs reaching for `issues get` with exactly this error, and was closed **NOT_PLANNED** — aliases are a deliberate no, so do not wait for it. The recovery problem it describes is tracked separately as [#281](https://github.com/linearis-oss/linearis/issues/281) (open): malformed commands emit plain-text on stderr while valid-command errors emit the JSON envelope, both exit 1, so an agent cannot tell "I called it wrong" from "that issue doesn't exist".

Practical consequence: **a non-JSON error means you got the verb or arity wrong, not that the entity is missing.** Related verbs on the same object: `search <query>` (full-text), `archive` / `unarchive` / `delete <issue>`.

**8. Sub-collections come back as `{nodes: […]}`, not bare arrays.** `issues read` returns `labels`, `comments`, `children` and `relations` each wrapped in a `nodes` key, while `issues list` returns its results under a top-level `nodes`. So `jq '[.labels[].name]'` fails with `Cannot index array with string "name"` — it needs `jq '[.labels.nodes[].name]'`. Cheap way to avoid guessing:

```bash
linear issues read ENG-227 | jq 'keys'          # what fields exist
linear issues read ENG-227 | jq '.labels'       # what shape a given field is
```

**Version pin:** every gotcha above was re-verified against **2026.6.0** on 2026-08-05. Two of the eight had already gone stale by then (2 and 3 — both told you to go and use the web UI for something the CLI had since learned to do), so check before trusting:

```bash
linear --version; npm view linearis version    # drifted? re-verify 1-8 before relying on them
```

---

## Workspace identifiers — `~/.config/dotfiles/env.sh`

This skill is public and carries **no real IDs**. Team, project and milestone UUIDs are not credentials — nobody can act on them without your auth — but they describe a private tracker, so they live in a local file instead. Every example here uses obvious placeholders (`00000000-proj-…`, `abcdef012345`, `ENG-123`).

**Do not resolve IDs by querying at session start.** That reinstates exactly the discovery tax this skill exists to remove, and burns calls against the complexity ceiling in gotcha 6. Source the file instead — zero API calls:

```bash
source ~/.config/dotfiles/env.sh
linear issues list --project "$LINEAR_PROJECT_OGHAM" --limit 20
```

Create it once. Keep it outside any git repo, `chmod 600`:

```bash
mkdir -p ~/.config/linearis && chmod 700 ~/.config/linearis
linear teams list --limit 10 | jq -r '.nodes[] | "\(.key)  \(.id)  \(.name)"'
linear projects list --limit 20 | jq -r '.nodes[] | "\(.name)  \(.id)"'
linear milestones list --project "<project-uuid>" | jq -r '.nodes[] | "\(.name)  \(.id)  \(.targetDate)"'
```

then write the values into `env.sh` as exports and `chmod 600` it. The names this skill's recipes expect:

| Variable | Holds |
|---|---|
| `LINEAR_TEAM` | team key, e.g. `ENG` |
| `LINEAR_TEAM_ID` | team UUID |
| `LINEAR_PROJECT_<NAME>` | project UUID, one per project you work in |
| `LINEAR_MS_<VERSION>` | milestone UUID, one per active release |

Refresh after a workspace change — milestone IDs are stable but target dates and labels drift.

Remember gotcha 5: `--project` takes the **display name or full UUID**, never a `slugId` and never the slug in a project URL.

### Ogham conventions

- **Title carries the release**: prefix atomic issues with `[vX.Y.Z]` (e.g. `[v0.16] Migrations 041-043: ...`). Milestone linkage is separate, but titles let you scan a mixed list.
- **Milestone = release, Issue = atomic backlog item, in-session TaskCreate = per-session scratch.** Don't mirror Linear issues into the in-session task tracker. Do stamp `ENG-N` into a scratch task's description before closing it as ported.
- **Release-execution issue per milestone** — the last issue in each milestone, blocked-by all the others, invokes CLAUDE.md's 10-step release playbook. Named `[vX.Y.Z] Execute release per 10-step playbook (blocked by all above)`.
- **Priority mapping**: `2` = release-critical, `3` = medium, `4` = nice-to-have. `1` (urgent) is reserved for hotfixes.

### The Linear ↔ Ogham dogfooding loop

The reason to drive `linearis` from Claude Code rather than clicking Linear's web UI is the Ogham workflow experiment: **durable state lives in Linear** (issue status, blocked-by, milestone); **transient session context lives in Ogham**, the shared-memory database.

Ogham ships its own CLI — a Go binary, MCP client for the Ogham memory stack, JSON output by default. Source: <https://github.com/ogham-mcp/ogham-cli>.

**Do not hardcode its path or assume its name.** The binary is `omcli` on some machines and `ogham` on others, and is sometimes only a checkout rather than on `PATH`. Worse, on machines running the Python MCP a bare `ogham` may resolve to *that* rather than the Go CLI. So resolve it — `env.sh` (above) exports `OGHAM_CLI` by trying `omcli` first, then `ogham`, then the known checkout locations, and leaves it empty rather than failing if nothing is found.

So when an agent picks up `ENG-114`:

```bash
source ~/.config/dotfiles/env.sh
linear issues read ENG-114                          # durable: atomic spec, status, blocked-by
"$OGHAM_CLI" search "typed edges store_triple"      # transient: design memory (hybrid vector+keyword)
```

Guard on it if the command is load-bearing: `[ -n "$OGHAM_CLI" ] || echo "ogham cli not found"`.

`ogham search <query>` runs the fast native-Go hybrid search; add `--sidecar` for the full retrieval pipeline (intent detection, MMR, graph augmentation), `--limit N` / `--tags a,b` to scope. That pairing — spec from Linear, design memory from Ogham — is the loop every prior task-tracking attempt was missing. See `ENG-131` for the recipe deliverable in v0.17.

## Common recipes

Examples use the Ogham IDs above; swap `OGHAM`/`ENG`/milestone IDs for your own workspace.

**Create an atomic issue against a milestone**:

```bash
source ~/.config/dotfiles/env.sh
# milestone id comes from env.sh too
linear issues create "[v0.16] <what>" \
  --team "$LINEAR_TEAM" --project "$LINEAR_PROJECT_OGHAM" --project-milestone "$LINEAR_MS_V0_16" \
  --labels "Feature" --priority 2 \
  --description "$(cat <<'MD'
Body markdown.
MD
)"
```

**Batch create with error surfacing** — errors go to stdout as JSON, so `tee` a file and grep it:

```bash
OUT=/tmp/linear_batch.jsonl; : > "$OUT"
mk() {
  local title=${title:?} labels=${labels:?} prio=${prio:?} body=${body:?}
  linear issues create "$title" --team "$LINEAR_TEAM" --project "$LINEAR_PROJECT_OGHAM" \
    --labels "$labels" --priority "$prio" --description "$body" 2>&1 \
    | tee -a "$OUT" | grep '"identifier"'
}
title="[v0.16] Foo" labels="Feature" prio=2 body="..." mk
title="[v0.16] Bar" labels="Improvement" prio=3 body="..." mk
grep '"error"' "$OUT" || echo "clean"
```

> Named variables rather than `$1`-`$4` on purpose. A skill's markdown is rendered into the agent's context with its arguments interpolated, so bare positional parameters inside a fenced block get **silently replaced by whatever the caller passed as skill args** — an agent then copies a recipe that creates an issue titled `an` with the label `issue`. Observed on 2026-08-05. Keep shell examples in this file positional-free.

**Backfill a milestone across a range of issues**:

```bash
# milestone id comes from env.sh too
for n in 109 110 111 112 113; do
  linear issues update "$LINEAR_TEAM-$n" --project-milestone "$LINEAR_MS_V0_16" | grep '"identifier"'
done
```

**Wire blocked-by** — one call per dependency, `--blocked-by` on `issues update`:

```bash
blocker=121
for dep in 109 110 111 112 113 114 115 116 117 118 119 120 122 123; do
  linear issues update "$LINEAR_TEAM-$blocker" --blocked-by "$LINEAR_TEAM-$dep" 2>&1 | grep '"error"'
done
```

**Filter open issues in a release**:

```bash
linear issues list --project "$LINEAR_PROJECT_OGHAM" --milestone v0.16 --limit 50 | \
  jq -r '.nodes[] | "\(.identifier)  \(.state.name)  \(.title)"'
```

**Clean up a stray test issue** (archive over delete — leaves history; both need `admin` scope):

```bash
linear issues archive ENG-102
```

