# Linear Manager

> Manage Linear tickets/comments via direct HTTP GraphQL with `python3 scripts/linear_manager.py` and token env vars only (no Linear MCP). Supports create/read/update/delete tickets and sub-tickets, issue templates, discovery commands (whoami, teams, projects), creating/moving tickets under a project, workflow status changes, team reassignment, cycle/label updates, and comment read/create/thread reply.

- Skill: `snvtac/linear-manager` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add snvtac/linear-manager`
- Raw SKILL.md: https://api.skillmd.com/api/skills/snvtac/linear-manager/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Product & Planning
- Author: snvtac (https://skillmd.com/u/snvtac)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/snvtac/linear-manager

---


# Linear Manager

## Overview
Use this skill for Linear operations through `https://api.linear.app/graphql` only.
Execution entrypoint is `python3 scripts/linear_manager.py`.

## Capability Scope
- Read:
`list`, `get`, `templates`, `states`, `children`, `comments`
- Discovery (for an unfamiliar/multi workspace):
`workspaces` (all token env vars → workspace names), `whoami` (current user + workspace), `teams` (id/key/name), `projects` (id/name + teams)
- Write:
`create`, `update`, `delete`, `comment`
- Sub-ticket:
`create --parent <ISSUE_REF>`
- Template:
`templates`, `create [--template-id|--template-name|--use-default-template]`
- Project:
`projects [--team-key <KEY>|--team-id <UUID>] [--name <SUBSTR>]`
`create --project-id <UUID>` or `create --project-name <NAME>`
`update --project-id <UUID>` or `update --project-name <NAME>`
- Team management:
`update --team-key <KEY>` or `update --team-id <UUID>`
- Status management:
`update --state <name>` or `update --state-id <uuid>`
- Cycle/label management:
`update --cycle-id|--cycle-name|--cycle-number`
`update --set-labels|--add-labels|--remove-labels`
`update --set-label-ids|--add-label-ids|--remove-label-ids`

## Hard Constraints
- Always operate via HTTP script: `python3 scripts/linear_manager.py`.
- Never use Linear MCP tools (`mcp__linear__*`) in this skill.
- If token is missing or HTTP fails, return an error with short suggestions only.
- Do not fallback to Linear MCP under any failure mode.

## Token Policy
Default env var is `LINEAR_API_TOKEN`. Select a non-default token with `--token-env <ENV_NAME>`.

Never print or echo a token value (do not run `echo "$LINEAR_API_TOKEN"`). To verify a token, resolve its workspace with `whoami`/`workspaces` instead — those never expose the token string.

If the chosen token env var is missing, stop and provide suggestions only:
```bash
export LINEAR_API_TOKEN='lin_api_xxx'
```

## Multi-Workspace Policy (Pick One, Then Lock It)
The environment may define MORE THAN ONE Linear token, because the user works across multiple workspaces (e.g. two `lin_api_*` tokens in `.bashrc`). Treat the active workspace as explicit per-session state:

1. **Discover.** Run `workspaces` to list every Linear token env var and the workspace it maps to. Token values are never printed — only env var names, the viewer, and `organization.name`/`urlKey`.
   ```bash
   python3 scripts/linear_manager.py --pretty workspaces
   ```
2. **Ask the user to choose — by workspace name.** If more than one workspace is available and the user has not already named one this session, STOP before any operation and present the choice as selectable options whose labels are the `organization.name` values from `workspaces` (e.g. "Final Round AI", "Dark Legion"). Never guess or default silently.
3. **Lock for the session, with a hard guard.** Once the user picks (or only one token exists), record the corresponding `--token-env` AND the chosen workspace name. On EVERY subsequent command this session, pass **both** `--token-env <ENV>` and `--expect-workspace <NAME>`. The guard runs `whoami` before the command and refuses to run (exit code `3`) if the token does not resolve to that workspace — so a forgotten or wrong `--token-env` can never silently hit another workspace. Do not switch workspaces unless the user explicitly asks.
   ```bash
   python3 scripts/linear_manager.py \
     --token-env PERSON_LINEAR_API_TOKEN \
     --expect-workspace "Dark Legion" \
     --show-workspace --pretty list --team-key ENG --limit 5
   ```
4. **Report the workspace on every operation.** State which workspace each result ran against. Output is labeled with `_tokenEnv`, and `--show-workspace` adds the resolved `_workspace` (org name) so reports are never ambiguous.

If the user later names a different workspace explicitly, switch to its token env var (and matching `--expect-workspace`) and re-confirm by reporting the new workspace name.

