Gareth Wilson Gareth Wilson

How to Build Linear Agents with Hookdeck CLI

Published


Linear agents are AI-powered bots that operate as first-class members of your workspace. They get mentioned in comments, assigned to issues, and respond with structured updates — all through the same interface your team already uses. The Agent Interaction SDK gives you the webhook events and API primitives to build them.

The challenge is that agents need a publicly accessible URL to receive webhooks from Linear, which makes local development awkward. You either deploy every change to test it, or you reach for a tunnel.

This guide walks through building a Linear agent from scratch using Hookdeck CLI to bridge that gap, giving you a stable public URL that routes Linear's webhook events to your local server, with full visibility into every payload along the way.

What Linear agents can do

Linear agents aren't limited to a single task. The Agent Interaction SDK supports a range of use cases:

  • Issue triage — Analyze incoming issues and route them to the right team based on content, labels, or custom rules.
  • Code review and bug fixing — Delegate well-defined issues to an agent that reads your codebase and proposes fixes.
  • Research and drafting — Have an agent research a topic and produce a first draft directly in the issue thread.
  • Customer feedback routing — Parse support transcripts and file categorized issues attributed to the right customer.
  • Workflow automation — Respond to assignments with multi-step workflows: fetch data from external APIs, run analyses, and report back.

The key insight is that agents appear in Linear like any other team member. Users interact with them naturally — @mention the agent, assign it an issue, or reply to its output with feedback. No separate UI required.

Prerequisites

Before getting started, you'll need:

  • A Linear workspace where you have admin access
  • Node.js v18 or later
  • The Hookdeck CLI installed (brew install hookdeck/hookdeck/hookdeck on macOS, or npm install hookdeck-cli -g)
  • A Hookdeck account (free tier works fine)
  • An API key from your LLM provider (OpenAI, Anthropic, etc.)

Step 1: Create a Linear OAuth application

Linear agents authenticate via OAuth with a special actor=app parameter that creates an app-level identity rather than impersonating a user. This is what makes your agent appear as its own entity in the workspace.

Head to Linear Settings > API > Applications and create a new application. You'll need to configure:

  • Name and icon — This is how the agent appears in the workspace, so pick something descriptive. "Triage Bot" beats "My App."
  • Callback URL — Your OAuth redirect. For local development, http://localhost:3000/callback works.
  • Webhooks — Enable webhooks and check Agent session events at the bottom. This is the event category that notifies your server when the agent is mentioned or assigned an issue.
  • Scopes — At minimum, request read, write, app:assignable, and app:mentionable. The app: scopes are what allow users to @mention and delegate issues to your agent.

Save the application and note your Client ID, Client Secret, and Webhook Signing Secret. You'll need all three.

Authorize the app in your workspace

Build an OAuth authorization URL with actor=app:

https://linear.app/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=http://localhost:3000/callback&response_type=code&scope=read,write,app:assignable,app:mentionable&actor=app

Visit this URL, authorize the app for your workspace, and exchange the returned code for an access token using Linear's OAuth token endpoint. Store this token — it's how your agent authenticates API calls.

The actor=app parameter requires workspace admin permissions. If you're not an admin, you'll need one to authorize the app.

Step 2: Start Hookdeck CLI

Your agent needs a public URL for Linear to send webhooks to. Hookdeck CLI creates a persistent tunnel from a stable public endpoint to your local server.

Log in to Hookdeck CLI first:

hookdeck login

Then start listening on the port your server will run on:

hookdeck listen 3000 linear-agent --path /webhook/linear

