linctl

module
v0.7.0 Latest Latest
Warning

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

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

README ΒΆ

linctl

CI Go Release

Reads everywhere. Writes fail closed. No standing context cost.

Your agent's Linear MCP server loads its tool definitions into every session before it does any work, on the order of ~13k tokens (measured).[^mcp] linctl is one binary with no standing context: only a command's output costs tokens, and reads need no pin. Every write re-resolves the active OAuth credential and fails closed when the resolved org/team doesn't match the target pinned for the repo, so stale or wrong auth can't quietly land issues in the wrong team. There is no bypass flag.

linctl issue list --mine --state started     # read anything
linctl issue create --title "Spike: exports" # write only inside the pinned target

πŸ”’ How writes stay safe

linctl's vocabulary deliberately separates reads from writes:

  • Pinned Target β€” the org/team/(optional project) a repo declares in .linctl.toml as the only allowed destination for writes.
  • Resolved Target β€” the org/team/project proven from the active OAuth credential at command time.
  • Target Mismatch β€” when the two disagree. For a guarded write this is a hard stop, never a prompt or a warning.

The guard checks the active OAuth credential, not just the file. An agent that edits .linctl.toml to point at another org still can't write there unless its local auth also resolves to that org. Team-scoped creates compare org + team (the entity does not exist yet); resource-scoped updates and archives resolve the existing entity first, then compare the pinned project_id when one is configured. There is no bypass flag β€” --org, --team, --team-id, and --project set the pinned target, they do not relax the guard. See docs/adr/0001-linctl-architecture-baseline.md.

Mermaid source for the diagram above
flowchart LR
    A[linctl write command] --> B[Resolve Target<br/>from active auth]
    B --> C{Resolved matches<br/>Pinned Target?}
    C -->|match| D[Guarded write proceeds]
    C -->|mismatch| E[Target Mismatch<br/>hard stop Β· no mutation]

πŸ€– Agent-first

linctl is built to be driven by an LLM agent from a Bash tool, with deterministic output and no standing context cost:

  • No MCP tax. The Linear MCP server loads ~13k tokens of tool definitions into every session before any work.[^mcp] linctl loads none β€” linctl usage returns a compact, on-demand reference (~400 tokens) and linctl <group> --help covers the rest.
  • Structured output. --json / --compact / --fields / --id-only give stable, pipeable shapes; diagnostics go to stderr so stdout stays clean.
  • Drop-in skill. skills/linctl/SKILL.md teaches an agent to drive linctl and ships an AGENTS.md snippet for consuming repos. Verify a checkout with no credentials via bash skills/linctl/scripts/linctl-offline-smoke.sh, or do a read-only auth check via bash skills/linctl/scripts/linctl-smoke.sh.

⚑ Quickstart

Install
# Homebrew cask (macOS)
brew install --cask KyaniteHQ/linctl/linctl

# Go toolchain (macOS / Linux / Windows)
go install github.com/KyaniteHQ/linctl/cmd/linctl@latest
linctl --version && linctl usage   # zero-config smoke check β€” no auth required

Prebuilt binaries (darwin/linux/windows Γ— amd64/arm64) and checksums are attached to every release.

From source checkout
git clone https://github.com/KyaniteHQ/linctl.git && cd linctl
GOBIN="$PWD/bin" go install ./cmd/linctl
./bin/linctl --version

Use your platform or distro package manager to install Go first. If you install Go manually from go.dev/dl, verify the published checksum and follow Go's platform-specific instructions instead of replacing a managed /usr/local/go.

Configure

Pin the repo target in .linctl.toml, then configure OAuth app material outside repo config.

You need a Linear OAuth app client id before the auth step can continue. Export it, or pass the value directly with --client-id; use a client secret only when your app has one. Keep both values out of .linctl.toml and do not print them.

: "${LINCTL_OAUTH_CLIENT_ID:?set this to your Linear OAuth app client id}"

linctl auth configure \
  --client-id "$LINCTL_OAUTH_CLIENT_ID" \
  --redirect-uri "http://127.0.0.1:8765/callback" \
  --scopes read,write,issues:create,comments:create

Use --client-secret "$LINCTL_OAUTH_CLIENT_SECRET" when the app has a secret. Never print secrets; report them as set or missing.

