lin

module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 20, 2026 License: MIT

README

lin

CI Release Go Reference Go version

The Linear CLI in Go: a single, fast, native binary that is first-class for both humans and AI agents, covering the Linear GraphQL API comprehensively.

Documentation: lin.alexgrais.dev - guides, the full command reference, and the agent contract.

brew install alexgrais/tap/lincli
lin auth login
lin issue create --team ENG --title "Fix login" --labels bug --assignee me

Why a CLI, not just the MCP

The Linear MCP server is the easy way to give an agent Linear access, but it has a measurable cost. Benchmarked over a 40-turn session with 23 Linear operations, with response sizes measured against a real workspace:

Approach Tokens for the session
MCP, eager client (Claude Desktop, most frameworks) ~808,500
MCP, lazy client (loads tools on demand) ~69,800
lin ~8,300

That is 8.4x cheaper than a well-implemented lazy MCP client, and ~98x cheaper than one that keeps all 57 tool schemas resident. The 8.4x figure is the honest one to judge lin by: it compares against the MCP at its best.

The token gap is a symptom. The cause is that lin gives the caller levers the Linear MCP does not expose at all, checked across twenty of its tools:

Control lin Linear MCP
Choose the fields returned --fields identifier,title,state not exposed
Preview a mutation --dry-run on every mutation not exposed
Choose the output shape --output json|ndjson|table fixed
Guard destructive calls --confirm=<id> not exposed
Follow every page --all drive the cursor yourself
Branch on failure sysexits exit codes read the error prose

--fields decides what enters the context: three fields instead of everything is the difference between ~1,200 and ~11,300 tokens on a single 20-issue call. --dry-run makes a mutation inspectable before it happens, so an agent can check its own work before anything is written.

Full methodology, raw payloads, and scripts are in bench/ - including where lin measures worse.

What you get

  • JSON-first. Data on stdout, messages on stderr, regardless of TTY. A machine is always assumed to be reading.
  • Predictable over clever. Resource-first grammar (lin <resource> <verb>), sysexits exit codes, cursor pagination, --dry-run on every mutation. Learn one command and the rest follow.
  • Discoverable at runtime. lin describe emits the whole surface as JSON, so agents never parse help text and the docs cannot drift from the binary.
  • Comprehensive through codegen. Commands are typed GraphQL operations compiled against a vendored schema, so coverage is broad without being brittle.
  • Safe with secrets. API keys come from the environment or an XDG config file, never from flags, and are masked in output. Mutations only ever change the fields you name; unset never means "clear".
  • Runs anywhere. One native binary in a terminal, a cron job, or a CI step - no MCP server hop, no runtime to install.

Install

brew install alexgrais/tap/lincli          # Homebrew (binary is still `lin`)
go install github.com/alexgrais/lin/cmd/lin@latest
go build -o bin/lin ./cmd/lin              # from a clone

Prebuilt binaries for macOS, Linux, and Windows (amd64 + arm64) are attached to every release.

Authentication

The easiest path stores a validated key in ~/.config/lin/config.json at mode 0600:

lin auth login                     # prompts for the key, hidden, validated before saving
lin auth login --profile work      # named profiles for multiple workspaces
lin auth status                    # who am I, which profile, key masked
lin auth switch work               # change the default profile

Environment variables keep priority over the config, so CI needs no config file:

export LINEAR_API_KEY=lin_api_...            # or
export LINEAR_API_KEY_FILE=~/.secrets/lin    # a file containing the key

Keys are never accepted as flags. Verify with lin whoami.

Usage

Resource-first grammar: lin <resource> <verb>.

lin issue create --team ENG --title "Fix login" --labels bug --assignee me
lin issue list --team ENG --state "In Progress" --fields identifier,title --output table
lin issue update ENG-123 --state Done --add-labels regression
lin issue update ENG-123 --state Done --dry-run   # print the wire payload, send nothing
lin project get "Mobile app v2"                   # includes lead + milestones
lin attachment add ENG-123 ./screenshot.png       # 3-step upload, returns a markdown embed
lin comment add ENG-123 --body "Deployed to staging"
lin describe                                      # full machine-readable command surface
Command surface

41 resource groups, each with the same verb grammar. Run lin describe for the machine-readable version.

