$ backgroundclaude
blog · 2026-04-13 · 8 min read

Linear CLI + claude -p: the poor-man's background agent in 30 lines of shell

If you want a Claude Code agent triggered by Linear issues, you have two paths. The full one is a hosted agent with isolated worktrees, mid-run approvals, and streaming events back to the issue — this is the shape Cyrus builds. The fast one is a 30-line shell loop that polls Linear, pipes the issue body into claude -p, and posts the result back as a comment. It works. It's the pattern every team tries first. Here's how to build it, where it breaks, and when you graduate out of it.

The Linear CLI you want

The community CLI worth installing is @schpet/linear-cli. It's git-aware, shell-friendly, and every command you need for the agent loop is a one-liner.

# Install (pick one)
brew install schpet/tap/linear
npm install -g @schpet/linear-cli
deno install -A --reload -f -g -n linear jsr:@schpet/linear-cli

# Auth + per-repo config
linear auth login
cd my-project-repo
linear config

Create an API key at linear.app/settings/account/security. You need workspace member access to create one.

The three commands the agent loop uses

You only need three Linear CLI commands to build the loop:

# 1. List your unstarted issues (assigned to you, not yet in progress)
linear issue mine

# 2. View a specific issue's full body + metadata
linear issue view ABC-123

# 3. Post a comment on the current branch's issue
linear issue comment add

Bonus: linear issue start ABC-123transitions an issue to “In Progress” and checks out the right branch. Useful inside the loop when you want to claim the issue before working it.

The 30-line shell loop

This is the minimal thing that actually works. It polls for new issues, runs Claude Code headless against each one, posts the result as a Linear comment, and transitions the issue.

#!/usr/bin/env bash
# background-claude.sh — poor-man's Linear agent
set -euo pipefail

TEAM=${LINEAR_TEAM_ID:-ENG}
POLL_INTERVAL=60       # seconds between polls
MAX_BUDGET=3           # USD per issue
MAX_TURNS=10

while true; do
  # 1. Ask Linear for your queued issues (JSON)
  issues=$(linear issue mine --format json 2>/dev/null || echo "[]")

  # 2. Pick the first with label:agent-ready
  ident=$(echo "$issues" \
    | jq -r '.[] | select(.labels[]?.name == "agent-ready") | .identifier' \
    | head -n1)

  if [ -n "$ident" ]; then
    echo "[$(date -u +%FT%TZ)] picking up $ident"

    # 3. Pull the full body
    body=$(linear issue view "$ident" --format json | jq -r '.description')

    # 4. Transition + checkout
    linear issue start "$ident"

    # 5. Hand to claude -p headless
    result=$(claude -p "$body" \
      --output-format text \
      --permission-mode dontAsk \
      --max-turns "$MAX_TURNS" \
      --max-budget-usd "$MAX_BUDGET")

    # 6. Post the result back
    echo "$result" | linear issue comment add --stdin

    # 7. Never re-pick this issue
    linear issue update "$ident" --remove-label agent-ready
  fi

  sleep "$POLL_INTERVAL"
done

Thirty lines. Assumes you have jq, linear, and claudeon PATH, and you're authenticated to all three. The agent-ready label is what turns “any issue assigned to me” into “any issue I want the agent to handle.” Move the label on, it runs. Move it off, it doesn't.

Why this works at all

Three things make the loop viable without extra infrastructure:

Four things the shell loop breaks on

I have run something close to this pattern. You should know exactly where it falls over before you rely on it for anything real.

1. Concurrent issues fight over the working directory

The moment two issues arrive within POLL_INTERVAL seconds of each other, both invocations of Claude will checkout different branches in the same repo, and the second one will either fail or silently destroy the first one's work. The fix is git worktree isolation — one worktree per issue — which adds another 50 lines of bash and a directory layout convention you now have to maintain.

2. No live feedback while the agent runs

The user opened the Linear issue, added the agent-readylabel, and walked away. The agent thinks for 6 minutes, posts one enormous comment with the result, done. If Claude hits a mid-run question (“this is ambiguous, should I A or B?”), either you wired permission modes to auto-accept everything (risky), or it blocks forever. There is no streaming status back to Linear. This is fine for demos, but unacceptable for real work where humans want to course correct.

The right answer is stream-json output format plus a bridge that converts events into Linear comments or reactions in near-real-time. That's another 100 lines of code and a persistent service.

3. The polling loop is the wrong primitive

sleep 60in a while loop means: up to 60 seconds of latency between the label change and the agent picking up, the process holds a shell open (restart on reboot is your problem), and you're hitting Linear's API every minute whether there's work or not. Linear has webhooks specifically to avoid this. Wiring webhooks means: public endpoint, HMAC verification, a queue, and a worker. You're four services deep now.

4. There is no audit trail

Where did the agent run? What version of Claude, what model, what budget, what tools did it invoke, what files did it change, what was the final cost? On the shell loop, the answer is “my laptop” and “no idea.” For anything security-sensitive or team-shared, this is a non-starter. You'd need a structured log sink, cost tracking per-run, and a way to go back and replay a specific invocation. That's another day of work.

When the shell loop is the right answer

When you graduate out

That list is the exact shape of a real background agent, and it's what Cyrus is — a long-running service that subscribes to Linear webhooks, runs Claude Code (or Codex, Cursor, Gemini) in per-issue git worktrees, streams events back into the issue, handles mid-run approvals, and gives you a real audit trail. The shell loop is a great first step. Cyrus is what day two looks like.

Takeaways

graduate the shell loop

Webhooks. Worktrees. Audit. Mid-run approvals.

Cyrus runs Claude Code (or Codex, Cursor, Gemini) in isolated git worktrees per Linear issue, streams agent activity back to the issue as it happens, supports mid-run approvals, and gives you a real audit trail. Community self-hosted is free forever.

Try Cyrus free →