.linctl.toml example + auth setup paths
[target]
org_id     = "linear-org-id"
team_key   = "LIT"
team_id    = "linear-team-id"
project_id = "optional-linear-project-id"   # omit for team-scoped writes

Local auth state is machine-local and stays outside .linctl.toml. Use linctl auth app for headless app-actor auth when a client secret is available, or linctl auth login for browser authorization-code auth. Browser login uses the app actor by default; pass --actor user when you need personal attribution. linctl auth refresh explicitly refreshes or reacquires token state, and linctl auth logout revokes tokens when Linear accepts revocation, removes local token state, and keeps app configuration unless --forget-app is passed.

Environment OAuth variables are non-persistent automation overrides, not repo config. A repo .linctl.toml overlays global target config.

First commands
linctl usage              # orientation β€” no auth required
linctl auth login         # browser authorization-code auth
linctl auth status        # actor, scopes, expiry, and target readiness
linctl target --json      # confirm the active auth org / team / project
linctl doctor             # config, auth, and target health
linctl issue list --mine  # your issues in the pinned team

πŸ“– Command reference

Across 60 top-level command groups, linctl maps the Linear schema. The most-used ones are below; the exhaustive catalog with GraphQL backing lives in docs/domain-map.md, and linctl <group> --help lists every subcommand.

Context & health

linctl target --json          # resolved org/team/project for the active auth
linctl doctor                 # config / auth / target health report
linctl current                # the issue for the current git branch
linctl next --dry-run         # preview the top-ranked unblocked issue

Issues β€” reads, rich list filters, and guarded writes. Related: issue-relation, comment, agent-session.

linctl issue list --state started --mine --limit 20
linctl issue get LIT-123 --json
linctl issue deps LIT-123                       # parent / children / blocks / blocked-by
linctl issue search "flaky export test"
linctl issue create --title "Spike: exports" --assignee <user-id> --label <label-id> --estimate 3
linctl issue link https://example.com/spec LIT-123   # attach a URL (guarded)
More groups β€” projects, cycles, planning, teams, search, releases, customers, metadata

Projects β€” reads plus create/update/archive. Related: project-update, project-status, project-label, project-relation.

linctl project list --limit 20
linctl project get <project-id> --json
linctl project issues <project-id>
linctl project-milestone list <project-id>

Cycles & sprints β€” cycle writes the schema entity; sprint is a read-only report alias.

linctl cycle list
linctl sprint current                           # active cycle for the team
linctl sprint report <cycle-id>

Planning β€” Initiatives are the current strategic surface; roadmap* is legacy read-only.

linctl initiative list
linctl initiative projects <initiative-id>
linctl initiative-to-project list

Teams, users & org

linctl team list
linctl team members <team-id>
linctl user me
linctl organization teams

Search

linctl search issues "rate limit"
linctl semantic-search "exports are slow" --limit 20

Releases β€” release, release-note, release-pipeline, release-stage, issue-to-release, external-link.

linctl release list
linctl release-pipeline list

Customers β€” customer, customer-need, customer-status, customer-tier.

linctl customer list
linctl customer-need list

Metadata & more β€” most groups support list/get plus entity-specific reads: label, document, template, workflow-state, time-schedule, notification, triage-responsibility, sla-configuration, rate-limit, application, audit-entry, agent-activity, agent-session, agent-skill, external-user, custom-view, favorite, emoji, attachment. Run linctl <group> --help or see docs/domain-map.md.

🧰 Output & scripting

Output controls are global flags β€” combine them with any command.

Flag Effect
--json / --compact JSON output; --compact makes it single-line
--fields a,b.c project JSON to an allowlist of (dot-path) keys
--id-only emit only the Linear id, for $(...) chaining
--quiet suppress output on a successful write
--fail-on-empty exit non-zero when a list result is empty (monitors)
--sort FIELD --order asc|desc deterministic list ordering
--format minimal|compact|full human (non-JSON) output detail
--profile / --org / --team / --team-id / --project config profile and target overrides
--timeout 30s total per-command deadline across retries
--debug structured diagnostics to stderr (LINCTL_DEBUG_JSON=1 for JSON)
linctl issue list --json --compact --fields identifier,title,state
id=$(linctl --id-only issue create --title "task"); linctl issue start "$id"
linctl issue list --fail-on-empty --sort title --order asc

