Skip to content

MaudeCode/linear-cli

Repository files navigation

linear-cli

CI

Machine-friendly TypeScript CLI for Linear, designed for LLM agents and automation scripts.

The CLI binary is linear-cli.

This project is not affiliated with Linear.

Note

This project is built for LLM agents and was developed with substantial LLM assistance. Its primary interface is stable JSON for automation, not interactive human CLI ergonomics.

Install

bun install
bun run build

Run locally after building:

./dist/linear-cli --help
./dist/linear-cli workspace list --json

During development:

bun src/index.ts workspace list --json

Multi-Workspace Auth

Linear API keys are workspace-scoped, so config is workspace-scoped too. Do not use a single global LINEAR_API_KEY.

The CLI searches these config files, in order:

.linear-cli.yaml
.linear-cli.json
~/.config/linear-cli/config.yaml
~/.config/linear-cli/config.json

YAML example:

defaultWorkspace: personal

workspaces:
  engineering:
    displayName: "Engineering Workspace"
    apiKey: "lin_api_xxx"

  personal:
    displayName: "Personal"
    apiKey: "lin_api_yyy"

JSON example:

{
  "defaultWorkspace": "personal",
  "workspaces": {
    "engineering": {
      "displayName": "Engineering Workspace",
      "apiKey": "lin_api_xxx"
    },
    "personal": {
      "displayName": "Personal",
      "apiKey": "lin_api_yyy"
    }
  }
}

Namespaced environment variables may override config API keys:

export LINEAR_API_KEY_ENGINEERING="lin_api_xxx"
export LINEAR_API_KEY_PERSONAL="lin_api_yyy"
export LINEAR_WORKSPACE="engineering"

Workspace names are uppercased and non-alphanumeric characters become underscores. For example, engineering-prod maps to LINEAR_API_KEY_ENGINEERING_PROD.

API keys are never included in JSON output or routing cache files.

Repo Workspace Hint

To bind a repository checkout to a configured workspace, add a simple dotfile at the repo root:

printf "maudecode\n" > .linear-cli-workspace

The file contains only the local workspace name from your user config. It does not contain API keys or OAuth tokens. linear-cli searches from the current directory up through parent directories, so commands run from subdirectories use the nearest .linear-cli-workspace.

To also bind repo issue commands to a default Linear project, use YAML:

workspace: maudecode
project: Cove

The project hint is used as the default --project for issue list, issue search, and issue create. An explicit --project flag always wins.

Login With API Key

Add or update a workspace API key:

linear-cli auth api-key --workspace personal --api-key "lin_api_..." --display-name "Personal" --default

The command validates the key by listing Linear teams before saving. To save without validation:

linear-cli auth api-key --workspace personal --api-key "lin_api_..." --skip-validate

For automation, use JSON mode with explicit flags:

linear-cli auth api-key --workspace engineering --api-key "$LINEAR_API_KEY_ENGINEERING" --json

In JSON mode, missing required flags return VALIDATION_ERROR instead of prompting. In human mode, missing values are prompted from the terminal, and API-key input is hidden when the terminal supports it.

Login With OAuth

Linear does not let a CLI retrieve a personal API key from browser login. Browser login uses OAuth and stores OAuth tokens instead.

This build includes the Linear OAuth application client ID, so normal users do not need to export or paste OAuth app credentials.

The registered OAuth callback URL is:

http://127.0.0.1:17896/callback

Run:

linear-cli login

For admin-level Linear API operations, such as permanent issue deletion, request the admin OAuth scope during login:

linear-cli login --admin

The CLI will:

  1. Start a local callback server.
  2. Open Linear in your browser.
  3. Exchange the OAuth authorization code with PKCE.
  4. Read the authenticated Linear workspace metadata.
  5. Validate the token by discovering Linear teams.
  6. Store OAuth credentials in the same workspace config.

The local config workspace name defaults to Linear's workspace URL key. Override it only when you need a different local alias:

linear-cli login --workspace personal

Development builds can override the built-in OAuth app client ID:

linear-cli login --client-id "lin_oauth_client_id"
LINEAR_OAUTH_CLIENT_ID="lin_oauth_client_id" linear-cli login

For a different callback port, configure the same URI in Linear and pass --port:

linear-cli login --port 3000

OAuth config is stored like:

workspaces:
  personal:
    displayName: Personal
    auth:
      type: oauth
      accessToken: "..."
      refreshToken: "..."
      expiresAt: "..."
      clientId: "..."
      scope: "read,write"

OAuth tokens are never included in JSON output.

Access tokens are refreshed automatically before Linear API calls when the stored OAuth token is expired or within five minutes of expiry. Linear returns a rotated refresh token during refresh; the CLI persists the new access token, refresh token, expiry, and scope back to the config file without printing secrets.