Area Resources
Work items issue (CRUD, relations, sub-issues, archive lifecycle), comment, attachment, triage, favorite, reaction, emoji
Organisation team, state, label, user, cycle, org, template, schedule
Planning project, project-label, project-status, milestone, initiative, initiative-label, custom-view
Delivery release, release-note, release-pipeline, release-stage, status-update, document
Customers customer, customer-need, customer-status, customer-tier
Integrations and admin webhook, external-link, notification, audit, agent
Meta auth, whoami, version, describe
The contract
  • --output json|ndjson|table (default json, regardless of TTY), --fields id,title masking
  • --dry-run on every mutation: prints {operation, variables} as they would go on the wire, resolves references, never executes
  • --json '{...}' raw payload on create/update commands (- reads stdin), 1:1 with the GraphQL input types, unknown fields rejected
  • Cursor pagination on every list: --limit, --cursor, --all; next_cursor in output
  • Destructive commands require --confirm=<id>
  • References accept human values: team key or name, me, emails, state/label/project names
  • --fields rejects a name the payload does not carry (exit 65) and lists the valid ones, so a typo never reads as an absent value
  • Retrying transport: backoff with jitter, Retry-After honoured, mutation-safe retry policy
Exit codes (sysexits)
Code Meaning
0 success
1 generic error
65 validation error (bad flags or input)
66 not found
70 internal error
75 temporary failure (rate limited, 5xx)
77 permission denied (check the API key)
78 configuration error (no API key)

Agents

lin describe returns the whole command surface as JSON - no help-text parsing needed. See AGENTS.md for the invariants agent consumers should load into context, and the agent guide online.

Development

go build -o bin/lin ./cmd/lin   # build
go test ./...                   # tests (mocked transport, no network)
golangci-lint run               # lint
go generate ./...               # regenerate the typed GraphQL client (genqlient)
go run ./tools/schema           # refresh the vendored Linear schema (ADR-0001)

See CONTRIBUTING.md to get set up, VISION.md for the design principles and non-goals, and SECURITY.md to report a vulnerability. Architecture decisions are recorded under docs/decisions/ (MADR). Releases follow the checklist in docs/RELEASING.md.

Directories