> Cross-session note: this lock lives only in the current session. A new `claude` session starts with no memory of the choice and MUST re-run step 1–2. The selection is intentionally never persisted, so workspaces cannot leak across sessions.

## Write Safety Policy (Default Dry-Run)
`create`, `update`, `delete`, `comment` are dry-run by default.
To perform real write, caller must pass `--execute`.

Delete has an extra safety requirement:
1. Run dry-run first to inspect the resolved target and expected confirmation value.
2. Re-run with both `--execute` and `--confirm-delete <IDENTIFIER>` after the user explicitly agrees.

Recommended two-step workflow:
1. Run dry-run first and inspect payload.
2. Run the same command with `--execute` to apply.

Example:
```bash
python3 scripts/linear_manager.py --pretty update --id ENG-123 --state "In Progress"
python3 scripts/linear_manager.py --pretty update --id ENG-123 --state "In Progress" --execute
```

## Command Reference
Run from skill folder:

```bash
python3 scripts/linear_manager.py --pretty list --limit 20
```

```bash
python3 scripts/linear_manager.py --pretty templates --team-key ENG
```

```bash
python3 scripts/linear_manager.py --pretty templates \
  --team-key ENG \
  --name "Issue Templates 2025"
```

```bash
python3 scripts/linear_manager.py --pretty workspaces
```

```bash
python3 scripts/linear_manager.py --pretty whoami
```

```bash
python3 scripts/linear_manager.py --pretty teams --limit 100
```

```bash
python3 scripts/linear_manager.py --pretty teams --name core
```

```bash
python3 scripts/linear_manager.py --pretty projects --limit 50
```

```bash
python3 scripts/linear_manager.py --pretty projects --team-key ENG --limit 50
```

```bash
python3 scripts/linear_manager.py --pretty projects --team-key ENG --name "platform"
```

```bash
python3 scripts/linear_manager.py --pretty get --id ENG-123 --include-children --comments-limit 20
```

```bash
python3 scripts/linear_manager.py --pretty create \
  --team-key ENG \
  --title "Issue title"
```

```bash
python3 scripts/linear_manager.py --pretty create \
  --team-key ENG \
  --title "Issue title" \
  --description "Issue description" \
  --execute
```

```bash
python3 scripts/linear_manager.py --pretty create \
  --team-key ENG \
  --title "Issue title" \
  --template-name "Issue Templates 2025"
```

```bash
python3 scripts/linear_manager.py --pretty create \
  --team-key ENG \
  --title "Issue title" \
  --template-id <TEMPLATE_UUID> \
  --execute
```

```bash
python3 scripts/linear_manager.py --pretty create \
  --team-key ENG \
  --title "Issue title" \
  --use-default-template
```

```bash
python3 scripts/linear_manager.py --pretty create \
  --team-key ENG \
  --title "Issue title" \
  --project-name "Platform Foundation 2026"
```

```bash
python3 scripts/linear_manager.py --pretty create \
  --team-key ENG \
  --title "Issue title" \
  --project-id <PROJECT_UUID> \
  --execute
```

```bash
# --team-key/--team-id is optional when the project resolves to a single team; the team
# is derived (output then includes derivedTeam + a warning to verify it).
python3 scripts/linear_manager.py --pretty create \
  --title "Issue title" \
  --project-name "Platform Foundation 2026"
```

```bash
python3 scripts/linear_manager.py --pretty create \
  --team-key ENG \
  --title "Sub-ticket title" \
  --parent ENG-123 \
  --execute
```

```bash
python3 scripts/linear_manager.py --pretty update \
  --id ENG-123 \
  --cycle-name "Cycle 25" \
  --add-labels "TEST,bug bash"
```

```bash
python3 scripts/linear_manager.py --pretty update \
  --id ENG-123 \
  --cycle-name "Cycle 25" \
  --add-labels "TEST,bug bash" \
  --execute
```

```bash
python3 scripts/linear_manager.py --pretty update \
  --id ENG-123 \
  --description-file /tmp/issue-desc.md \
  --execute
```

```bash
python3 scripts/linear_manager.py --pretty update \
  --id ENG-123 \
  --team-key DEVOPS
```

```bash
python3 scripts/linear_manager.py --pretty update \
  --id ENG-123 \
  --team-key DEVOPS \
  --execute
```

```bash
python3 scripts/linear_manager.py --pretty update \
  --id ENG-123 \
  --project-name "Platform Foundation 2026" \
  --execute
```

```bash
python3 scripts/linear_manager.py --pretty states --team-key ENG
```

```bash
python3 scripts/linear_manager.py --pretty children --id ENG-123 --limit 50
```

```bash
python3 scripts/linear_manager.py --pretty comments --id ENG-123 --limit 20
```