Workspace Resolution

Commands resolve the active workspace in this order:

  1. --workspace <name>
  2. Cached team-key routing for issue identifiers like ENG-123 and explicit team flags like --team ENG
  3. Nearest repo .linear-cli-workspace
  4. LINEAR_WORKSPACE
  5. defaultWorkspace
  6. Structured WORKSPACE_NOT_RESOLVED error

Issue identifiers are normalized, so eng-123 is treated as ENG-123.

Team-Key Routing Cache

Issue prefixes are Linear team keys. They are discovered from Linear, not configured manually.

Refresh routing manually:

linear-cli workspace refresh-routing --json

The cache contains only:

{
  "generatedAt": "2026-01-01T00:00:00.000Z",
  "routes": {
    "ENG": "engineering",
    "PER": "personal"
  }
}

The default cache staleness window is 24 hours. Normal commands use the cache and refresh only when the cache is missing, stale, manually refreshed, or an issue lookup through cached routing returns ISSUE_NOT_FOUND.

Duplicate team keys across configured workspaces fail with DUPLICATE_TEAM_KEY.

Commands

linear-cli auth api-key --workspace personal --api-key "lin_api_..." --default
linear-cli login

linear-cli config show --json
linear-cli config path --json

linear-cli document create --issue ENG-123 --title "Implementation Plan" --content-file .plans/eng-123-plan.md --json
linear-cli document create --project "Agent Platform" --title "Project Notes" --content "# Notes" --json
linear-cli document list --issue ENG-123 --json
linear-cli document get abc123 --json
linear-cli document update abc123 --content-file .plans/eng-123-plan.md --json

linear-cli workspace list --json
linear-cli workspace current --json
linear-cli workspace current --workspace engineering --json
linear-cli workspace current --issue ENG-123 --json
linear-cli workspace validate --json
linear-cli workspace refresh-routing --json

linear-cli issue get ENG-123 --json
linear-cli issue get ENG-123 --fields identifier,title,url,description --json
linear-cli issue get eng-123 --json
linear-cli issue get ENG-123 --workspace engineering --json
linear-cli issue list --workspace engineering --team ENG --state Todo --project "Agent Platform" --json
linear-cli issue list --workspace engineering --project "Agent Platform" --fields identifier,title,state --json
linear-cli issue list --workspace engineering --project "Agent Platform" --flat --fields all --json
linear-cli issue list --workspace engineering --project "Agent Platform" --fields all --json
linear-cli issue search --workspace engineering --team ENG --query "auth bug" --state Todo --project "Agent Platform" --label bug --priority high --json
linear-cli issue create --workspace engineering --team ENG --title "Fix auth redirect" --description "..." --project "Agent Platform" --priority High --parent ENG-123 --assignee maude@example.com --label Bug --label Frontend --state Todo --cycle current --json
linear-cli issue create --workspace engineering --team ENG --title "Add callback test" --parent ENG-123 --json
linear-cli issue update PER-456 --title "New title" --project "Agent Platform" --priority High --parent PER-123 --assignee maude@example.com --label Bug --label Frontend --state Todo --json
linear-cli issue update PER-456 --add-label agent --remove-label triage --json
linear-cli issue transition ENG-123 started --json
linear-cli issue comment CLIENT-789 --body "Comment body" --json
linear-cli issue attachment add ENG-123 --url "https://example.com/spec" --title "Spec" --json
linear-cli issue relation add ENG-123 --related ENG-456 --type blocks --json
linear-cli issue archive ENG-123 --confirm --json
linear-cli issue delete ENG-123 --confirm --confirm-identifier ENG-123 --json
linear-cli issue delete ENG-123 --confirm --confirm-identifier ENG-123 --permanent --json

linear-cli team list --workspace engineering --json
linear-cli team get ENG --workspace engineering --json

linear-cli project list --workspace engineering --team ENG --json
linear-cli project get "Project Name" --workspace engineering --json

JSON Output

All commands support --json. In JSON mode, stdout contains only one valid JSON object and stderr is empty unless the runtime fails before command handling.

Success with workspace metadata:

