Linearis CLI Reference
Verified against Linearis v2026.4.9 on 2026-05-31.
⚠️ READ vs WRITE — the rule that governs everything below. Linear READS → query the local replica by direct SQL (
~/catalyst/catalyst-replica.db), or calllinear_read_ticket <ID>. Never shelllinearis issues readfor a routine read — that hits the rate-limited API and 429s the shared quota. WRITES (create/update/state/comment/estimate/label) and list/search/non-issue domains → thelinearisCLI. Full rule + freshness gate: Reading Linear.
CRITICAL: Always use these exact patterns. Do NOT guess or improvise syntax.
⚠️ Read the Gotchas & Traps section before scripting —
issues listsilently hides Done tickets,lineariseats stdin in loops, and there is no--jsonflag. These bite hard.
Reading Linear
This is the single source of the Linear read rule. Other skills/agents reference this section — they do not restate it.
Linear READS → query the local Catalyst Cloud replica directly with SQL. Linear WRITES →
always linearis. The replica (~/catalyst/catalyst-replica.db, a SQLite mirror kept
current by the per-host catalyst-cloud-sync change-feed writer) holds every issue field plus
labels, relations, projects, cycles, users, and PR/review state — so one SQL query serves ANY
read, including the label/relation joins the old CLI couldn't. This supersedes the
catalyst-linear wrapper (now deprecated — see below) for agent/skill ad-hoc reads.
Why direct SQL, not bare
linearis. Barelinearisreads always hit the rate-limited Linear API. On the shared-quota fleet that burns budget and 429s everyone. The replica is a sub-ms local copy that already has the answer — reading it is what makes "every client reads the replica" actually true.
The rule
- Confirm the replica is FRESH + SEEDED — both gates (mirrors the daemon's
isReplicaFresh,execution-core/replica-read.mjs):- Writer alive:
<db>.writer.lockmtime is < 5 min old. The writer heartbeats this file every few seconds regardless of Linear activity, so it tracks liveness, not data-change cadence — do NOT gate on the.db/-walmtime (a quiet feed makes those look stale even when the mirror is perfectly current). - Seed complete:
sync_metahas a non-emptycursorrow. The writer deletes it at the start of a re-seed and rewrites it on completion, so its presence means you are not reading a half-truncated table mid-reseed.
- Writer alive:
- Fresh + seeded → query the replica and TRUST it. Do not re-verify against live Linear — that defeats the cache and re-burns the quota.
- Not fresh / not seeded / your row missing → this is an ALARM, not a silent reroute. Say
so loudly, fall back to
linearisfor that one read, and file a ticket — a stale/absent replica signals a writer or mirror gap worth fixing, not a one-off retry.
Caveat — the gates prove writer-liveness + seed-completeness, NOT per-row apply success. A rare class of rows (~0.7%) can be present but stale because their change-feed apply silently failed (the
errno:1apply-drift, catalyst-cloud#127 / CTL-1402) — the writer heartbeats and the cursor advances past them, so the freshness gate reads green while that one row holds an old value. Direct SQL cannot make this loud on its own. So: if a specific field contradicts something you just directly observed (e.g. a state you just wrote), treat that one field as an anomaly — re-read it vialinearis, use the live value, and surface it. This is the residual reason writer reliability + apply-failure telemetry (CTL-1402) matter; it is not license to re-verify reads that don't contradict anything.
Freshness gate (copy-paste, portable macOS/Linux)
# Resolve the DB the way the daemon does: $CATALYST_REPLICA_DB, else $CATALYST_DIR, else $HOME.
DB="${CATALYST_REPLICA_DB:-${CATALYST_DIR:-$HOME/catalyst}/catalyst-replica.db}"
replica_fresh() {
local lock="$DB.writer.lock" now age
[[ -f "$lock" ]] || return 1
# GNU `stat -c %Y` first, BSD `stat -f %m` fallback (on Linux `-f` is --file-system, not mtime).
now=$(date +%s); age=$(( now - $(stat -c %Y "$lock" 2>/dev/null || stat -f %m "$lock") ))
(( age < 300 )) || return 1 # writer heartbeat < 5 min
[[ -n "$(sqlite3 "$DB" "SELECT 1 FROM sync_meta WHERE key='cursor' AND value<>'' LIMIT 1;")" ]] # seed complete
}
Scripts should use the shared helper instead of re-implementing this: source
plugins/dev/scripts/lib/linear-read-replica.sh and call linear_read_ticket <ID> (freshness
gate → SQL → loud linearis fallback).
Querying (discover the schema — don't guess columns)
Run sqlite3 "$DB" .schema (or .schema issues) to see the live columns. Verified 2026-07-01:
issues.stateis the state NAME directly (Backlog/Implement/PR/Done…) — no join.issuesalso has:identifier,title,estimate,priority/priority_label,description,url,branch_name,parent_identifier,project_id,cycle_id,team_id,assignee_id, the timestamp columns, and arawcolumn with the full Linear JSON.- Labels:
issue_labels ⋈ labels—JOIN labels l ON l.id = il.label_id WHERE il.issue_id = i.id. - Relations (blocks / blocked-by / …): the
relationstable (type, issue_identifier, related_identifier). Relations lag ≤ 5 min (reconcile poll, no webhook) — everything else is real-time. - Any uncolumned field:
json_extract(raw,'$.path')(e.g.json_extract(raw,'$.state.type')).
Representative read — identifier, title, state, estimate, and labels in one query:
sqlite3 -json "$DB" "
SELECT i.identifier, i.title, i.state, i.estimate,
(SELECT group_concat(l.name, ', ') FROM issue_labels il
JOIN labels l ON l.id = il.label_id WHERE il.issue_id = i.id) AS labels
FROM issues i WHERE i.identifier = 'ENG-123' AND i.removed_at IS NULL;"
AND removed_at IS NULLis REQUIRED: a tombstoned (removed) issue must read as a MISS → fall back to live Linear, never as a stale hit.
Still needs linearis (no issue-shaped replica form)
- Non-issue domains:
cycles/projects/milestones/initiativeslist & read — uselinearis(Core Operations below). Simplecycles/projectslookups can use those replica tables, but the linearis commands are the full path. - Genuinely unmirrored gaps: cross-team-unsynced parent/child, plus a few unselected fields
(
relation.id,cycle.name— CTC-147;state.id,team.key— CTC-148;childrenis always[]). These are closeable gaps, not permanent carve-outs — file/track them; don't route around the replica by habit.
Writes — always linearis
create / update / state transitions / discuss / estimate / label always go through
linearis. The replica is read-only.
catalyst-linear CLI — DEPRECATED
The catalyst-linear read|list|search wrapper (CTL-1391) is superseded by direct SQL for
agent/skill reads and retained only as a fail-open compatibility shim. Prefer direct SQL.
(list/search were always linearis passthrough — no replica benefit — and the wrapper's
additive _meta field + duplicate-flag collapsing broke bare-linearis jq pipelines.) The
daemon's own read paths use replica-read.mjs directly and are unaffected by this deprecation.
Looking Up Syntax
For full flag details, run linearis usage (all domains) or linearis <domain> usage (one domain).
The usage output is authoritative and always current — prefer it over memorizing flags.
linearis usage # Full overview of every domain and flag
linearis issues usage # Just issue operations
linearis milestones usage # Just milestone operations
linearis cycles usage # Jus