Linear
GitHub-only and Linear-only Claude Code skills that port the jot CLI loop — capture brain-dumps into well-formed tracker issues, decide what to work on next, and keep the backlog tidy. Each skill is a single SKILL.md invoked from inside Claude Code.
npx -y skills add andresd/jot-skills --skill linearAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Capture, triage, and plan work on Linear issues from natural language, across one or many teams. Auto-triggers on phrases like "capture this in Linear:", "what should I work on next in Linear", "what's on my plate in Linear", "tidy the Linear backlog", "find duplicate Linear issues", "all <X> Linear bugs to @<user>", references to Linear issue keys (e.g. "ENG-123"), and explicit `/jot-linear <cmd>`, `linear:`, or `jot-linear:` prefixes. Use when the user wants to turn brain-dump text into a well-formed Linear issue, decide what to work on next in Linear, run dedup/stale passes, or bulk-edit Linear issues by natural-language intent. Requires the Linear MCP server to be configured in Claude Code (`https://mcp.linear.app/sse`).
SKILL.md
21.3 KB, as published. Nobody here has run it
jot-linear
A Linear-only port of the jot CLI loop: capture brain-dumps into well-formed issues, decide what to work on next, and keep the backlog tidy — all through Linear's official MCP server. No tracker abstraction (Linear only), no licensing, no persistent SQLite. Auth is handled by the MCP transport (OAuth).
This skill is the Linear sibling of jot-gh. If both are installed, disambiguate by the user's explicit cue (the word "Linear", a linear: prefix, or a Linear issue identifier like ENG-123). When the cue is missing and a git repo is present, prefer jot-gh.
When to invoke
Explicit (always invoke):
/jot-linear <command>— slash formlinear: <command>orjot-linear: <command>— prefix form
Auto-invoke (use judgment, prefer explicit when unsure):
- "capture this in Linear: ..." / "log this in Linear: ..."
- "what should I work on next in Linear?" / "what's next in Linear?"
- "what's on my plate in Linear today?" / "what am I doing today in Linear?"
- "tidy the Linear backlog" / "find duplicate Linear issues"
- "(re)assign all <X> Linear bugs to @<user>"
- "make all <X> Linear issues urgent/high/medium/low"
- "what does the Linear backlog say about <topic>?"
- Any input that names a Linear issue identifier (
ENG-123,OPS-7) and asks for an issue-lifecycle action.
Do NOT invoke for:
- General Linear questions unrelated to issue triage ("how do I configure Linear's GitHub integration?")
- Cycle planning, roadmap editing, or project-level work (out of scope for this skill)
- Anything that does not touch the issue lifecycle (issues, labels, assignees, comments, workflow states)
Prerequisites
Before any command, verify the Linear MCP is reachable. The MCP must be configured in Claude Code, typically via:
claude mcp add linear --transport sse https://mcp.linear.app/sse
The first call will trigger an OAuth flow in the browser. If the MCP tools are not present in this session's available tools, stop and tell the user:
The Linear MCP isn't configured. Add it with
claude mcp add linear --transport sse https://mcp.linear.app/sse, complete the OAuth flow in the browser, and re-invoke this command.
MCP tool discovery
Linear's MCP server exposes a set of typed tools. The exact names depend on the MCP server version and may be prefixed by the name the user gave the server in their config (commonly linear or Linear).
Discover the actual tool names at invocation time from the available-tools list in this session. Look for tools matching these operations:
| Operation | Likely tool name |
|---|---|
| Create issue | create_issue / createIssue |
| Update issue (any field) | update_issue / updateIssue |
| Get issue by id or identifier | get_issue / getIssueById |
| Search issues by query/filter | search_issues / searchIssues |
| List issues (filtered) | list_issues / getIssues |
| Get issues assigned to a user | get_user_issues / list_my_issues |
| Add comment | create_comment / addComment / createComment |
| Delete comment | delete_comment / deleteComment |
| List teams | list_teams / getTeams |
| List workflow states for a team | get_workflow_states / getWorkflowStates |
| List labels for a team | list_labels / getLabels |
| Archive issue | archive_issue / archiveIssue |
If a needed operation is not exposed (e.g. some servers omit delete_comment), surface that to the user rather than working around it — Linear's UI is fine for the missing step.
Throughout this file, calls are written as linear.createIssue({...}) for brevity. Substitute the actual MCP tool name at invocation time.
Team resolution
Most commands operate on one team at a time. To pick:
- If the user passed
--team TEAM_KEY(e.g.--team ENG), use that. - Else if
LINEAR_DEFAULT_TEAMenv var is set, use it. - Else, call the list-teams tool and ask via
AskUserQuestion. Offer the top 3 teams as options.
Hold both the team key (string like ENG, used for display and identifiers) and the team id (UUID, used in mutations). Most MCP tools accept either; pass the id when offered the choice.
Multi-team flows (next by default, ask if no team context): use the search/list tools without a team filter. Each issue carries its team in the result; treat writes per-team. Do not run tidy, assign, or reprioritize across teams — those are single-team only.
Issue identifiers
Always display issues as TEAM-KEY-NUMBER (e.g. ENG-42), not as bare numbers or UUIDs.
For mutations that require a UUID, resolve the identifier via the get-issue tool first:
linear.getIssue({ id: "ENG-42" }) // returns { id: <uuid>, identifier: "ENG-42", ... }
(Most MCP get-issue tools accept either form on the id parameter.)
Priority mapping
Linear has a native priority field (Int, 0–4). The skill maps to it directly — no priority labels.
| Linear value | Meaning | jot mental model |
|---|---|---|
| 1 | Urgent | P0 |
| 2 | High | P1 |
| 3 | Medium | P2 |
| 4 | Low | P3 |
| 0 | No priority | none |
When the user says "P0" / "urgent" / "blocker", map to 1. When they say "P3" / "low", map to 4. Treat the words as interchangeable; output uses the Linear name (Urgent/High/Medium/Low) for clarity.
Workflow states
Linear teams define their own workflow states, each with a type in { triage, backlog, unstarted, started, completed, canceled }.
- "Open" issues = state type ∈
{triage, backlog, unstarted, started}. - "Close" an issue = move it (via update-issue with
stateId) to a state of typecompleted(or, when the user explicitly says "cancel as duplicate / won't do",canceled). - For each team, call the list-workflow-states tool once per turn and cache in-memory.
- Pick the canonical completed state: prefer one named exactly "Done", else the first
type == "completed".
If a team has no completed state (unusual), stop and tell the user — do not fabricate one.
Memory file: ~/.config/jot-linear/<team-key>.md
Linear is not tied to a git repo, so memory lives in user config rather than alongside source.
- Path:
~/.config/jot-linear/<team-key>.md(lowercase the team key, e.g.eng.md). - Cap at ~4KB. If larger, take the first 4KB and tell the user the file was truncated.
- Read it before any LLM-y step (extraction, ranking, dedup decision).
The memory file holds team-specific rules: glossary, priority defaults, label conventions, "all bugs filed by @alice are High", "anything reported in #incidents Slack channel is Urgent", workflow-state preferences.
Apply memory rules when:
- Extracting structured fields in
capture(priority, labels, assignee) - Ranking issues in
next - Deciding dedup similarity in
captureandtidy
If memory rules conflict with the user's explicit request in this turn, the user wins.
Cache: ~/.cache/jot-linear/
Cache recent issue digests at ~/.cache/jot-linear/<team-key>.json so next/tidy don't re-fetch hundreds of issues per invocation.
- TTL: 10 minutes. Compare file
mtimeto now; if older, refetch. - Bypass: any command with
--no-cache. - Miss path: call the list-issues tool filtered to the team and open states, write the result to cache.
- Cache is not load-bearing. Safe to delete at any time. Never treat a cached value as authoritative for writes — re-fetch the issue before any update.
Action log: ~/.cache/jot-linear/actions.jsonl
Every successful write appends one JSON line:
{"ts":"2026-05-20T10:00:00Z","cmd":"capture","team":"ENG","action":"created","identifier":"ENG-42","issue_id":"<uuid>","url":"https://linear.app/...","inverse":{"op":"archive","issue_id":"<uuid>"}}
- Keep only the last 100 lines (trim head on each append).
- Used exclusively by
undo. - Skip the log for previews, dry-runs, and read-only commands.
Commands
capture <text>
Natural-language brain-dump → well-formed Linear issue, with dedup check first.
Flow:
- Resolve team. Load
~/.config/jot-linear/<team-key>.mdif present. - Pull existing labels and the team's completed state via the list-labels and list-workflow-states tools (cached for the turn).
- Extract 3–5 keywords from
<text>(noun phrases, key verbs). - Dedup search via the search-issues tool, scoped to the team and to open states:
If the MCP exposes only a list/filter tool, use it with an equivalent filter.linear.searchIssues({ query: "<keywords>", teamId: "<id>", first: 10 }) - Score top 5 candidates for semantic similarity vs.
<text>. If best score reads as "likely duplicate" (same bug/feature, not just same topic), surface that. Treat completed/canceled candidates as informational only — do not propose commenting on them. - Extract structured fields, constrained by the existing-labels list and memory rules:
- title: concise, imperative for actions ("Fix login redirect loop"), declarative for observations ("Search returns 500 on empty query").
- description: original text, then a blank line, then
_Captured via jot-linear on YYYY-MM-DD._ - priority: integer 0–4. Default
3(Medium) unless memory rules say otherwise. - labelIds: pick zero-or-more from the existing labels (by UUID).
- assigneeId: only if
<text>explicitly names a person via@handle; resolve via the user-lookup tool (or fall back to the team's user list).
- Preview via
AskUserQuestion:Create issue(default)Comment on dup ENG-N(only if a dup was found)Edit fields firstCancel
- On
Create issue:
Append to action log withlinear.createIssue({ teamId, title, description, priority, labelIds: [...], assigneeId })inverse: { op: "archive", issue_id: <new uuid> }. - On
Comment on dup ENG-N:
Log withlinear.createComment({ issueId: "<uuid>", body: "<captured text>\n\n_Linked via jot-linear dedup._" })inverse: { op: "delete_comment", comment_id }. - On
Edit fields first: ask which field (title/description/priority/labels/assignee), update, re-prompt.
Do not invent labels that don't already exist on the team. If a needed label is missing, ask the user before creating it (via the label-create tool if the MCP exposes it, or surface that the label needs to be created in the Linear UI).
next [--for @user] [--scope <filter>] [-n N] [--commit] [--team TEAM_KEY]
Top-N ranked open issues with a one-line rationale per item. Defaults: --for @me, n=5, multi-team.
Flow:
- List candidates via the list-issues or get-user-issues tool:
For# multi-team (default) linear.listIssues({ filter: { assignee: { isMe: true }, state: { type: { nin: ["completed", "canceled"] } } }, first: 100 }) # single-team when --team passed linear.listIssues({ filter: { team: { key: "<KEY>" }, assignee: { isMe: true }, state: { type: { nin: ["completed", "canceled"] } } }, first: 100 })--for @other-user, resolve the user first, then filter byassigneeId. - For each distinct team in the results, load
~/.config/jot-linear/<team-key>.md(cache the load per turn). - Rank by, in order:
- Priority:
1 (Urgent) > 2 (High) > 3 (Medium) > 4 (Low) > 0 (No priority) - State type:
started > unstarted > backlog > triage jot-todaylabel present (boost)- Age (newer wins for Urgent/High, older wins for Medium/Low)
- Memory-rule hints
- Priority:
- Output a numbered list. Each line:
[ENG-42]prefix, title, priority badge (e.g.[High]), state in parens, one-sentence rationale, then URL on the next line. - If
--commit: confirm via a singleAskUserQuestion(Commit these N to today / Cancel), then for each chosen item:- Ensure the team has a
jot-todaylabel (create if missing, asking first). linear.updateIssue({ id, labelIds: [...current, jotTodayLabelId] }).- Log each.
inverse: { op: "set_labels", issue_id, labelIds: [<prior list>] }.
- Ensure the team has a
tidy [--team TEAM_KEY]
Interactive backlog hygiene. Single-team only (multi-team tidy is too noisy).
Flow:
- Resolve team.
- Fetch all open issues via the list-issues tool, requesting up to
first: 250. Include description, labels, assignee, and last-comment timestamp where available. - Run four passes:
- dup — cluster by title+description similarity. Report any cluster of 2+, recommending the oldest as canonical and the rest to close-as-duplicate (move to a canceled-type state with a "duplicate of ENG-N" comment).
- stale —
updatedAt > 30 days agoand no recent comments. Recommend close with comment, or labelstale. - stale-assignment — has assignee, no comment or edit in last 14 days. Recommend unassign and ping.
- missing-priority —
priority == 0. Recommendpriority: 3(Medium) as default (or memory-rule default).
- Output a single numbered list (text mode — not
AskUserQuestion; tidy routinely produces 20+ findings):1. [dup] ENG-42 looks like dup of ENG-38 — close ENG-42 with link? 2. [stale] ENG-15 — no activity since 2026-04-12 (38d). Close with comment? 3. [stale-asn] ENG-71 — @bob assigned 22d ago, no movement. Unassign? 4. [priority] ENG-88 — no priority set. Add Medium? ... - Prompt the user in plain text:
Apply which? Reply with numbers (e.g.
1,3,5),all, orcancel. - Apply each accepted finding via the appropriate MCP tool (
updateIssue,createComment, etc.). Log every write.
ask <question> [--team TEAM_KEY]
NL Q&A over the backlog with ENG-N citations. Read-only.
Flow:
- Resolve team (or stay multi-team if none given).
- Search via the search-issues tool, including completed/canceled results:
linear.searchIssues({ query: "<keywords>", teamId: "<id or null>", first: 30, includeArchived: false }) - Answer in 2–4 sentences. Cite each referenced issue as
[ENG-42](URL)(markdown link). - No writes. No action log.
assign <NL intent> [--team TEAM_KEY]
Bulk reassignment by natural language. Single-team only.
Example intents:
- "all open auth bugs to @alice"
- "@bob's open issues to @carol"
- "everything labeled regression to @me"
Flow:
- Parse the intent into
{ label?, assignee_old?, assignee_new, state? }. If parsing is uncertain, ask viaAskUserQuestionwith the best-guess filter. - Resolve assignee user(s) via the user-lookup or list-users tool. Hold the UUID for use in mutations.
- List candidates via the list-issues tool, building the filter from non-null fields only. Linear's filter accepts
team,assignee,labels,statepredicates; omit predicates whose argument is null. - Show the numbered candidate list (text mode).
- Prompt:
Reassign these to @<new>? Reply with numbers,
all, orcancel. Numbers SKIPPED will keep their current assignee. - For each chosen item, re-read the issue (fresh, no cache). If
<new>is already assigned, skip. Else:
Linear's assignee is single-valued;linear.updateIssue({ id: "<uuid>", assigneeId: "<new uuid>" })assigneeIdreplaces the previous. - Log each.
inverse: { op: "set_assignee", issue_id, assigneeId: <prev uuid or null> }.
reprioritize <NL intent> [--team TEAM_KEY]
Bulk priority change by natural language. Single-team only.
Example intents:
- "all launch-blocker label → Urgent"
- "all Low-priority bugs older than 90 days → cancel as stale" (this overlaps with
tidy; route totidyif the intent is hygiene) - "@alice's open issues to High"
Same flow as assign, but operating on the native priority field (1–4):
- For each accepted item:
linear.updateIssue({ id, priority: <new> }). - Log with
inverse: { op: "set_priority", issue_id, priority: <prev> }.
status [--team TEAM_KEY]
Default: list issues with jot-today label assigned to @me, across teams. Show priority, team, and state.
linear.listIssues({
filter: {
assignee: { isMe: true },
labels: { name: "jot-today" },
state: { type: { nin: ["completed", "canceled"] } }
},
first: 50
})
--team TEAM_KEY: same query but scoped to one team.
--team TEAM_KEY --all-assignees: list jot-today issues in the team across all assignees, grouped by assignee.
No writes.
clear-plan
Remove jot-today from all open issues currently labeled with it for @me (across all teams).
Flow:
- List as in
status. - Show count and titles. Confirm via
AskUserQuestion:Clear N items / Cancel. - For each: re-fetch labels,
linear.updateIssue({ id, labelIds: [<prior minus jot-today>] }). Log.
undo
Reverse the most recent action within 24 hours.
Flow:
- Read
~/.cache/jot-linear/actions.jsonl(tail). - Find the most recent entry where
ts > now - 24h. If none, tell the user no recent action is reversible. - Show the action and its planned inverse via
AskUserQuestion:Undo / Cancel. - Re-read the affected issue's current state (no cache). If state has changed in a way that makes the inverse meaningless (e.g. issue already archived, label already gone), abort with a clear explanation rather than forcing it.
- Apply the inverse using the appropriate MCP tool:
created issue_id→linear.archiveIssue({ id }). Append a comment "Reverted by jot-linear undo" viacreateCommentfirst so the audit trail is visible.archived issue_id→ unarchive tool if available; otherwise tell the user to unarchive in the Linear UI.set_state to=X from=Y→updateIssue({ id, stateId: Y })set_priority to=X from=Y→updateIssue({ id, priority: Y })set_assignee to=X from=Y→updateIssue({ id, assigneeId: Y })(Y may be null)set_labels prior=[...]→updateIssue({ id, labelIds: [prior...] })commented comment_id→deleteComment({ id })
- On success, remove the entry from the log. Do not chain undos — one
undoreverses one action.
help
Print one line per command above. No MCP calls. No log.
Output conventions
- Cross-team listings: always show
[TEAM-KEY-N](e.g.[ENG-42]). - Single-team listings: show bare
ENG-42. - Every write reports the resulting URL on the next line (MCP
createIssue/updateIssueresponses include the issue URL — use it directly). - No emoji in skill output unless the user uses emoji first.
- MCP tool errors surface verbatim — do not swallow them. If a mutation returns an error or
success: false, stop the batch and report which item failed. - Priority labels in output use Linear's names (Urgent/High/Medium/Low/No priority), not the
P0..P3mental model — match Linear's UI vocabulary so URLs and screenshots line up.
Confirmation patterns
- Single write (
capturecreate, single-itemassign,undo,--commit):AskUserQuestionwith Confirm / Edit / Cancel. - Multi-write (
tidy, bulkassign, bulkreprioritize,clear-plan): numbered text list, free-text reply (1,3,5/all/cancel).AskUserQuestiononly fits 4 options, so it does not scale here. - Read-only (
status,ask,nextwithout--commit): no confirmation.
Anti-patterns
- Don't fall back to raw
curlagainst the Linear GraphQL API while the MCP is present. The MCP is the supported transport; bypassing it loses auth handling and rate-limit retries. - Don't batch writes without showing a preview first, even when the user said "go ahead" earlier in the turn — previews are cheap insurance against an LLM misparse.
- Don't invent labels that aren't already on the team. If a needed label is missing, ask before creating it.
- Don't write to completed or canceled issues unless the user explicitly named the issue.
- Don't store the OAuth token anywhere — the MCP transport owns it.
- Don't treat the cache as authoritative for writes — always re-fetch the issue before update.
- Don't run
tidy,assign, orreprioritizeacross teams. They are single-team by design. - Don't chain undos. One
undoreverses one action; if the user wants more, they invokeundoagain. - Don't delete issues on undo — archive them. Linear's archive is soft and recoverable; delete is permanent and not a safe inverse of "created".
- Don't use priority labels (
P0,P1, ...). Linear has a native priority field; using labels for the same concept fragments the data. - Don't paper over a missing MCP tool by inventing a workaround. If the connected Linear MCP doesn't expose a needed operation (e.g.
deleteComment), surface that to the user and ask them to perform that step in the Linear UI.