@soukypro/linear-agent-cli
v0.6.0
Published
Read-only Linear CLI designed for AI agents: stable JSON output, a static allowlisted query registry, and zero write operations.
Maintainers
Readme
linear-agent-cli
A read-only Linear CLI designed for AI agents and automation: stable JSON output on stdout, structured errors on stderr, deterministic exit codes, and a static allowlisted GraphQL query registry with zero write operations.
This tool never sends a Linear mutation. It contains no GraphQL mutation or subscription documents, exposes no write commands, and has no code path that sends a user-supplied GraphQL document. Every request is a named query from a frozen, source-audited registry, re-validated by a runtime read-only guard immediately before it is sent. See Read-only guarantee.
Installation
From npm
Requires Node.js >= 20.
npm install -g @soukypro/linear-agent-cli # or: bun add -g @soukypro/linear-agent-cli
linear-agent init # interactive first-run setup (stores your API key)Global install puts the linear-agent binary on your PATH, so it is callable from anywhere.
Or run without installing:
npx @soukypro/linear-agent-cli capabilities # or: bunx @soukypro/linear-agent-cli capabilitiesFrom source
git clone https://github.com/souky-byte/linear-agent-cli.git
cd linear-agent-cli
npm ci
npm run build
node dist/main.js --help # or: npm link && linear-agent --helpOr install straight from GitHub (builds automatically via the prepare script):
npm install -g github:souky-byte/linear-agent-cliAuthentication
The CLI needs a Linear personal API key (create one at https://linear.app/settings/account/security). A key is only ever used to send read-only queries.
The quickest way to get set up is the interactive onboarding:
linear-agent init # banner, hidden key prompt, live validation, saves the key
linear-agent init --force # replace an already-stored key without asking
linear-agent init --no-skill # skip the Claude Code skill installation stepinit validates the key with a read-only viewer query before saving it, prints who you are authenticated as (key masked), and shows a few starter commands. As a second step it offers to install the bundled Claude Code skill into ~/.claude/skills/linear-agent/ so Claude Code knows how to drive the CLI in every project. All decoration goes to stderr; stdout still emits the usual single JSON payload, so even init is safe to script around.
Credential resolution order:
LINEAR_API_KEYenvironment variable (highest priority; recommended for agents and CI):LINEAR_API_KEY=lin_api_XXXXXXXX linear-agent teamsLocal config file, written by
linear-agent auth setup.Interactive first-run prompt. When no credential is found and both stdin and stderr are TTYs, any command prompts for the key on stderr with input hidden, validates it with a read-only
viewerquery, reports the authenticated user with the key masked, and offers to save it to the config file. In non-interactive contexts (pipes, CI, agent harnesses) the CLI never prompts; it fails fast with a structuredAUTH_MISSINGerror and exit code 3.
Config file path and permissions
The key is stored as JSON at:
$XDG_CONFIG_HOME/linear-agent-cli/config.json, or~/.config/linear-agent-cli/config.jsonwhenXDG_CONFIG_HOMEis unset;$LINEAR_AGENT_CLI_CONFIG_DIR/config.jsonwhen that override variable is set.
The file is written atomically with owner-only permissions (0600, directory 0700) and is never logged. Manage it with:
linear-agent auth setup # prompt, validate, and store a key
echo "$KEY" | linear-agent auth setup --key-stdin # non-TTY setup (CI)
linear-agent auth status # credential source + masked presence; never prints the secret
linear-agent auth clear # delete the stored key (local only; never calls Linear)auth status works without any credential and exits 0, so agents can probe auth state safely. The exception is auth status --check, which validates the credential and exits 3 (AUTH_MISSING/AUTH_INVALID) when it is absent or rejected.
Usage for agents
stdout is always a single machine-readable payload (or NDJSON lines); everything else — warnings, prompts, pagination hints, errors — goes to stderr.
linear-agent viewer
linear-agent issues --team ENG --state started --assignee @me --limit 20
linear-agent issues --search "payment webhook" --format ndjson --quiet | jq -r .identifier
linear-agent issue ENG-123 --pretty
linear-agent context team ENG | jq '.team.activeCycle'
linear-agent projects --status started --all --max-pages 5
linear-agent releases --stage started --pipeline "Mobile app"
linear-agent issues --release 2.41.0 --state completed
linear-agent release 2.41.0 # release detail: issues (with projects), release note, docs
linear-agent issues --team ENG --group-by assignee --count-only # counts, not lists
linear-agent issues --sla at-risk --blocked --view minimal # SLA + blocked, tiny JSON
linear-agent issues --ask "urgent bugs that missed SLA" # NL filter (Linear-hosted)
linear-agent issues --team ENG --state started --explain # dry-run: filter only, no request
linear-agent search "payment webhook" --limit 5 # relevance-ranked search
linear-agent standup --team ENG --since -P1D # 5 activity sections, 1 request
linear-agent capabilities # machine-readable contract: queries, exit codes, env varsToken-saving output controls
--view minimal(any list): project nodes to the table columns — flat, parsable JSON at a fraction of the size.issues --group-by <key> [--count-only]: client-side aggregation (assignee|state|state-type|priority|project|team|label|cycle|milestone); with--count-onlythe payload is just{key, count}pairs.issue <ref> --max-body <n>/comments --max-body <n>: truncate long description/comment bodies with a machine-detectable[truncated N chars]marker and a warning.--explain(issues, projects, releases, release-pipelines): print the exact operation + variables and exit without contacting the API (no credential needed) — debug filters for free.- Ref lookups (team keys, project names/slugs, issue identifiers, users, releases) are cached for 10 minutes in the config directory, cutting the extra resolution round-trip from scoped commands;
--no-cachebypasses it andauth clearwipes it.
Identifier arguments are flexible and resolved deterministically: UUIDs pass through without a request; issues accept ENG-123; teams accept a key or name; users accept an email, name, or @me; projects and initiatives accept a slug or name. Zero matches exit 4 (NOT_FOUND); multiple matches exit 8 (AMBIGUOUS) with the candidate list in error.details.candidates so a caller can retry with an exact id.
Command reference
| Command | Description |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| init | Interactive first-run setup: hidden key prompt, live validation, stores the key, installs the Claude Code skill |
| auth setup / auth status / auth clear | Manage the local credential (local file only; setup validates with a read-only viewer query) |
| viewer (whoami) | Authenticated user and their organization |
| workspace (organization, org) | Workspace metadata and counts |
| capabilities (schema) | Machine-readable contract: allowlisted queries, formats, exit codes, env vars |
| teams / team <id-or-key> | List teams / show one team (by UUID, key ENG, or name) |
| workflow-states (states) | List workflow states (issue statuses) |
| cycles | List cycles (sprints) |
| users / user <ref> | List users / show one user (UUID, email, name, or @me) |
| projects / project <ref> | List projects / show one project with milestones, members, recent docs |
| initiatives / initiative <ref> | List initiatives / show one initiative with its projects |
| documents / document <id-or-slug> | List documents / show one document including its markdown content |
| project-updates | List project status updates |
| project-milestones | List project milestones |
| issues | List issues with filters (team, project, cycle, assignee, creator, state, label, priority, estimate, search, parent, release, milestone, SLA, blocked/blocking, created/updated/due ranges); --group-by, --ask, --explain |
| issue <id-or-identifier> | Full issue detail: description, comments, relations, attachments, agent sessions |
| labels | List labels (--kind project for project labels) |
| comments | List comments (most recent first unless scoped) |
| attachments | List attachments for an issue or by URL |
| issue-relations <ref> | Relations (blocks, duplicates, related) for one issue, both directions |
| releases | List releases with filters (pipeline, stage, version, name, completed/uncompleted) |
| release <ref> | One release (by UUID, name, or version) with its issues incl. their projects, release note, documents |
| release-pipelines | List release pipelines with their stages (--team, --production) |
| context <scope> [ref] (snapshot) | Composed read snapshot for agents; scope: workspace | team | project | issue |
Run linear-agent <command> --help for the full flag list of any command.
Output formats
Every command accepts --format json|ndjson|table (default json), --pretty, --quiet, and --raw:
json— a single compact JSON payload on one line (--prettyto indent). List commands emit{ nodes, pageInfo, meta }; entity commands emit the entity object.ndjson— one JSON object per node per line, followed by a trailing{"_meta":{...}}line withpageInfoand counts.--quietsuppresses the_metaline, so the stream is pipe-clean forjq -r.table— human-readable aligned columns with long cells truncated; a "more pages" hint (including the exact--after <cursor>to continue) is printed to stderr, never stdout.
JSON payloads are compacted by default to keep agent context small without losing information: null/empty fields are omitted (absence reads the same), and nested { nodes, pageInfo } connection wrappers collapse to plain arrays once exhausted — a truncated nested connection keeps { nodes, pageInfo: { hasNextPage: true, endCursor } } so more data is always signalled. The top-level { nodes, pageInfo, meta } keys of list commands are always present. Pass --raw to disable compaction and get payloads exactly as the API shaped them.
Non-fatal warnings (partial GraphQL data, pagination truncation) go to stderr as single-line JSON {"warning":"..."} and are also collected in meta.warnings. They are printed even with --quiet (which only suppresses the _meta line and pagination hints): with quiet ndjson output, stderr is the only channel left that can flag an incomplete result.
Pagination
List commands share a uniform pagination contract:
--limit <n>— page size, 1–250 (default 50).--after <cursor>— resume from a cursor (pageInfo.endCursorof a previous call).--all— follow pages sequentially (never in parallel) until exhausted or--max-pagesis hit.--max-pages <n>— page cap for--all(default 10, max 1000); using it without--allis a usage error.--include-archived,--order createdAt|updatedAt— passthroughs to Linear.
The payload's meta reports count, pagesFetched, limit, and truncated: true when --all stopped early; the accompanying warning includes the exact cursor to continue from.
Errors and exit codes
Errors are a single line of JSON on stderr, shaped as:
{
"error": {
"code": "NOT_FOUND",
"message": "team \"nope\" not found",
"exitCode": 4,
"hint": "...",
"details": {}
}
}Exit codes are stable and part of the contract (also emitted by capabilities):
| Exit code | Meaning |
| --------- | ---------------------------------------------------------- |
| 0 | Success |
| 1 | Internal error (including any read-only guard violation) |
| 2 | Usage error (bad flag or argument) |
| 3 | Auth error (AUTH_MISSING / AUTH_INVALID) |
| 4 | Not found |
| 5 | Rate limited (details carry Linear's rate-limit headers) |
| 6 | Network error, timeout, or 5xx server error |
| 7 | GraphQL/API error |
| 8 | Ambiguous reference (candidates listed in error.details) |
Transient failures (network errors, timeouts, HTTP 408/429/5xx) are retried with bounded exponential backoff honoring Retry-After — --retries <n> (default 2) and --timeout <ms> (default 30000) tune this per invocation.
Read-only guarantee
No Linear mutation is ever sent by this CLI. Enforcement is layered:
- Static allowlisted registry — the only GraphQL documents in the codebase live in one frozen registry (
src/queries/operations.ts, 36 named queries). The client refuses any operation name outside it, and there is no command, flag, or API for sending arbitrary GraphQL. - Runtime guard — immediately before every request, the document is re-validated (
src/guard.ts): exactly one namedqueryoperation, fragments only otherwise, balanced braces, and the tokensmutation/subscriptionrejected anywhere in the document. A violation is an internal error (exit 1), not a request. - Source-level safety tests —
test/safety.test.tsparses every registry document with the referencegraphqlimplementation and asserts: single named query, every top-level selection against an allowlist of Query root fields, no write-looking field names, no mutation/subscription operation documents anywhere in shipped sources, and no write-sounding CLI command names. - No write dependencies — the CLI speaks plain GraphQL-over-HTTP; it does not embed an SDK with mutation helpers.
The only "writes" the tool performs are to the local config file for credential storage.
API endpoint and authentication detail
Requests are POST https://api.linear.app/graphql with:
Content-Type: application/json
Authorization: <your API key> # personal lin_api_ keys are sent bare; lin_oauth_ tokens get a "Bearer " prefix
User-Agent: linear-agent-cli/<version>The body is { "query": "<allowlisted document>", "variables": {...}, "operationName": "<Name>" }. LINEAR_AGENT_CLI_ENDPOINT can override the endpoint for testing against a mock server.
Environment variables
| Variable | Purpose |
| ----------------------------- | ------------------------------------------------------- |
| LINEAR_API_KEY | API key (highest-priority credential source) |
| LINEAR_AGENT_CLI_CONFIG_DIR | Override the config directory (default: XDG config dir) |
| LINEAR_AGENT_CLI_ENDPOINT | Override the GraphQL endpoint (testing only) |
Claude Code skill
The repo bundles an agent skill at .claude/skills/linear-agent/SKILL.md, and the npm package ships it too. The easiest way to install it globally is linear-agent init, which offers to copy it into ~/.claude/skills/linear-agent/ (and updates it on later runs when the bundled version changes). Claude Code also picks it up automatically when working inside this repository; to use it in a single project instead, copy the linear-agent directory into that project's .claude/skills/. The skill teaches the agent the command map, filter recipes, pagination/exit-code contract, and context-saving invocation patterns.
Development
npm ci # install
npm run typecheck # tsc --noEmit
npm test # unit + safety tests (offline; includes the read-only safety suite)
npm run lint # eslint
npm run format:check # prettier
npm run build # compile to dist/
npm run smoke:pack # npm pack, install the tarball into a temp dir, run offline commands
npm run test:live # optional live smoke tests; requires LINEAR_LIVE=1 and LINEAR_API_KEYThe default test suite runs fully offline; live tests are opt-in and read-only like everything else.
Publishing releases
The package is published publicly as @soukypro/linear-agent-cli. Releases use npm Trusted Publishing (GitHub Actions OIDC), so no npm token is stored in GitHub or the repository.
Before the first automated release, configure the package's Trusted Publisher on npmjs.com:
- Provider: GitHub Actions
- Organization or user:
souky-byte - Repository:
linear-agent-cli - Workflow filename:
publish.yml - Environment: leave empty unless you add a GitHub deployment environment
- Allowed action:
npm publish
To release a new version after changing the code:
npm version patch # or: npm version minor / npm version major
git push origin main --follow-tagsThe v* tag starts the publish workflow after CI-style verification. The workflow must publish a version that does not already exist on npm.
License
MIT — see LICENSE.
