feat(plugins): add linear-triage plugin#15
Conversation
Distributable Claude Code plugin that polls the Linear triage queue for issues labeled "Claude Code" and implements each one end-to-end (worktree, draft PR, tests, status updates). Bundles three skills: linear-triage-setup, linear-triage-poller, linear-issue-worker. Built on the Symphony pattern (https://github.com/openai/symphony): tracker-as-queue + isolated worktree per issue + agent-per-issue.
There was a problem hiding this comment.
Left some comments. Two high-severity issues need attention before merging: a race condition in issue claiming that will cause duplicate work under concurrent poller runs, and the Bash allow-all permission in settings.json that enables arbitrary code execution without prompts in unattended mode.
| ### 3a. Claim the issue (prevent double-pickup) | ||
| Call `mcp__claude_ai_Linear__save_issue` with: | ||
| - `id`: the issue identifier (e.g., `"ABC-123"`) | ||
| - `state`: value of `statusMap.inProgress` from config |
There was a problem hiding this comment.
Race condition: claim step does not prevent duplicate pickup across concurrent poller runs
(High) Step 3a moves the issue to In Progress after the poller already fetched the list in Step 2. The query in Step 2 filters by state: "Triage" — if two poller instances run close together (e.g., the 15-minute RemoteTrigger fires while a manual /linear-triage-poller is also running), both will see the same Triage issues and both will claim and spawn workers for them. Linear's API has no compare-and-swap or optimistic locking on state transitions, so the second save_issue simply overwrites the first without error. The result is two worker agents implementing the same issue concurrently, creating duplicate branches, duplicate PRs, and duplicate Linear comments. The fix is to query for issues NOT already in In Progress AND with the label, and to treat a save_issue conflict (state already changed) as a skip signal — but neither safeguard is present.
| "mcp__claude_ai_Linear__list_teams", | ||
| "mcp__claude_ai_Linear__get_user", | ||
| "Bash", | ||
| "Read", |
There was a problem hiding this comment.
Bare Bash in the allow-list grants unattended arbitrary command execution
(Critical) settings.json line 14 allows Bash unconditionally. When the user merges this file into .claude/settings.local.json (the documented setup path), every bash command the worker or poller constructs — including the git clone [repoUrl] and [testCommand] expansions that interpolate values from the Linear issue description — runs without a permission prompt. A malicious or misconfigured Linear issue could inject shell commands through gitBranchName, repoUrl, or testCommand. The worker shells out with these values verbatim (Stage 3 git worktree add, Stage 6 [testCommand] 2>&1). The minimum fix is to restrict the Bash allow pattern to specific safe commands (e.g., Bash(git *), Bash(gh *), Bash(npm test)) rather than a wildcard.
|
|
||
| BRANCH="[gitBranchName from issue]" | ||
| ISSUE_ID="[issue identifier in lowercase, e.g. abc-123]" | ||
|
|
||
| # Create worktree using Linear's branch name | ||
| mkdir -p .worktrees | ||
| git worktree add ".worktrees/${ISSUE_ID}" -b "${BRANCH}" origin/main |
There was a problem hiding this comment.
Worker clones into a user-supplied path without validation, enabling path traversal
(High) Stage 3 of the worker unconditionally clones repoUrl into the repo path from config, and creates worktrees at [repo]/.worktrees/[issue-id] where issue-id comes from the Linear API response. If gitBranchName or the issue identifier contains ../ sequences, the worktree will be created outside the intended directory. There is no sanitization or bounds check on these path components anywhere in the skill.
…lt branch - settings.json: allow Task and Agent so the headless poller can spawn per-issue workers via RemoteTrigger without hitting a permission prompt that no human is around to approve. - worker SKILL: replace hardcoded origin/main with default-branch detection (git symbolic-ref, falling back to git remote show) so the plugin works on repos whose default branch isn't named main. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
plugins/linear-triage/that polls a Linear team's triage queue for issues labeled Claude Code and implements each one end-to-end (worktree → draft PR → tests → ready-for-review), posting stage updates back to the issue as comments.linear-triage-setup,linear-triage-poller,linear-issue-worker) plus a recommendedsettings.jsonpermission allowlist so the poller can run unattended viaRemoteTriggercron.docs/architecture.mddiagram of how the skills coordinate with the Linear MCP server, README setup/troubleshooting/permissions sections, and an example config.Built on the Symphony pattern: tracker-as-queue + isolated worktree per issue + one agent per issue.
Test plan
claude --plugin-dir plugins/linear-triageloads the plugin and exposes all three slash commands/linear-triage-setupcreates the "Claude Code" label in Linear and registers the*/15 * * * *RemoteTrigger/linear-triage-pollerclaims the issue (state → In Progress), posts an intake comment, and spawns a workergitBranchName, opens a draft PR auto-linked to the issue, runstestCommand, and flips the issue to In Review on successneeds-humanand stop without mergingsettings.jsonmerged into.claude/settings.local.json, no permission prompts block the background worker🤖 Generated with Claude Code