Stable JSON shapes for parsing are documented in skills/linctl/references/json-output.md.

✍️ Guarded writes

Every mutation is checked against the pinned target before it runs. Coverage:

  • Issues β€” create (with --assignee, --label, --due-date, --estimate, --parent for sub-issues, templates, and guarded import), update / --append, start, comment, reply, close, done, next start, and link (attach a URL).
  • Issue relations β€” relate, unrelate.
  • Comments β€” update, delete.
  • Projects β€” create, update, archive. Project updates β€” create.
  • Documents β€” create, update.
  • Cycles β€” create, update, archive.
  • Project milestones β€” create, update.

--estimate is validated against the team's estimation config; --parent confirms the parent belongs to the pinned target. For test runs, create namespaced throwaway resources (linctl-it-<runid>) and clean them up β€” close disposable issues, archive disposable projects.

πŸ”§ Development

go tool task ci                 # deps-check β†’ fmt-check β†’ generate-check β†’ vet β†’ test β†’ build β†’ smoke-run β†’ lint β†’ shellcheck β†’ actionlint β†’ vuln
go tool task coverage           # 100% hand-written statement coverage
go tool task release-preflight  # pre-tag local release gate; no tag, push, publish, or release secrets

internal/client/generated.go is generated by genqlient from internal/client/operations/*.graphql; CI fails on drift, so run go generate ./... and commit it after changing operations. Integration tests and the live smoke harness hit a disposable Linear org and never run under plain go test:

linctl auth configure --client-id "$LINCTL_OAUTH_CLIENT_ID" --redirect-uri "http://127.0.0.1:8765/callback" --scopes read,write,issues:create,comments:create
linctl auth app
go test -count=1 -tags=integration ./internal/client
go tool task browser-login-smoke-check
go tool task live-oauth
go tool task live-smoke
go tool task browser-login-smoke

For the project Infisical setup, the fixture secrets live under /linctl:

go tool task live-oauth-infisical
go tool task live-smoke-infisical
go tool task browser-login-smoke-infisical

browser-login-smoke-check is the deterministic non-live harness check. It requires no Linear secrets, browser, or network write, and proves missing-fixture handling, callback listener capture, and redaction sentinels. browser-login-smoke is the manual browser-login smoke in an isolated temp auth state: it starts a one-shot localhost callback listener, prints the Linear authorization URL, shows a browser success page after authorization, verifies redacted auth status --json, and removes the temp token state. The auth URL uses Linear re-consent, and the task defaults to user-actor login because an already-installed app-actor OAuth fixture opens Linear's Manage screen instead of producing a repeatable callback. Use live-oauth for the live OAuth app fixture check and repeatable app-actor fixture coverage; pass -- app only when testing a fresh browser install path.

Contributor workflow and the release process are in CONTRIBUTING.md; domain vocabulary is in CONTEXT.md; command-to-GraphQL mapping and named test scenarios are under docs/.

πŸ“„ License

MIT Β© 2026 KyaniteHQ

[^mcp]: Measured against the official Linear MCP server's tools/list: 38 tools, ~10.2k tokens compact and ~15.3k pretty-printed (tiktoken o200k_base), loaded before any work. The common ~13k is a midpoint; where you land depends on how your client serializes the schema and the Linear server version.

Directories ΒΆ

Path Synopsis
cmd
linctl command
Package main starts the linctl binary.
Package main starts the linctl binary.
internal
auth
Package auth manages local OAuth application and token state.
Package auth manages local OAuth application and token state.
cli
Package cli owns the linctl command-line surface.
Package cli owns the linctl command-line surface.
client
Package client contains Linear GraphQL client primitives.
Package client contains Linear GraphQL client primitives.
config
Package config loads linctl configuration from files and profiles.
Package config loads linctl configuration from files and profiles.
gitctx
Package gitctx derives Linear context from the current VCS checkout.
Package gitctx derives Linear context from the current VCS checkout.
oauth
Package oauth exchanges OAuth grants with Linear's token endpoint.
Package oauth exchanges OAuth grants with Linear's token endpoint.
render
Package render writes human and JSON command output.
Package render writes human and JSON command output.

Jump to

Keyboard shortcuts

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