# How to Build Linear Agents with Hookdeck CLI

[Linear agents](https://linear.app/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](https://linear.app/developers/agents) 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](https://hookdeck.com/docs/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](https://linear.app) workspace where you have admin access
* [Node.js](https://nodejs.org/) v18 or later
* The [Hookdeck CLI](https://hookdeck.com/docs/cli) installed (`brew install 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](https://linear.app/settings/api) 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](https://linear.app/developers). 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:

```bash
hookdeck login

```

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

```bash
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](https://dashboard.hookdeck.com). 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:

```typescript
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 Type | Purpose | Example |
| --- | --- | --- |
| `thought` | Show internal reasoning (ephemeral) | "Searching the codebase for related issues..." |
| `action` | Describe a tool invocation | "Querying the database for customer records" |
| `elicitation` | Ask the user a question | "Which environment should I deploy to?" |
| `response` | Deliver the final result | The completed output, in Markdown |
| `error` | Report a failure | "API rate limit exceeded. Try again in 5 minutes." |

Emit activities using the Linear SDK:

```typescript
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:

```typescript
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.

```typescript
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:

```xml
<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:

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

```

Or configure the connection directly in the [Hookdeck dashboard](https://dashboard.hookdeck.com).

## 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](https://hookdeck.com/docs/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:

* [Linear Agent Interaction SDK docs](https://linear.app/developers/agents)
* [Linear Agent Activities reference](https://linear.app/developers/agent-interaction)
* [Hookdeck CLI docs](https://hookdeck.com/docs/cli)
* [Hookdeck webhook testing guide](https://hookdeck.com/docs/guides/how-to-test-webhooks-locally)
* [Linear Weather Bot example](https://github.com/linear/linear/tree/master/packages/weather-bot) — Linear's sample agent implementation