{
  "ok": true,
  "workspace": "engineering",
  "data": {
    "id": "uuid",
    "identifier": "ENG-123",
    "title": "Issue title",
    "description": "Markdown description",
    "url": "https://linear.app/...",
    "priority": 2,
    "priorityLabel": "High",
    "state": {
      "id": "uuid",
      "name": "Todo",
      "type": "unstarted"
    },
    "team": {
      "id": "uuid",
      "key": "ENG",
      "name": "Engineering"
    },
    "assignee": null,
    "project": {
      "id": "uuid",
      "name": "Agent Platform",
      "url": "https://linear.app/..."
    },
    "cycle": {
      "id": "uuid",
      "number": 42,
      "name": "Cycle 42"
    },
    "labels": [
      {
        "id": "uuid",
        "name": "bug"
      }
    ],
    "parent": {
      "id": "uuid",
      "identifier": "ENG-100",
      "title": "Parent issue",
      "url": "https://linear.app/..."
    },
    "children": [],
    "createdAt": "2026-01-01T00:00:00.000Z",
    "updatedAt": "2026-01-02T00:00:00.000Z"
  }
}

Error:

{
  "ok": false,
  "error": {
    "code": "WORKSPACE_NOT_RESOLVED",
    "message": "No active Linear workspace could be determined.",
    "details": {}
  }
}

Error Codes

WORKSPACE_NOT_RESOLVED
WORKSPACE_NOT_FOUND
DUPLICATE_TEAM_KEY
TEAM_ROUTING_UNAVAILABLE
CONFIG_NOT_FOUND
CONFIG_INVALID
MISSING_LINEAR_API_KEY
LINEAR_AUTH_FAILED
ISSUE_NOT_FOUND
TEAM_NOT_FOUND
PROJECT_NOT_FOUND
VALIDATION_ERROR
API_ERROR
UNKNOWN_ERROR

Exit codes:

0 success
1 generic error
2 validation/config error
3 not found
4 auth error
5 API/network error

Agent Usage Notes

Use --json for every command in automation. Do not scrape human-readable output.

Prefer explicit workspace flags when ambiguity matters:

linear-cli issue get ENG-123 --workspace engineering --json

When no workspace is explicit, issue identifiers and explicit --team flags use discovered team-key routing before falling back to the nearest repo .linear-cli-workspace, environment, or default workspace. Check ok first, then branch on error.code. Retry only API_ERROR; ask for auth/config fixes on MISSING_LINEAR_API_KEY, LINEAR_AUTH_FAILED, WORKSPACE_NOT_FOUND, or WORKSPACE_NOT_RESOLVED.

The CLI never prompts for input and does not require a TTY.

Agents can upload reviewed Markdown files to Linear documents with document create --content-file <path> and can verify the upload with document get <slugId> --json. Use --issue <identifier> or --project <name> to attach a document to the intended Linear parent.

Destructive issue commands require explicit confirmation flags. issue archive requires --confirm. issue delete uses Linear's issueDelete mutation and requires both --confirm and --confirm-identifier <identifier>. Permanent purge is opt-in with --permanent and requires logging in with linear-cli login --admin so the token includes Linear's admin OAuth scope.

Issue assignment fields are accepted by issue create and issue update where applicable: --project, repeated --label, --add-label, --remove-label, --priority, --parent, --assignee, --state, and --cycle. On issue create, repeated --label sets the initial label set. On issue update, repeated --label is an enrichment alias for adding labels; use --remove-label to remove labels. issue transition uses Linear's canonical workflow state types: backlog, triage, unstarted, started, completed, and canceled.

issue list paginates through all matching Linear issues by default and returns a parent/children tree. Child issues are nested under their parent when the parent is present in the result set, so they are not duplicated as top-level nodes. Use --flat when an agent intentionally needs the flat issue array. Use --limit <n> when an agent intentionally wants a bounded queue sample.

issue list and issue search use compact issue JSON by default to avoid dumping every issue description into LLM context. The default collection fields are identifier, title, state, priorityLabel, and updatedAt; issue list also includes children for tree structure. project is included only when it is not already implied by --project or the repo dotfile. team is not included by default because the team key is already encoded in the issue identifier.

Default issue JSON is intended for agents, not for reproducing Linear's raw API shape. Internal Linear IDs and URLs are omitted by default. state is the state name string, labels are label name strings, and related objects such as project, parent, children, assignee, and cycle are reduced to identifier/name fields. Use --fields all for the full issue payload, or pass a comma-separated list such as --fields identifier,title,url,description. Explicit field selection still uses compact objects unless all is requested.

issue get, issue create, and issue update return detailed compact issue output by default and also accept --fields.

Development

bun run test
bun run build
bun run lint
bun run format

Core files:

src/lib/output.ts        JSON success/error envelopes
src/lib/errors.ts        Stable error classes and exit codes
src/lib/config.ts        Config discovery and validation
src/lib/workspaces.ts    Workspace objects and resolution precedence
src/lib/team-routing.ts  Team discovery and routing cache
src/lib/linear.ts        Linear SDK client and API mapping
src/commands             oclif command files

License

MIT

About

Machine-friendly TypeScript CLI for Linear, built for LLM agents.

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors