Bridges Linear and Anthropic Managed Agents Sessions.
Assign a Linear issue to Claude, and this bridge spins up a managed agent session with the issue context. The session persists across replies — when you reply in the Linear thread, the bridge feeds your message back into the same Claude session, with full conversation history. When the session goes idle, Claude's final response is posted as a response activity in the Linear agent sidebar.
I built this because I wanted to hand off Linear tickets to Claude without leaving Linear or copy-pasting context.
- You assign a Linear issue to the agent user.
- The bridge creates a Managed Agents session bound to your pre-configured agent + environment.
- The bridge sends the issue context (title, description, prior thread) to Claude as the first user message.
- Claude works inside its managed cloud container. The bridge consumes the session's event stream but does not relay intermediate agent messages to Linear — the sidebar stays quiet during the turn.
- When the session goes idle (
stop_reason: end_turn), the bridge posts Claude's full final response as oneresponseactivity in the Linear sidebar. - If you reply in the Linear agent thread, the bridge resumes the same Claude session with your reply — Claude already has the full prior history.
Earlier versions of this bridge fired pre-configured Claude Code Routines for each interaction. Routines is fire-and-forget: every Linear reply spawned a fresh Claude session that had to re-read prior context, and there was no way to know when Claude was finished.
Switching to the Managed Agents Sessions API fixes both:
- Sessions persist by ID. Replies feed into the same conversation. No re-reading prior context.
session.status_idleis a real completion signal. The bridge posts Claude's final answer back to Linear when the turn ends.- The bridge holds zero per-session state. The Claude session ID is stored on the Linear agent session itself (in
externalUrls) and read back on follow-ups, so the bridge survives restarts mid-conversation.
Five steps: register a Linear OAuth app, get an Anthropic API key, create the agent and environment via the Anthropic API, then fill in .env and run.
Go to linear.app/settings/api/applications → Create new.
- Actor:
App user(this creates a dedicated agent user — don't pick "User") - Scopes:
read,write,app:assignable - Redirect URI:
<BASE_URL>/oauth/callback - Webhook URL:
<BASE_URL>/webhook - Webhook events: check
Agent session events
Copy the Client ID, Client secret, and Webhook signing secret.
Create one at console.anthropic.com/settings/keys. The key needs Managed Agents access (the managed-agents-2026-04-01 beta).
Run these one-shot curls (substitute $ANTHROPIC_API_KEY):
# Agent — defines model, system prompt, tools, MCP servers
curl -X POST https://api.anthropic.com/v1/agents \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-d '{
"name": "linear-bridge",
"model": "claude-opus-4-7",
"system": "You were invoked from a Linear issue. Keep your own session output focused on the work; the user is reading your final response in the Linear agent sidebar.",
"tools": [{"type": "agent_toolset_20260401"}]
}'
# Environment — defines the cloud container Claude runs inside
curl -X POST https://api.anthropic.com/v1/environments \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-d '{
"name": "linear-bridge",
"config": { "type": "cloud", "networking": {"type": "unrestricted"} }
}'Copy the returned id from each — they look like agent_… and env_….
If you want Claude to operate on a GitHub repo, attach an MCP server (e.g. the GitHub MCP) at the agent level when you create it, or update the agent later to add it. See the agent setup docs.
cp .env.example .envFill in:
| Variable | What it is |
|---|---|
LINEAR_CLIENT_ID |
From your Linear OAuth app |
LINEAR_CLIENT_SECRET |
From your Linear OAuth app |
LINEAR_WEBHOOK_SECRET |
The webhook signing secret from Linear |
ANTHROPIC_API_KEY |
Anthropic API key with Managed Agents access |
CLAUDE_AGENT_ID |
The agent_… ID from step 3 |
CLAUDE_ENVIRONMENT_ID |
The env_… ID from step 3 |
BASE_URL |
Public HTTPS URL where this bridge is reachable |
npm install
npm run devThen visit <BASE_URL>/oauth/authorize once and approve the app. You should see Installed for workspace: <name> in the server logs.
Assign any issue to the agent user in Linear. Within a few seconds you should see a thought activity appear in the Linear issue's agent sidebar, then a response activity once Claude finishes.
Linear needs to reach the bridge over HTTPS. Pick one:
Local + cloudflared (easiest for testing, no signup):
brew install cloudflared
cloudflared tunnel --url http://localhost:3001Use the printed https://...trycloudflare.com URL as BASE_URL.
Local + ngrok (also fine):
brew install ngrok
ngrok config add-authtoken <your-token>
ngrok http 3001A hosting platform (Fly, Railway, Render, etc.): set the env vars as secrets, and use the platform's HTTPS URL as BASE_URL. There's no database or queue — just the one Node process.
Kubernetes via Helm (charts/linear-claude-bridge/):
CI publishes a multi-arch image to ghcr.io/northisup/linear-claude-bridge on every push to main (tags: latest, sha-<short>, and the SemVer pieces on v* tags). Pull it directly, or build your own from the Dockerfile.
# Optional: build and push your own image instead of using the published one.
# docker build -t ghcr.io/<you>/linear-claude-bridge:0.2.0 .
# docker push ghcr.io/<you>/linear-claude-bridge:0.2.0
# Install. Either set secrets inline or point at an externally-managed Secret.
helm install linear-bridge charts/linear-claude-bridge \
--set image.repository=ghcr.io/northisup/linear-claude-bridge \
--set image.tag=latest \
--set baseUrl=https://bridge.example.com \
--set claude.agentId=agent_… \
--set claude.environmentId=env_… \
--set secrets.linearClientId=<…> \
--set secrets.linearClientSecret=<…> \
--set secrets.linearWebhookSecret=<…> \
--set secrets.anthropicApiKey=<…> \
--set ingress.enabled=true \
--set ingress.host=bridge.example.com \
--set ingress.tls.enabled=true \
--set ingress.tls.secretName=bridge-tlsFor production, pin to a sha-<short> tag instead of latest so rollbacks and rolling updates are deterministic.
The chart pins to replicas: 1 with a Recreate strategy — the bridge holds the Linear OAuth token in process memory and cannot be scaled horizontally. Per-Linear-session state is stored on Linear (in agent session externalUrls), so the bridge survives restarts mid-conversation, but the OAuth install must be re-done after every pod replacement (visit <baseUrl>/oauth/authorize again). See charts/linear-claude-bridge/values.yaml for the full set of knobs (resources, probes, security context, existingSecret, etc.).
Heads up: free-tier tunnel URLs change when you restart them. If the URL changes, update both
BASE_URLin.envand the Redirect URI + Webhook URL in your Linear app settings.
1. No live progress visibility.
The Sessions API doesn't return a browser-facing session URL today, so there's no link to click and watch Claude work in real time. The bridge also doesn't relay intermediate agent.message events to Linear in v1 — the sidebar shows a thought when the turn starts, then stays quiet until Claude is done, then posts a single response activity with the final answer. The synthetic https://platform.claude.com/sessions/<id> URL stored on the Linear session is for ID round-tripping only; it may not render a viewer.
2. Long-running turns hold an SSE stream open.
The bridge keeps the session's stream open until session.status_idle. There's a 30-minute hard timeout per turn. A flood of concurrent issues could pile up open streams in the single Node process — this bridge is sized for one human user, not a team-wide deployment.
3. Restarts wipe the OAuth token.
The Linear access token lives in memory. If the server restarts, you have to visit /oauth/authorize again. Per-session state is not affected: Claude session IDs are stored on Linear, not in the bridge. For local dev, set DEV_PERSIST_TOKEN=1 to cache the token in a gitignored file.
4. Protect the webhook secret.
Anyone with LINEAR_WEBHOOK_SECRET can fake Linear events and burn your Anthropic credits. Treat it like the Anthropic API key.
5. Tool confirmation isn't supported.
If your agent uses tools with permission_policy: always_ask, the session will hang at session.status_idle with stop_reason: requires_action and the bridge won't auto-confirm. Configure your agent with always_allow for v1 use.
Upfront: I'm an amateur developer. This project works for me but it's very likely to have bugs, rough edges, and things a more experienced engineer would do differently. If you spot something broken or see a better way to do it, please open an issue or PR — I'd genuinely welcome the feedback.
git clone <this-repo>
cd claude-linear-agent
npm install
cp .env.example .env
# fill in the seven values from the Setup section above
npm run devnpm run dev uses tsx watch so it reloads on file changes. In another terminal, start a tunnel so Linear can reach your local server:
cloudflared tunnel --url http://localhost:3001Paste the https://...trycloudflare.com URL into BASE_URL in .env, and into the Redirect URI and Webhook URL fields in your Linear app settings. Then visit <BASE_URL>/oauth/authorize once to install.
From there, assigning a Linear issue to the agent user should trigger the whole flow end-to-end.
DEV_PERSIST_TOKEN=1— caches the OAuth token to.token-dev.jsonsotsx watchreloads don't wipe it. Local only.DEBUG_PAYLOAD=1— logs the first 6 KB of each Linear webhook payload. Handy for figuring out what Linear is actually sending.
Releases are version-driven. To cut one:
- Bump
versioninpackage.jsonandappVersionincharts/linear-claude-bridge/Chart.yamlto the same value (e.g. both to0.3.2). The chart's image-tag default isappVersion, so they have to stay in lockstep —release.ymlwill refuse to tag if they don't match. - Commit and push to
main. - Two workflows fire in parallel:
docker.ymlbuilds the multi-arch image and pushessha-<short>+latesttoghcr.io/northisup/linear-claude-bridge.release.ymltype-checks, creates thev<version>git tag, waits forsha-<short>to appear in ghcr, then re-tags it with<version>and<major>.<minor>viadocker buildx imagetools create(rewrites the registry manifest, no rebuild). Finally it creates a GitHub Release with auto-generated notes plus a footer linking the published images.
The retag step takes seconds because no layers are transferred — the SemVer tags point at the exact same multi-arch manifest as the sha- tag from the same commit. End-to-end, a release lands ~6–8 min after the push (dominated by the parallel docker.yml build).
If docker.yml fails on a release commit, release.yml times out after 12 min waiting for the sha image and exits non-zero. Fix the docker build, push another commit (or re-run via workflow_dispatch), and the release picks up.
MIT. See LICENSE.