Integrate your gptme-based agent with Linear using the Agent Framework.
- @Mentionable: Users can
@mentionyour agent in Linear issues/comments - Assignable: Issues can be delegated to your agent
- Real-time: Webhook-based responses in seconds (not polling)
- Zero billable seats: Uses OAuth app actor, not human accounts
- Auto token refresh: Automatically refreshes OAuth tokens when expired
User @mentions agent in Linear | v Linear sends AgentSessionEvent webhook | v ngrok tunnel (public HTTPS) | v linear-webhook-server.py (Flask server:8081) | +---> Validates token (auto-refresh if expired) +---> Emits acknowledgment activity +---> Creates git worktree +---> Spawns gptme session +---> Merges changes back to main
| File | Description |
|---|---|
linear-webhook-server.py |
Flask webhook server - receives Linear events |
linear-activity.py |
CLI tool to emit activities back to Linear |
.env |
Configuration (secrets - never commit!) |
.tokens.json |
OAuth access/refresh tokens (never commit!) |
services/ |
Systemd service templates |
Follow these steps to set up Linear integration for your agent.
- Linux server with systemd (user services)
- Python 3.10+ with
uvinstalled ngrokinstalled and authenticated (see below)gptmeinstalled and accessible in PATH- Access to Linear workspace settings
- Agent workspace with
gptme.tomlconfiguration
Have your human operator do the following:
- Sign up for a free ngrok account at https://ngrok.com
- Install ngrok:
# Linux (snap) sudo snap install ngrok # Linux (apt) curl -sSL https://ngrok-agent.s3.amazonaws.com/ngrok.asc \ | sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null \ && echo "deb https://ngrok-agent.s3.amazonaws.com buster main" \ | sudo tee /etc/apt/sources.list.d/ngrok.list \ && sudo apt update \ && sudo apt install ngrok # macOS brew install ngrok
- Authenticate ngrok with your authtoken (from ngrok dashboard):
ngrok config add-authtoken <your-authtoken>
First, you need a public HTTPS URL for Linear to send webhooks to.
# Start ngrok temporarily to get your URL
ngrok http 8081
# Note the Forwarding URL, e.g.:
# https://abc123.ngrok-free.appNote: Free ngrok tier gives random subdomains. For production, consider ngrok paid plan (static subdomain) or Cloudflare Tunnel.
Have your human operator do the following in Linear:
-
Go to Linear → Settings (gear icon) → Workspace Settings
-
Click API in the left sidebar
-
Click OAuth Applications
-
Click the "+" button to create new application
-
Fill in the form:
Field Value Name Your agent name (this becomes the @username) Description Brief description of your agent Webhook URL https://<your-ngrok-domain>/webhookOAuth Callback URL https://<your-ngrok-domain>/oauth/callback -
Enable the Webhooks toggle
-
At the bottom, check these webhook event types:
- ✅ Agent session events (REQUIRED - triggers when @mentioned)
- ✅ Inbox notifications (optional)
-
Click Create
-
IMPORTANT: Copy these values:
- Client ID (
LINEAR_CLIENT_ID) - Client Secret (
LINEAR_CLIENT_SECRET) - Webhook Secret (
LINEAR_WEBHOOK_SECRET)
- Client ID (
The OAuth scopes automatically requested include:
app:mentionable- Agent appears in @mention autocompleteapp:assignable- Agent appears in assignee dropdown
The Name you enter becomes the agent's @username in Linear.
Run the interactive setup script which creates symlinks to gptme-contrib (so you get updates automatically via submodule update):
cd /path/to/gptme-contrib/scripts/linear
./setup.shThe script will:
- Check prerequisites
- Prompt for configuration values
- Display exact values to enter in Linear OAuth app
- Create symlinks in your workspace
- Set up systemd services
- Offer to run OAuth flow
If you prefer to manage files manually:
# Copy the linear integration scripts to your workspace
cp -r /path/to/gptme-contrib/scripts/linear ~/repos/<your-workspace>/scripts/Note: With manual copy, you'll need to re-copy files to get updates.
Create the .env file with your secrets:
cat > ~/repos/<your-workspace>/scripts/linear/.env << 'EOF'
LINEAR_WEBHOOK_SECRET=<webhook-secret-from-step-2>
LINEAR_CLIENT_ID=<client-id-from-step-2>
LINEAR_CLIENT_SECRET=<client-secret-from-step-2>
EOF.env file contains secrets. It must NEVER be committed to git.
The first time, you need to authorize the app to get access tokens.
cd ~/repos/<your-workspace>/scripts/linear
# Start OAuth flow - opens browser for authorization
uv run linear-activity.py authThis will:
- Generate the Linear authorization URL
- Include a one-time OAuth
statevalue in that URL - Open it in your browser (or print if no display)
- After you authorize, Linear redirects to your callback URL
- Validate the returned
state, then exchange the code for tokens
If the CLI auth flow doesn't work, manually create tokens:
-
Run
uv run linear-activity.py authand copy the full authorization URL it prints. Do not replace thestateparameter with a hardcoded value; the command generates a one-time state token for each auth attempt. -
Visit that exact URL in your browser and authorize.
-
After redirect, extract both the
codeandstateparameters from the URL. -
Verify the returned
statematches the URL from step 1, then exchange thecodefor tokens:curl -X POST https://api.linear.app/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "client_id=<CLIENT_ID>" \ -d "client_secret=<CLIENT_SECRET>" \ -d "redirect_uri=<CALLBACK_URL>" \ -d "code=<CODE>"
-
Save the response to
.tokens.json:{ "access_token": "<from response>", "refresh_token": "<from response>", "expires_at": "<calculate: now + expires_in seconds>" }
# Check token status
uv run linear-activity.py token-statusCopy and customize the service templates:
# Create systemd user directory if needed
mkdir -p ~/.config/systemd/user
# Copy templates
cp ~/repos/<your-workspace>/scripts/linear/services/*.template ~/.config/systemd/user/
# Rename and edit (replace <AGENT_NAME>, <WORKSPACE>, <HOME>)
cd ~/.config/systemd/user
mv agent-linear-webhook.service.template <agent-name>-linear-webhook.service
mv agent-ngrok.service.template <agent-name>-ngrok.service
# Edit both files to replace placeholders with actual values# Reload systemd
systemctl --user daemon-reload
# Enable services to start on boot
systemctl --user enable <agent-name>-linear-webhook.service
systemctl --user enable <agent-name>-ngrok.service
# Start services
systemctl --user start <agent-name>-linear-webhook.service
systemctl --user start <agent-name>-ngrok.service
# Verify they're running
systemctl --user status <agent-name>-linear-webhook.service
systemctl --user status <agent-name>-ngrok.serviceAsk your human operator to run:
sudo loginctl enable-linger <username>This ensures services start when the machine boots, not just when you log in.
-
Check services are running:
systemctl --user status <agent-name>-linear-webhook
-
Test by @mentioning your agent in Linear:
- Go to any Linear issue
- Type
@<your-agent-name>in a comment - Watch logs:
journalctl --user -u <agent-name>-linear-webhook -f
Emit activities back to Linear from your agent sessions.
# Show thinking/progress (session stays active)
uv run linear-activity.py thought <session-id> "Analyzing the codebase..."
# Send final response (CLOSES the session)
uv run linear-activity.py response <session-id> "Done! See PR #42."
# Report error
uv run linear-activity.py error <session-id> "Failed to access repository"
# Check token status
uv run linear-activity.py token-status
# Manually refresh token
uv run linear-activity.py refresh| Type | Purpose | Effect |
|---|---|---|
thought |
Show reasoning/progress | Session stays active |
response |
Final answer | Closes the session |
error |
Error occurred | Marks session as errored |
Important: When mentioning users in Linear comments or agent responses via the API, you must use the full Linear profile URL format. Using @username syntax does not work through the API.
Use the full profile link in your comment body:
[User Name](https://linear.app/<workspace>/settings/account/<user-id>)Instead of writing:
@ErikBjare can you review this?Write:
[Erik Bjäreholt](https://linear.app/superuserlabs/settings/account/ace04b67-c8dc-432f-a00d-85953cc14e13) can you review this?To find a user's profile link:
- Go to Linear
- Click on any user's avatar/name to open their profile
- Copy the URL from the browser address bar
The URL format is: https://linear.app/<workspace>/settings/account/<user-id>
Where:
<workspace>is your Linear workspace slug (e.g.,superuserlabs)<user-id>is the user's UUID (e.g.,ace04b67-c8dc-432f-a00d-85953cc14e13)
# Check status
systemctl --user status <agent-name>-linear-webhook <agent-name>-ngrok
# View logs (follow)
journalctl --user -u <agent-name>-linear-webhook -f
# Restart services
systemctl --user restart <agent-name>-linear-webhook <agent-name>-ngrok
# Stop services
systemctl --user stop <agent-name>-linear-webhook <agent-name>-ngrok- Check ngrok is running:
systemctl --user status <agent-name>-ngrok - Check webhook URL in Linear matches ngrok URL
- Check logs:
journalctl --user -u <agent-name>-linear-webhook -f
- Check
.tokens.jsonexists and has valid tokens - Check
.envhas CLIENT_ID and CLIENT_SECRET for token refresh - Try manual refresh:
uv run linear-activity.py refresh
Ensure .env contains all three values:
LINEAR_WEBHOOK_SECRETLINEAR_CLIENT_IDLINEAR_CLIENT_SECRET
Check if lingering is enabled:
loginctl show-user <username> | grep LingerIf Linger=no, ask human to run: sudo loginctl enable-linger <username>
-
Linear OAuth Application settings (Settings > API > OAuth Applications):
- Webhook URL
- OAuth Callback URL
-
Your .env file:
LINEAR_CALLBACK_URL
To minimize disruption:
- Use ngrok paid plan ($8/month) - Get a static subdomain that never changes
- Use Cloudflare Tunnel (free) - Alternative to ngrok with stable URLs:
# Install cloudflared curl -L https://pkg.cloudflare.com/cloudflared-linux-amd64.deb -o cloudflared.deb sudo dpkg -i cloudflared.deb # Authenticate and create tunnel cloudflared tunnel login cloudflared tunnel create linear-webhook cloudflared tunnel route dns linear-webhook your-subdomain.yourdomain.com # Run tunnel (configure in systemd for persistence) cloudflared tunnel run --url http://localhost:8081 linear-webhook
- Run ngrok as persistent service - Fewer restarts means fewer URL changes
Future enhancement: Linear's API could potentially be used to auto-update the webhook URL on ngrok restart. This would require storing the OAuth application ID and using the Linear Admin API.
- Never commit secrets:
.envand.tokens.jsonmust be in.gitignore - Webhook signatures: All webhooks are verified using HMAC-SHA256
- OAuth tokens: Auto-refreshed when expired (requires CLIENT_ID/SECRET)
- ngrok authtoken: Keep private
| Variable | Purpose | Required |
|---|---|---|
LINEAR_WEBHOOK_SECRET |
Verify webhook signatures | Yes |
LINEAR_CLIENT_ID |
OAuth token refresh | Yes |
LINEAR_CLIENT_SECRET |
OAuth token refresh | Yes |
AGENT_NAME |
Agent name for paths (default: "agent") | No |
AGENT_WORKSPACE |
Path to agent workspace (default: ~/repos/$AGENT_NAME) |
No |
WORKTREE_BASE |
Path for session worktrees (default: ~/repos/$AGENT_NAME-worktrees) |
No |
PORT |
Webhook server port (default: 8081) | No |
| Path | Purpose |
|---|---|
scripts/linear/ |
Webhook server and CLI |
logs/linear-sessions/ |
Session execution logs |
/tmp/<agent>-linear-notifications/ |
Raw webhook payloads |
~/.config/systemd/user/ |
Systemd service files |