card
The bridge between planning in Linear and working in a thread. Two moves, one at each end of a piece of work.
Authentication
Needs a Linear API key, resolved in this order.
LINEAR_API_KEYin the environment.op readagainstLINEAR_TOKEN_OP_REF, if the 1Password CLI is installed. Defaults toop://Business/LINEAR_API_TOKEN/credential.
Get a key at Linear, Settings, API, Personal API keys. It needs read access to issues, and write access if you use move two.
Paths below assume the skill is at ~/.claude/skills/card. Adjust if you installed it elsewhere.
Move one — load a card
node ~/.claude/skills/card/scripts/card.mjs JAM-56 # let the card name its own skill
node ~/.claude/skills/card/scripts/card.mjs JAM-56 ce-plan # you name the skill
node ~/.claude/skills/card/scripts/card.mjs JAM-56 /ce-plan # same, leading / or $ is stripped
The script prints one JSON object and writes files under ~/.claude/cards/<IDENTIFIER>/.
What comes back
{
"identifier": "JAM-56", "title": "...", "url": "https://linear.app/...",
"state": "Next", "project": "Agency", "labels": ["agent-ready"],
"dir": "/Users/you/.claude/cards/JAM-56",
"brief": "/Users/you/.claude/cards/JAM-56/brief.md",
"comments": "/Users/you/.claude/cards/JAM-56/comments.md",
"commentCount": 3,
"attachments": [{ "ok": true, "file": "attachments/spec.pdf", "bytes": 445873, "contentType": "application/pdf" }],
"attachmentsOk": 1, "attachmentsFailed": 0,
"links": [{ "url": "https://github.com/org/repo/pull/12", "title": "Fix the thing", "sourceType": "github" }],
"skill": "ce-plan", "skillSource": "description"
}
attachments are files, downloaded to disk. links are URLs somebody pinned to the card and are never fetched, because the Linear token must not travel to a host Linear does not own. Links are also listed at the foot of the brief. Open one yourself if it bears on the task.
skillSource is argument, description or none. It tells you where the answer came from, so a surprise is visible rather than silent.
Then do this, in order
- Read
brief. Always. It is the work. - Read
commentswhencommentCountis above zero. Detail added after the card was written usually lives there, and it is often the part that changed the requirement. - Look at
attachments. Read the ones that bear on the task. A PDF or an image is evidence, not decoration. An image embedded in the description already points at its local copy, so read it from the path in the brief. Checklinkstoo. A pinned pull request or document often carries the constraint the card assumes you know. - Resolve the skill.
skillis set → say one line naming it and where it came from, then invoke it with the Skill tool, passing the brief path as args.skillis null → decide from the brief. If one of the available skills clearly fits, invoke it. If none does, just do the work.
- Report
attachmentsFailedif it is above zero. Never continue silently past a failed download. Say which one and why.
⛔ Never paste the brief into your reply. It is a file. Name the path and work from it. A long card pasted back is context spent twice.
⛔ Never invoke a skill that is not in the available-skills list. If the card names one that does not exist, say so plainly, then decide from the brief instead. A card can name a skill that was renamed or removed.
Move two — hand the card back
node ~/.claude/skills/card/scripts/card-done.mjs JAM-56 waiting --comment-file /tmp/result.md
node ~/.claude/skills/card/scripts/card-done.mjs JAM-56 done --comment "Landed in #412."
node ~/.claude/skills/card/scripts/card-done.mjs JAM-56 doing
Lanes are inbox, next, doing, waiting, done, cancelled. The lane resolves against the card's own team, so a card on a team you have never touched still works.
Write a real comment. What changed, what to look at, what is still open. A state change with no comment makes the reader open the branch to find out what happened.
For a comment longer than a line, write it to a file and use --comment-file. Shell quoting mangles markdown.
Where the skill name comes from
Three layers, first match wins.
- You passed one.
card.mjs JAM-56 ce-plan. Highest precedence, because you know more at execution time than the card did at planning time. - Line one of the description is a bare
/nameor$name. The script strips that line from the brief, so the work never sees the command. - Neither.
skillis null and you decide from the brief.
Failure modes
| What you see | What it means | What to do |
|---|---|---|
no Linear token available |
Neither auth route resolved | See Authentication above. If using 1Password, check the CLI is authenticated and will not block on a prompt. |
is not a card identifier |
Malformed input | Expects TEAM-123. Ask which card. |
no issue X is visible to this token |
Wrong identifier, or no access to that team | Confirm the identifier before assuming a permissions problem. |
attachment(s) failed to download |
Usually a 401, meaning a stale token | Report it. Do not pretend the evidence was read. |
has no state named X |
That team uses different lane names | The error prints the real names. Pick one. |
larger than the ... byte cap |
An attachment over 64MB | It was not downloaded. Say so, and open it in Linear if it matters. |
Verify before reporting done
- The brief file exists and is not empty.
- Every attachment in the manifest has
ok: true, or you have said which did not and why. - If you invoked a skill, you named it and its source in one line first.
- If you moved the card, the JSON came back with
moved: true.