| name | using-lin-linear-cli |
| description | MUST be used when creating, updating, searching, assigning, or commenting on Linear issues — anything beyond reading a known ticket ID. `lin` is a human-friendly wrapper over `linctl graphql` that resolves team/project/user names and issue identifiers to IDs internally, so you stop fumbling syntax. Covers child issues with parent links, assigning to other users, fuzzy user/team/project lookup, filtered search, the learned disambiguation cache, and the raw GraphQL escape hatch. Triggers on: linear ticket, create issue, child ticket, sub-issue, assign ticket, assign to, link parent, search linear, find tickets, linear comment, update issue, lin cli, linctl, who is on linear, ambiguous name linear. |
Using lin — the Linear CLI
lin is a thin wrapper over linctl graphql (~/dotfiles/scripts/lin.ts, symlinked to ~/.local/bin/lin). It exists because linctl's first-class commands can't set a parent or assign to other users, and because every non-trivial Linear task used to mean fumbling name→ID resolution. lin accepts human values (team keys, project/user names, issue identifiers) and resolves them to IDs internally.
It shells out to linctl graphql, so it inherits linctl's existing auth — no separate token, no MCP.
When to use lin vs linctl
- Reading a known ticket: either works (
lin get BEE-123 delegates to linctl issue get).
- Everything else (create, child, assign-to-someone-else, search by filters, comment with multi-line body, update): use
lin. linctl issue create literally cannot set --parent or assign to anyone but yourself.
Core commands
lin who <query>
lin team <query>
lin project <query>
lin get <id>
lin find [filters]
lin new --title ... -t ...
lin child <parent> ...
lin update <id> [opts]
lin comment <id> [opts]
lin cache [clear]
lin gql '<query>' ['<vars-json>']
The five things that used to be painful (now one command each)
lin child BEE-20001 --title "Wire up API" --assignee phil
lin who phil
lin find -t BEE --project group -a me --state "In Progress"
lin new --title "Fix bug" -t WEB --project "group" -a me --priority high
lin update BEE-20001 -a "Phil Mills" -s "In Review"
Name resolution rules (how ambiguity is handled)
Resolution is fail-loud, never silent. For users, in order:
me → the authenticated viewer.
- Exact email / displayName / full-name match wins.
- Team-membership filter — if the issue's team is known, candidates not on that team are dropped. This dissolves most "two people, same name" cases for free. (Example: there are two Phils, but they never share a team, so
--team BEE or a BEE project resolves "phil" → Phil Mills unaided.)
- Recency (scoped MRU) — if still ambiguous, the most-recently-resolved matching user in this scope wins, with an
ℹ … note to stderr.
- Still ambiguous → stop with exit 1 and list candidates.
When you hit an ambiguity error, you have two moves:
- Pass an unambiguous value once:
--assignee phil@beehiiv.com (or the exact display name). This also teaches it — see below.
- Add context that disambiguates:
--team BEE.
Projects: partial names + the MRU cache
Never type a full project name, and never type the em-dash. beehiiv project names are full of — (sometimes with double spaces, e.g. Podcast — Phase 2) which is painful and error-prone from a shell. lin fuzzy-matches with containsIgnoreCase, so pass a distinctive partial:
--project "group sub"
--project "505"
Projects often cluster (Foo — MVP, Foo V2, Foo — Phase 2…), so a partial can match several. Resolution handles that without an LLM, fail-loud:
- Exact or single match → use it, and record it to the MRU.
- Multiple matches → if one (or more) is in the MRU, auto-pick the most recently used and print an
ℹ … note to stderr.
- Multiple matches, none in MRU → stop with exit 1, list candidates. Re-run with a more specific partial; the one that resolves is remembered, so next time the loose query auto-picks it.
The MRU (projectMru in the cache) is also fed whenever you work a specific ticket — lin child <id> / lin update <id> push that ticket's project to the front. So in a normal session you rarely hit ambiguity for new: you've already touched tickets in the active project, and the loose --project partial resolves straight to it.
Don't hardcode the project — infer it from the ticket you're working from or the conversation, pass a partial, and let the MRU do the disambiguating. lin cache shows the current MRU.
How learning works: success IS the learning
There is no explicit teach step (no --learn flag). Every time a name resolves to exactly one user or project, lin records it to an MRU; the next ambiguous lookup uses that recency. The cache is "frozen intelligence" — you make the smart call once (by typing an exact name/email), and the dumb script replays it forever.
The flow when two people genuinely share a team:
lin child BEE-20001 --title "..." --assignee chris
lin child BEE-20001 --title "..." --assignee "Chris Smith"
lin child BEE-20002 --title "..." --assignee chris
Scoping matters and is deliberate: projects use one global MRU (you move between projects sequentially, so recency = current focus). Users are scoped project → team → global, recording into the most specific scope available. A global user MRU would silently mis-pick the same first-name across different projects — exactly the clever-but-silent failure to avoid. So chris learned in one project does not leak to another; it'll fail loud there until you resolve it once in that context.
Multi-line descriptions & comments
Three input modes for --description (new/update) and --body (comment):
lin comment BEE-1 --body "short inline"
lin comment BEE-1 --body-file notes.md
printf 'multi\nline' | lin comment BEE-1 --body -
lin new --title X -t BEE --description "$(cat <<'EOF' # HEREDOC
Multi-line
body
EOF
)"
Flags (kept close to linctl where they overlap)
-t/--team · --project · --parent · -a/--assignee · -m/--assign-me · -d/--description · --description-file · -b/--body · --body-file · -s/--state · --labels a,b · --priority none|urgent|high|normal|low|0-4 · --query · --limit · --include-completed · -j/--json
Use -j/--json when you need to parse output (e.g. grab .id/.identifier with jq).
Escape hatch
Anything lin doesn't wrap, reach the Linear API directly — this is how the wrapper itself works, and how it was built:
lin gql 'query($id:String!){ issue(id:$id){ id identifier title } }' '{"id":"BEE-20001"}'
linctl graphql -j -q '<query>' --variables '<json>'
issueDelete trashes (soft-deletes / archives) — it won't appear on the active board but lingers under includeArchived:true until Linear purges it.
Closing as duplicate
lin dupe BEE-19635 BEE-19285
lin dupe BEE-19635 BEE-19285 -b "shipped in PR #23756"
Order matters under the hood: Linear refuses to move an issue into the Duplicate workflow state unless the duplicate-of relation already exists (API rejects with "missing duplicate relation"). lin dupe does relation-then-state-then-optional-comment in the right order. The optional -b is recommended when the why isn't obvious from the title — it's the human/AI reasoning the wrapper deliberately doesn't synthesize.
If you ever need to do it by hand (different relation type, batched mutation, etc.) — note that IssueRelationType is a GraphQL enum, so the value goes in bare without quotes: type: duplicate.
Extending it
This tool is built to grow. When a Linear task feels clunky, add a command or flag to ~/dotfiles/scripts/lin.ts (no build step — #!/usr/bin/env bun shebang, edit and run). Default values for beehiiv work live in ~/Projects/beehiiv/CLAUDE.md (team WEB, assignee michael.carter, current project).