Coding & Development🌐universal

linctl

A Go CLI for Linear with target-pinned guarded writes: every write re-resolves the active token and fails closed on org/team mismatch, with no bypass flag

Quick Start

Install this Skill

npx skills add KyaniteHQ/linctl
# linctl

[![CI](https://github.com/KyaniteHQ/linctl/actions/workflows/ci.yml/badge.svg)](https://github.com/KyaniteHQ/linctl/actions/workflows/ci.yml)
[![Go](https://img.shields.io/github/go-mod/go-version/KyaniteHQ/linctl)](go.mod)
[![Release](https://img.shields.io/github/v/release/KyaniteHQ/linctl)](https://github.com/KyaniteHQ/linctl/releases/latest)
[![License: MIT](https://img.shields.io/github/license/KyaniteHQ/linctl)](LICENSE)

**A Linear CLI that you can give to an AI agent.**

Your agent reads Linear as widely as its credential permits. Your agent writes only to the one
team that you name in the repo.

```bash
linctl issue list --mine                      # a read works anywhere
linctl issue create --title "Spike: exports"  # a write lands only in the pinned team
```

![What linctl is: an MCP server loads about 17,000 tokens of tool definitions into every agent session before any work, while linctl loads none. A write into the pinned team lands; the same write aimed at a team the credential cannot reach is refused with TARGET_MISMATCH and exit 1.](docs/assets/explainer.gif)

<sub>A [4K master](docs/assets/explainer-4k60.mp4) is also available. Source:
[`demo/readme-hero/`](demo/readme-hero/).</sub>

---

## The problem

You give an agent write access to Linear. Then one of these events occurs.

- The token is old, or it is the wrong token.
- The agent runs in a checkout that you forgot.
- The agent reads the wrong config and selects another team.

The agent writes anyway. The issues land in the wrong team, and you find the mistake later.

An OAuth token is usually much wider than the task in front of it. linctl makes the token
narrow. You name one team in the repo. Each write must then prove against the live credential
that it goes to that team.

## How the guard works

Before each write, linctl checks two facts against the **live** OAuth credential. linctl does
not use a cached value, and linctl does not trust the file on disk alone.

| | |
|---|---|
| **The organization** | The organization of the credential must be the organization pinned in `.linctl.toml`. A token from another organization fails here. |
| **The team** | The credential must reach the pinned team. An old token that lost its access fails here. |

If both facts hold, the write runs. If one fact fails, linctl stops, exits non-zero, and prints
`{"error_code":"TARGET_MISMATCH"}`. This is not a prompt. This is not a warning. **linctl has no
bypass flag.** [ADR 0001](docs/adr/0001-linctl-architecture-baseline.md) rejects a bypass flag.

![Guarded write flow: linctl resolves the target from the live OAuth credential, compares it to the target pinned in the repo, and either proceeds or hard-stops with no mutation](docs/assets/guard-flow.svg)

<details>
<summary>Diagram source (mermaid)</summary>

```mermaid
flowchart LR
    A[linctl write command] --> B[Resolve target<br/>from the live credential]
    B --> C{Matches the<br/>pinned target?}
    C -->|match| D[Write proceeds]
    C -->|mismatch| E[TARGET_MISMATCH<br/>hard stop, nothing mutated]
```

</details>

### See linctl refuse a write

The recording runs the same `issue create` two times. The first run goes to the pinned team, and
the issue lands. The second run goes to a team that the credential cannot reach, and linctl
changes nothing.

![linctl reads Linear freely, then refuses the same write when the active credential does not resolve to the pinned team, exiting non-zero with TARGET_MISMATCH](docs/assets/demo.gif)

The output and the exit codes are real. The organization id and the issue ids are invented, so
you can record the same demo with [`demo/render-fixture.sh`](demo/render-fixture.sh) and no
Linear account.

### What the guard does not do

Read this part. An honest limit is better than a safety claim that you must correct later.

- **The guard stops a confused agent. The guard does not stop a hostile agent.** linctl catches
  an old token, a wrong token, a stale checkout, and a config that the agent misread. linctl
  does not catch an agent that edits `.linctl.toml` to name a *sibling team that its own token
  already reaches*. The guard makes a credential narrow. The guard cannot be stronger than that
  credential. Make the token scope small as well.
- **The guard is a team boundary. The guard is not a correctness check.** If your agent updates
  the wrong issue *inside* the correct pinned team, the guard permits the write. The guard never
  claimed more.
- **The guard does not touch a read.** This is deliberate. `linctl issue list --all-teams` reads
  everything that the token can see.
- **Some label and team commands are organization-wide.** A label, a project label, and an
  initiative label have no team, so `--org-wide` compares the organization only. `team create`
  needs the same flag, because a new team is always outside the pinned team. The flag makes the
  blast radius visible, and a mismatch is still a hard stop, but the check is not a team check.
  The [gene

... (truncated)
Repository
12stars
1forks
View on GitHub
Details
DiscoveredOct 14, 2026
Last UpdatedOct 14, 2026
Quality Score
45/100