```bash
python3 scripts/linear_manager.py --pretty comment \
  --id ENG-123 \
  --body "Progress update"
```

```bash
python3 scripts/linear_manager.py --pretty comment \
  --id ENG-123 \
  --body "Progress update" \
  --execute
```

```bash
python3 scripts/linear_manager.py --pretty comment \
  --id ENG-123 \
  --parent-comment-id <COMMENT_UUID> \
  --body "Thread reply" \
  --execute
```

```bash
python3 scripts/linear_manager.py --pretty delete --id ENG-123
```

```bash
python3 scripts/linear_manager.py --pretty delete \
  --id ENG-123 \
  --confirm-delete ENG-123 \
  --execute
```

## Output And Exit Codes
- Exit code `0`:
success
- Exit code `1`:
validation/API error
- Exit code `2`:
missing token env var
- Exit code `3`:
`--expect-workspace` guard mismatch (token resolves to a different workspace)

## Operational Notes
- Prefer `--pretty` for readable JSON.
- `--id` and `--parent` accept both UUID and identifier (`TEAM-123`).
- `templates` is read-only. It lists issue templates by default and can filter by team/name.
- Template creation is optional. If no template flag is passed to `create`, the command keeps the original custom-content behavior.
- `create --template-name` uses a case-insensitive exact name match and must resolve to one available issue template.
- `create --title` is still required, so template title values are always overridden by the CLI title.
- CLI-provided create fields override matching template fields; fields not provided by the CLI are left for Linear to fill from the template.
- Form-field templates are not filled by this CLI version. Dry-run warns for explicit `--template-id`/`--template-name` selections when `hasFormFields=true`.
- `delete --execute` requires `--confirm-delete` to exactly match the resolved issue identifier.
- For an unfamiliar or multi-token environment, start with `workspaces` (pick the workspace), then `whoami` (confirm user + workspace), then `teams` (find team keys), then `projects` (find project ids/names).
- `workspaces` scans all candidate token env vars and does not require `--token-env` to be set; it never prints token values. Detection is by token VALUE prefix (`lin_api_`/`lin_oauth_`) only — never by env var name — so an unrelated secret is never sent to Linear. If a real token uses an unrecognized format, select it explicitly with `--token-env`.
- Output is labeled with `_tokenEnv`; add `--show-workspace` to also include `_workspace` (resolved org name) at one extra API call.
- `--expect-workspace <name|urlKey>` is a hard cross-workspace guard: it runs `whoami` first and refuses (exit `3`) unless the token resolves to that workspace. It applies to all commands except `workspaces`, and reuses the same `whoami` call as `--show-workspace`. In a multi-token environment, pass it on every command alongside `--token-env`. An empty value (e.g. `--expect-workspace "$UNSET"`) also refuses (exit `3`) rather than silently skipping.
- `whoami` and `teams` are read-only and need no arguments beyond optional `teams --name`/`--limit`.
- `projects` lists workspace projects by default; `--team-key|--team-id` lists projects accessible to that team. It is read-only.
- `projects --name` is a case-insensitive substring filter applied to the fetched page (`--limit`). `--limit` is capped at 250 (Linear's per-query complexity ceiling); to find a specific project, prefer `--name`, scope with `--team-key/--team-id`, or resolve it directly via `create/update --project-name`.
- `create`/`update` accept `--project-id` (UUID) or `--project-name`. `--project-name` is a case-insensitive exact match scoped to the issue's team and must resolve to exactly one project.
- `--project-id` is validated to be accessible to the target team; a project scoped to other teams (or with no team) is rejected before the mutation.
- `create` accepts a team via `--team-key`/`--team-id`, OR derives the team from `--project-id`/`--project-name` when that project belongs to exactly one team. If the project spans multiple teams (or none), pass `--team-key`/`--team-id` explicitly. When the team is derived, the output includes `derivedTeam` and a `warnings` entry — verify it is the intended team before relying on a real create.
- `update --team-key|--team-id` moves an issue to another team.
- Do not combine a cross-team move with `--state`, `--cycle-*`, or label mutation flags; move first, then apply destination-team workflow fields in a second command.
- For large markdown, use `--description-file` or `--body-file`.
- Use `--body-stdin` for piping comment content.

## Validation
Ensure no MCP call is introduced in executable script:
```bash
rg -n "mcp__linear__" skills/linear-manager/scripts/linear_manager.py
```
Expected result: no matches (exit code `1`).

## Resources
- Script entrypoint: `scripts/linear_manager.py`
- Test cases: `scripts/linear_manager_test_cases.md`
- GraphQL reference: `references/graphql-operations.md`