Path Synopsis
cmd
lin command
Command lin is the Linear CLI entrypoint.
Command lin is the Linear CLI entrypoint.
internal
agent
Package agent owns the lin agent command group (Linear agent sessions, read surface).
Package agent owns the lin agent command group (Linear agent sessions, read surface).
attachment
Package attachment owns the lin attachment command group, including the 3-step fileUpload dance.
Package attachment owns the lin attachment command group, including the 3-step fileUpload dance.
audit
Package audit owns the lin audit command group (admin read of audit logs).
Package audit owns the lin audit command group (admin read of audit logs).
auth
Package auth owns the `lin auth` command group: storing, inspecting and switching Linear API keys held as named profiles in the XDG config file, so users stop re-exporting LINEAR_API_KEY and can manage several workspaces.
Package auth owns the `lin auth` command group: storing, inspecting and switching Linear API keys held as named profiles in the XDG config file, so users stop re-exporting LINEAR_API_KEY and can manage several workspaces.
cli
Package cli assembles the lin command tree: root, global flags, version and describe.
Package cli assembles the lin command tree: root, global flags, version and describe.
cmdutil
Package cmdutil carries the shared plumbing every resource command uses: the factory (IO + lazy GraphQL client), output option resolution, dry-run rendering, pagination flags, input hardening and raw JSON payload decoding.
Package cmdutil carries the shared plumbing every resource command uses: the factory (IO + lazy GraphQL client), output option resolution, dry-run rendering, pagination flags, input hardening and raw JSON payload decoding.
comment
Package comment owns the lin comment command group.
Package comment owns the lin comment command group.
config
Package config resolves the Linear API key and client settings.
Package config resolves the Linear API key and client settings.
customer
Package customer owns the lin customer and customer-need command groups.
Package customer owns the lin customer and customer-need command groups.
customview
Package customview owns the lin custom-view command group.
Package customview owns the lin custom-view command group.
cycle
Package cycle owns the lin cycle command group.
Package cycle owns the lin cycle command group.
document
Package document owns the lin document command group.
Package document owns the lin document command group.
emoji
Package emoji owns the lin emoji command group (custom workspace emojis).
Package emoji owns the lin emoji command group (custom workspace emojis).
externallink
Package externallink owns the lin external-link command group (entity external links on teams, projects, initiatives, cycles, releases).
Package externallink owns the lin external-link command group (entity external links on teams, projects, initiatives, cycles, releases).
favorite
Package favorite owns the lin favorite command group (viewer sidebar).
Package favorite owns the lin favorite command group (viewer sidebar).
gql
Package gql owns the GraphQL transport and the genqlient-generated typed client.
Package gql owns the GraphQL transport and the genqlient-generated typed client.
gql/gqltest
Package gqltest provides the fake GraphQL client every command test injects: it records calls and replays JSON fixtures, so no test ever touches the network.
Package gqltest provides the fake GraphQL client every command test injects: it records calls and replays JSON fixtures, so no test ever touches the network.
initiative
Package initiative owns the lin initiative and initiative-label groups.
Package initiative owns the lin initiative and initiative-label groups.
issue
Package issue owns the lin issue command group: CRUD, archive lifecycle, relations and list filters.
Package issue owns the lin issue command group: CRUD, archive lifecycle, relations and list filters.
label
Package label owns the lin label command group (issue labels).
Package label owns the lin label command group (issue labels).
lerr
Package lerr defines the error taxonomy mapped to sysexits codes at the CLI boundary.
Package lerr defines the error taxonomy mapped to sysexits codes at the CLI boundary.
milestone
Package milestone owns the lin milestone command group (project milestones).
Package milestone owns the lin milestone command group (project milestones).
notification
Package notification owns the lin notification command group (viewer inbox).
Package notification owns the lin notification command group (viewer inbox).
org
Package org owns the lin org command group (organization admin).
Package org owns the lin org command group (organization admin).
output
Package output renders command results.
Package output renders command results.
project
Package project owns the lin project and project-label command groups.
Package project owns the lin project and project-label command groups.
reaction
Package reaction owns the lin reaction command group.
Package reaction owns the lin reaction command group.
release
Package release owns the lin release, release-note and release-pipeline command groups.
Package release owns the lin release, release-note and release-pipeline command groups.
resolve
Package resolve turns human-friendly references (team key, user email, project/state/label names, issue identifiers) into the UUIDs the GraphQL mutations require.
Package resolve turns human-friendly references (team key, user email, project/state/label names, issue identifiers) into the UUIDs the GraphQL mutations require.
schedule
Package schedule owns the lin schedule command group (time schedules).
Package schedule owns the lin schedule command group (time schedules).
state
Package state owns the lin state command group (workflow states).
Package state owns the lin state command group (workflow states).
statusupdate
Package statusupdate owns the lin status-update command group.
Package statusupdate owns the lin status-update command group.
team
Package team owns the lin team command group.
Package team owns the lin team command group.
template
Package template owns the lin template command group.
Package template owns the lin template command group.
testutil
Package testutil hosts the command-level test harness: build a factory around the fake client, run a command line through the real root, capture stdout/stderr and the returned error.
Package testutil hosts the command-level test harness: build a factory around the fake client, run a command line through the real root, capture stdout/stderr and the returned error.
triage
Package triage owns the lin triage command group (triage responsibilities).
Package triage owns the lin triage command group (triage responsibilities).
user
Package user owns the lin user command group (read-only).
Package user owns the lin user command group (read-only).
viewer
Package viewer owns the whoami command (authenticated user).
Package viewer owns the whoami command (authenticated user).
webhook
Package webhook owns the lin webhook command group.
Package webhook owns the lin webhook command group.
tools
docsgen command
Command docsgen turns `lin describe` JSON (read from stdin) into an MDX command reference for the Fumadocs site: one page per top-level resource group, an index, and a reference page for exit codes and global flags.
Command docsgen turns `lin describe` JSON (read from stdin) into an MDX command reference for the Fumadocs site: one page per top-level resource group, an index, and a reference page for exit codes and global flags.
schema command
Command schema refreshes the vendored Linear GraphQL schema (internal/gql/schema.graphql) from the public linear/linear monorepo.
Command schema refreshes the vendored Linear GraphQL schema (internal/gql/schema.graphql) from the public linear/linear monorepo.
smokemock command
Command smokemock is a local stand-in for Linear's GraphQL API used by scripts/smoke-local.sh to drive the REAL lin binary end-to-end (process exit codes, auth header, upload PUT) without network or credentials.
Command smokemock is a local stand-in for Linear's GraphQL API used by scripts/smoke-local.sh to drive the REAL lin binary end-to-end (process exit codes, auth header, upload PUT) without network or credentials.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL