Integrations

Connecting Linear to agents on your own machines

4 min read

Linear is the tracker DevPilot has wired today. Assign an issue, and a coding agent picks it up on a machine you own — not in our cloud, not in a container we operate. This page describes the path that issue actually takes, because the interesting parts are the failure modes rather than the setup.

Jira and GitLab are planned and not built. We would rather say so than list them next to Linear as though they were equivalent.

The path

Linear issue assigned
   → POST /api/webhooks/linear   (signed)
   → verify HMAC over the raw body
   → resolve workspace → team config → repo route → orchestrator
   → INSERT session + queue row   (one transaction)
   → your machine claims the row
   → agent runs locally
   → status and result written back to Linear

What crosses the network to us is the issue identifier, title, description, team, priority, labels and assignee. What does not cross is your repository, your git credentials, or your working tree. The agent runs where the code already is.

The signature is the authentication

The webhook endpoint is public because it has to be — Linear does not send a bearer token. The signature is the only thing standing between an anonymous POST and a dispatch, so it is mandatory:

const signature = request.headers.get('linear-signature')
// ...
if (!signature) return err('unauthenticated', 'Missing linear-signature header.', 401)
if (!workspace.verifySignature(raw, signature)) { /* 401 */ }

An earlier version read:

if (signature && workspace.webhookSecret) { /* verify */ }

which meant omitting the header skipped verification entirely. Anyone who learned a Linear organization id — not a secret; it appears in payloads — could forge a dispatch into somebody else's fleet. A missing header and a missing stored secret are each a 401 now, and there is no path that writes without verifying.

Two details that matter if you implement this yourself:

Verify over the raw bytes. The handler reads await request.text() before anything else and verifies against that exact string. Parsing to JSON and re-serializing produces different bytes — key order, whitespace, unicode escaping — and the signature will not match. The comment in the file says "raw body first" for this reason.

Reject stale timestamps. Webhooks older than sixty seconds are refused, which blunts replay of a captured request. A valid signature stays valid forever; the timestamp is what stops it being reusable forever.

The secret itself is stored encrypted with AES-256-GCM and never leaves the module that owns it — the verification runs behind a closure, so calling code can ask does this match and cannot ask what is the secret. That design is described here.

Routing, and what happens when it fails

Once verified, the handler resolves four things in order: the workspace from the Linear organization id, the team config, a repo route, and an orchestrator to send it to. Any of them missing is a legitimate no-op rather than an error — an issue in a team you have not configured should be ignored quietly, not retried forever.

This is the part most people underestimate. A webhook endpoint that treats "nothing to do" as a failure will generate an alarming amount of noise from the 90% of issues that were never meant for it.

The write is one transaction

The session row and the queue row are inserted together:

const result = await db.transaction(async (tx) => {
  // ...
  const { queueId, messageId } = await enqueueDispatch(tx, { ... })
})

enqueueDispatch requires the transaction handle as its first parameter. It is not optional and there is no overload without it, which makes "session exists but nothing was dispatched" impossible to write rather than merely discouraged. That window is the entire reason the design looks like this, and it is the longer story.

If your machine is offline

The work waits. The queue row is durable and unclaimed; your orchestrator claims it when it reconnects. Nothing is lost and nothing is retried into a void — see dispatch queue for what the row actually guarantees.

Setting it up

  1. Connect Linear from Settings → Linear in the portal. This stores the workspace and its webhook secret, encrypted.
  2. Map a team to a repository under repo routes.
  3. Register a machine: devpilot bridge connect.
  4. Assign an issue to the bot user.

Known limits

  • Linear only. Jira and GitLab are planned. The bridge protocol is published under MIT, so a different tracker is a connector rather than a rewrite.
  • Write-back has never run against a real workspace. The sync that posts status and pull request links back to Linear is unit-tested against a stub; no live Linear workspace has been connected end to end. It is the least proven part of this path and we would rather flag it than let you discover it.