This does three things:

  1. Creates a source called linear-agent with a stable public URL (something like https://hkdk.events/abc123def456)
  2. Routes incoming events to http://localhost:3000/webhook/linear
  3. Opens an interactive terminal showing every webhook as it arrives

Copy the source URL — you'll paste it into Linear's webhook configuration in the next step.

Every webhook delivered through Hookdeck is logged, inspectable, and replayable from the dashboard. When you're debugging a malformed payload or a missing field, being able to replay the exact webhook without re-triggering it in Linear saves significant time.

Step 3: Configure the webhook URL in Linear

Go back to your application settings in Linear and paste the Hookdeck source URL as your webhook endpoint. Linear will now send AgentSessionEvent webhooks to this URL whenever someone mentions or assigns your agent.

Step 4: Build your webhook server

Here's a minimal Express server that receives Linear webhooks, verifies signatures, and responds to agent sessions:

import crypto from "node:crypto";
import express from "express";

const app = express();

app.use(express.json({
  verify: (req, _res, buf) => {
    (req as any).rawBody = buf;
  },
}));

app.post("/webhook/linear", (req, res) => {
  // Verify the webhook signature
  const signature = req.headers["linear-signature"] as string;
  const expected = crypto
    .createHmac("sha256", process.env.LINEAR_WEBHOOK_SECRET!)
    .update((req as any).rawBody)
    .digest("hex");

  if (!crypto.timingSafeEqual(
    Buffer.from(signature, "hex"),
    Buffer.from(expected, "hex")
  )) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  const { action, agentSession, promptContext } = req.body;

  // Respond immediately — Linear expects a response within 5 seconds
  res.status(200).json({ ok: true });

  // Handle the event asynchronously
  if (action === "created") {
    handleNewSession(agentSession, promptContext);
  } else if (action === "prompted") {
    handleFollowUp(req.body);
  }
});

app.listen(3000, () => console.log("Listening on port 3000"));

The critical detail: respond to Linear within 5 seconds, then process asynchronously. If you don't emit an activity within 10 seconds of a created event, Linear marks the session as unresponsive.

Step 5: Emit agent activities

Agent activities are how your bot communicates back to Linear. There are five types, and each one maps to a specific UX in the Linear interface:

Activity TypePurposeExample
thoughtShow internal reasoning (ephemeral)"Searching the codebase for related issues..."
actionDescribe a tool invocation"Querying the database for customer records"
elicitationAsk the user a question"Which environment should I deploy to?"
responseDeliver the final resultThe completed output, in Markdown
errorReport a failure"API rate limit exceeded. Try again in 5 minutes."

Emit activities using the Linear SDK:

import { LinearClient } from "@linear/sdk";

const linear = new LinearClient({ accessToken: process.env.LINEAR_OAUTH_TOKEN });

// Acknowledge immediately with a thought
await linear.createAgentActivity({
  agentSessionId: sessionId,
  content: { type: "thought", body: "Looking into this..." },
});

// ... do your work ...

// Send the final response
await linear.createAgentActivity({
  agentSessionId: sessionId,
  content: { type: "response", body: "Here's what I found:\n\n..." },
});

Agent plans

For multi-step tasks, you can show a progress checklist using agent plans. Each step has a content string and a status of pending, inProgress, or completed, for example:

await linear.agentSessionUpdate(sessionId, {
  plan: {
    steps: [
      { content: "Research topic", status: "completed" },
      { content: "Write draft", status: "inProgress" },
      { content: "Verify accuracy", status: "pending" },
    ],
  },
});

Step 6: Handle the full lifecycle

A production agent needs to handle three webhook actions:

  • created — A new session. The promptContext field contains a formatted string with the issue title, description, and any guidance. This is your agent's starting context.
  • prompted — The user replied to your agent. The message is in agentActivity.body. Use this for follow-up requests or feedback loops.
  • stopped — The user cancelled the task (delivered as a prompted event with agentActivity.signal: "stop"). Clean up any running work and acknowledge with a stop signal.
if (action === "created") {
  // New task — extract context from promptContext and start working
  const topic = extractFromPromptContext(promptContext);
  await doWork(agentSession.id, topic);

} else if (action === "prompted") {
  const { agentActivity } = req.body;

  // Check for stop signal first
  if (agentActivity?.signal === "stop") {
    cancelWork(agentSession.id);
    await emitActivity(agentSession.id, "response", "Stopped.", "stop");
    return;
  }

  // Otherwise, handle the follow-up message
  await handleRevision(agentSession.id, agentActivity.body);
}

Gotchas and lessons learned

Building Linear agents is straightforward in concept but has a few sharp edges worth knowing about:

The webhook payload uses agentSession, not data

If you're used to Linear's data change webhooks, you might expect payload.data to contain the entity. Agent session events use payload.agentSession instead. The header is also Linear-Event: AgentSessionEvent, not just AgentSession.

promptContext is XML-formatted

The promptContext field contains structured XML, not plain text:

<issue identifier="ENG-42">
  <title>Fix login timeout on mobile</title>
  <description>Users are reporting...</description>
  <team name="Mobile"/>
</issue>

Parse accordingly — don't just grab the first line.

Stop signals arrive as prompted, not stopped

When a user stops your agent, Linear sends a prompted event with agentActivity.signal: "stop", not an action: "stopped" event. Check for the signal before processing the message body, or your agent will try to interpret the stop as a follow-up request.

The 10-second responsiveness window is real

Linear marks your agent session as unresponsive if it doesn't receive an activity within 10 seconds of a created event. Emit a thought activity immediately, then do your actual work asynchronously.

Plans must be replaced in full

You can't update a single step in an agent plan. Every call to agentSessionUpdate must include the complete array of steps with their current statuses. Build a helper function that manages the step states for you.

Agents don't count as billable users

Agents installed in your workspace are free. They don't consume a seat, which makes experimentation low-risk.

Using Hookdeck in production

During development, Hookdeck CLI tunnels webhooks to localhost. When you're ready to deploy, replace the tunnel with a direct Hookdeck connection pointing to your production server. Your source URL stays the same — you just change the destination.

This gives you the same benefits in production that made development easier:

  • Automatic retries — If your server is temporarily down, Hookdeck retries with backoff instead of dropping the webhook.
  • Event log — Every webhook is stored and inspectable. When something goes wrong at 2am, you can replay the exact payload that caused it.
  • Filtering and transformation — Route different event types to different endpoints, or transform payloads before they reach your server.
  • Rate limiting — Protect your server from webhook bursts during bulk operations.

Update your connection to point to your deployed server:

hookdeck listen https://your-server.com linear-agent --path /webhook/linear

Or configure the connection directly in the Hookdeck dashboard.

Wrapping up

Linear's Agent Interaction SDK turns your workspace into a platform where AI agents and humans collaborate through the same interface. The webhook-driven architecture means your agent can be as simple as a script that triages issues or as complex as a multi-step research pipeline.

Hookdeck CLI removes the friction of local webhook development by giving you a stable URL, full payload visibility, and replay capability from the start. When you move to production, the same infrastructure scales with you.

Resources:


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.