# Card

> Pull a Linear issue into the current thread as a brief on disk, with its comments and attachments, then hand off to whichever skill the card names. Also moves the card and posts a result comment when the work is finished. Use when the user gives a card identifier such as JAM-56, or says "work on JAM-56", "load that card", "pull up the issue", "start JAM-56", or when finishing and they say "close the card", "mark it done", "hand it back". Not for creating, triaging or planning issues.

- Skill: `jtksystems/card` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add jtksystems/card`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jtksystems/card/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: jtksystems (https://skillmd.com/u/jtksystems)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/jtksystems/card

---


# 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.

1. `LINEAR_API_KEY` in the environment.
2. `op read` against `LINEAR_TOKEN_OP_REF`, if the 1Password CLI is installed. Defaults to `op://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

```bash
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

```json
{
  "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

1. **Read `brief`.** Always. It is the work.
2. **Read `comments` when `commentCount` is above zero.** Detail added after the card was written usually lives there, and it is often the part that changed the requirement.
3. **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.
   Check `links` too. A pinned pull request or document often carries the constraint the card assumes you know.
4. **Resolve the skill.**
   - `skill` is set → say one line naming it and where it came from, then invoke it with the Skill tool, passing the brief path as args.
   - `skill` is null → decide from the brief. If one of the available skills clearly fits, invoke it. If none does, just do the work.
5. **Report `attachmentsFailed` if 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

```bash
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.

1. **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.
2. **Line one of the description is a bare `/name` or `$name`.** The script strips that line from the brief, so the work never sees the command.
3. **Neither.** `skill` is 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`.

