Skill: arnarsson/linear-webhook (version 1.0.0)

--- name: linear-webhook description: "Comment @mason or @eureka in Linear issues to dispatch tasks to agents. Webhook receives Linear comments and routes to correct agent." --- # Linear Webhook Skill Enables Linear issue comment @mentions to dispatch tasks to Clawdbot agents. ## How It Works 1. **Comment in Linear:** `@mason implement user authentication` or `@eureka plan Q2 roadmap` 2. **Linear webhook fires** on comment creation 3. **Clawdbot receives webhook** via exposed endpoint 4. **Transform parses payload:** - Extracts @mason or @eureka mention - Gets issue context (title, description, labels) - Prepares task prompt 5. **Routes to agent session:** - @mason → `mason` agent (code/implementation) - @eureka → `eureka` agent (planning/strategy) 6. **Agent processes task** and returns result 7. **Result posted back** as Linear comment ## Setup ### 1. Configure Clawdbot Webhooks Add to your `config.json5`: ```json5 { hooks: { enabled: true, [REDACTED_SECRET]", // Generate with: openssl rand -base64 32 path: "/hooks", transformsDir: [REDACTED_SECRET], mappings: [ { name: "linear", match: { path: "/linear", method: "POST" }, action: "agent", transform: { module: "./linear-transform.js", export: "transformLinearWebhook" }, deliver: false, // Don't auto-deliver to chat - Linear comments handle responses } ] } } ``` ### 2. Expose Webhook Endpoint Use Cloudflare Tunnel or Tailscale Funnel to make webhook publicly accessible: **Option A: Cloudflare Tunnel** (Recommended) ```bash # Install if needed brew install cloudflared # Start tunnel (replace with your domain) cloudflared tunnel --url http://localhost:18789 ``` **Option B: Tailscale Funnel** ```bash # Enable funnel tailscale funnel 18789 ``` Note the public URL (e.g., `https://your-tunnel.trycloudflare.com`) ### 3. Configure Linear Webhook 1. Go to Linear Settings → API → Webhooks 2. Click "Create new webhook" 3. Set URL: `https://your-tunnel.trycloudflare.com/hooks/linear` 4. Add custom header: `x-clawdbot-[REDACTED_SECRET]` 5. Select events: **Comment → Created** 6. Save webhook ### 4. Test Comment in a Linear issue: ``` @mason add user authentication to the login page ``` Expected flow: 1. Webhook fires to Clawdbot 2. Mason agent receives task 3. Mason implements or responds 4. Result posted back to Linear issue as comment ## Agent Routing - **@mason** → Code implementation, debugging, technical tasks - **@eureka** → Planning, strategy, research, communication - Other mentions → Ignored (not handled) ## Issue Context Provided The agent receives: - Issue title - Issue description - Issue labels - Comment text (the @mention) - Issue URL - Commenter name ## Customization ### Add More Agents Edit `linear-transform.js`: ```javascript const AGENT_MENTIONS = { '@mason': 'mason', '@eureka': 'eureka', '@designer': 'designer', // Add your own agents }; ``` ### Change Response Behavior Modify `deliver` and `channel` in config: ```json5 { deliver: true, channel: "telegram", to: "1878354815", // Your Telegram chat ID } ``` This will also send agent responses to Telegram. ## Security - **Never commit hook token** to version control - Use environment variables: `CLAWDBOT_HOOK_TOKEN` - Verify webhook source (Linear's IP ranges if needed) - Use HTTPS only (Cloudflare Tunnel provides this) ## Troubleshooting ### Webhook not firing - Check Linear webhook logs (Settings → API → Webhooks → View logs) - Verify tunnel is running: `curl https://your-tunnel.trycloudflare.com/hooks/linear` - Check Clawdbot logs: `clawdbot gateway logs` ### Agent not responding - Check transform is loading: Look for errors in gateway logs - Verify agent session exists: `clawdbot sessions list` - Test transform manually: `node linear-transform.js` ### Response not posting to Linear - Implement Linear API comment posting in transform - Add Linear API token to config - See `linear-transform.js` for example ## Linear API Access To post comments back to Linear, you need a Linear API token: 1. Go to Linear Settings → API → Personal API keys 2. Create new token with `write` scope 3. Add to environment: `CLAWDBOT_LINEAR_API_KEY=lin_api_...` 4. Transform will use this to post responses ## Files - `SKILL.md` - This documentation - `linear-transform.js` - Webhook payload parser and agent router - `linear-api.js` - Linear GraphQL API client (for posting comments) - `example-payload.json` - Sample Linear webhook payload for testing ## References - [Clawdbot Webhook Docs](/automation/webhook) - [Linear Webhooks API](https://developers.linear.app/docs/graphql/webhooks) - [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/)

path: README.md
content: # Linear Webhook Skill

Enables @mason and @eureka mentions in Linear issue comments to dispatch tasks to Clawdbot agents.

## Quick Start

### 1. Install Dependencies
```bash
# None required - uses Node.js built-ins
```

### 2. Configure Clawdbot

Add this to your Clawdbot config (`.clawd/config.json5`):

```json5
{
  hooks: {
    enabled: true,
    [REDACTED_SECRET],
    path: "/hooks",
    transformsDir: "/home/sven/clawd-mason/skills/linear-webhook",
    mappings: [
      {
        name: "linear",
        match: { path: "/linear", method: "POST" },
        action: "agent",
        transform: {
          module: "./linear-transform.js",
          export: "transformLinearWebhook"
        },
        deliver: false,
      }
    ]
  }
}
```

### 3. Set Environment Variables

```bash
# Generate hook token
export CLAWDBOT_HOOK_TOKEN=$(openssl rand -base64 32)

# Get Linear API key from: https://linear.app/settings/api
export LINEAR_API_KEY="lin_api_your_key_here"
```

### 4. Expose Webhook Endpoint

**Option A: Cloudflare Tunnel** (Recommended)
```bash
# Install
brew install cloudflared

# Start tunnel
cloudflared tunnel --url http://localhost:18789

# Note the public URL (e.g., https://abc-123.trycloudflare.com)
```

**Option B: Tailscale Funnel**
```bash
tailscale funnel 18789
```

### 5. Configure Linear Webhook

1. Go to **Linear Settings** → **API** → **Webhooks**
2. Click "Create new webhook"
3. **URL:** `https://your-tunnel-url.trycloudflare.com/hooks/linear`
4. **Custom Header:** 
   - Name: `x-clawdbot-token`
   - Value: `your-CLAWDBOT_HOOK_TOKEN`
5. **Events:** Select **Comment → Created**
6. **Save**

### 6. Test

Comment in a Linear issue:
```
@mason implement OAuth2 authentication
```

Check Clawdbot logs:
```bash
clawdbot gateway logs
```

## Usage

### Mention Agents

- **@mason** - Code implementation, debugging, technical tasks
- **@eureka** - Planning, strategy, research, communication

### Example Comments

```
@mason add user authentication to the API

@eureka plan the Q2 product roadmap

@mason debug the failing test in src/auth.test.js

@eureka research competitors and create comparison doc
```

## Agent Context

Agents receive full issue context:
- Issue title & description
- Issue labels
- Priority level
- Current status
- Assignee
- Comment text
- URL to issue

## Response Posting

To enable automatic posting of agent responses back to Linear:

1. Get Linear API key with `write` scope
2. Set `LINEAR_API_KEY` environment variable
3. Uncomment the response posting code in `linear-transform.js`
4. Restart Clawdbot gateway

## File Structure

```
linear-webhook/
├── SKILL.md                  # Main documentation
├── README.md                 # This file
├── linear-transform.js       # Webhook payload parser
├── config-example.json5      # Configuration template
├── example-payload.json      # Sample Linear webhook payload
└── test.sh                   # Testing script
```

## Testing

### Test Transform Locally
```bash
node linear-transform.js
```

### Test Webhook Endpoint
```bash
curl -X POST http://localhost:18789/hooks/linear \
  -H "x-clawdbot-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d @example-payload.json
```

### Monitor Logs
```bash
# Clawdbot gateway logs
clawdbot gateway logs --follow

# Or check session logs
clawdbot sessions list
```

## Troubleshooting

### Webhook not firing
- Check Linear webhook logs (Settings → API → Webhooks → View logs)
- Verify tunnel is running: `curl https://your-tunnel-url/hooks/linear`
- Check Clawdbot logs: `clawdbot gateway logs`

### Transform not loading
- Check `transformsDir` path in config
- Verify `linear-transform.js` exists and has no syntax errors
- Check gateway logs for errors

### Agent not receiving task
- Verify agent session exists: `clawdbot sessions list`
- Check session key format: `linear:mason:issue-123`
- Ensure webhook delivered successfully (200/202 response)

### Response not posting to Linear
- Verify `LINEAR_API_KEY` is set
- Check Linear API token has `write` scope
- Look for errors in `postLinearComment` function logs

## Security

- **Never commit tokens** to version control
- Use environment variables for sensitive values
- Keep webhook endpoint behind tunnel (not publicly exposed without auth)
- Verify webhook source (Linear's IP ranges)
- Rotate hook token regularly

## Limitations

- Currently supports only @mason and @eureka
- One mention per comment (takes first if multiple)
- Requires public endpoint (tunnel or proxy)
- Response posting is manual (requires additional code)

## Future Enhancements

- [ ] Support more agents (@designer, @qa, etc.)
- [ ] Multi-agent collaboration on single issue
- [ ] Automatic response posting via Linear API
- [ ] Thread tracking (multi-turn conversations)
- [ ] Priority-based routing
- [ ] Label-based agent selection
- [ ] Status updates (move issue to "In Progress" on agent accept)
- [ ] Time tracking integration
- [ ] Mention multiple agents in one comment

## Contributing

To add new agent mentions:

1. Edit `AGENT_MENTIONS` in `linear-transform.js`
2. Add agent name mapping
3. Ensure agent session exists in Clawdbot config
4. Test with example payload

## License

MIT - Use freely for personal and commercial projects


path: config-example.json5
content: // Clawdbot Configuration Example for Linear Webhook Skill
// Add this to your ~/.clawd/config.json5 or workspace config

{
  // ... other config ...

  // Enable webhooks
  hooks: {
    enabled: true,
    [REDACTED_SECRET]", // Generate with: openssl rand -base64 32
    path: "/hooks",
    
    // Point to transform directory
    transformsDir: "/home/sven/clawd-mason/skills/linear-webhook",
    
    // Linear webhook mapping
    mappings: [
      {
        name: "linear",
        match: {
          path: "/linear",
          method: "POST"
        },
        action: "agent",
        transform: {
          module: "./linear-transform.js",
          export: "transformLinearWebhook"
        },
        // Don't auto-deliver to chat - responses will be posted to Linear
        deliver: false,
        
        // Optional: Also send notifications to Telegram
        // deliver: true,
        // channel: "telegram",
        // to: "1878354815",
      }
    ]
  },

  // Optional: Agent-specific configurations
  agents: {
    enabled: true,
    defaults: {
      model: "anthropic/claude-sonnet-4-5",
      thinking: "low",
      timeoutSeconds: 300,
    },
    // Ensure mason and eureka agents are allowed
    sessions: {
      mason: {
        label: "Mason - Code Agent",
        model: "anthropic/claude-sonnet-4-5",
      },
      eureka: {
        label: "Eureka - Planning Agent",
        model: "anthropic/claude-sonnet-4-5",
      }
    }
  },

  // ... other config ...
}

// Environment Variables Needed:
// - CLAWDBOT_HOOK_TOKEN (or set in config above)
// - CLAWDBOT_LINEAR_API_KEY or LINEAR_API_KEY (for posting comments back)
//
// Generate Linear API key:
// 1. Go to Linear Settings → API → Personal API keys
// 2. Create new token with 'write' scope
// 3. Set as environment variable or in config


path: example-payload.json
content: {
  "type": "Comment",
  "action": "create",
  "createdAt": "2026-01-27T00:00:00.000Z",
  "data": {
    "id": "comment-abc123",
    "createdAt": "2026-01-27T00:00:00.000Z",
    "body": "@mason implement user authentication with OAuth2. Use Google and GitHub as providers.",
    "url": "https://linear.app/team/issue/ABC-123#comment-abc123",
    "user": {
      "id": "user-123",
      "name": "Sven Arnarsson",
      "email": "[REDACTED_SECRET]"
    },
    "issue": {
      "id": "issue-abc123",
      "identifier": "ABC-123",
      "title": "Add user authentication",
      "description": "We need OAuth2 authentication for the app to allow users to sign in with their Google or GitHub accounts. This should include:\n\n- OAuth2 flow implementation\n- User session management\n- Token refresh logic\n- Secure storage of credentials",
      "url": "https://linear.app/team/issue/ABC-123",
      "state": {
        "id": "state-123",
        "name": "In Progress",
        "type": "started"
      },
      "priority": 1,
      "assignee": {
        "id": "user-123",
        "name": "Sven Arnarsson",
        "email": "[REDACTED_SECRET]"
      },
      "team": {
        "id": "team-123",
        "name": "Engineering",
        "key": "ENG"
      },
      "labels": {
        "nodes": [
          {
            "id": "label-1",
            "name": "backend",
            "color": "#4a90e2"
          },
          {
            "id": "label-2",
            "name": "security",
            "color": "#e24a4a"
          },
          {
            "id": "label-3",
            "name": "priority",
            "color": "#f5a623"
          }
        ]
      }
    }
  },
  "organizationId": "org-123",
  "webhookId": "webhook-123"
}


path: post-response.js
content: #!/usr/bin/env node
/**
 * Linear Response Poster
 * 
 * Checks for completed agent sessions and posts responses back to Linear.
 * Can be run as a cron job or triggered after webhook completes.
 * 
 * Usage:
 *   node post-response.js [sessionKey]
 *   
 * Environment:
 *   LINEAR_API_KEY or CLAWDBOT_LINEAR_API_KEY required
 */

const { postLinearComment } = require('./linear-transform.js');

/**
 * Get session history from Clawdbot
 * 
 * @param {string} sessionKey - Session key to fetch
 * @returns {Promise<Object>} - Session data with messages
 */
async function getSessionHistory(sessionKey) {
  // This would use Clawdbot API or session logs
  // For now, placeholder that reads from sessions_send or logs
  
  // Example: Use clawdbot CLI
  const { exec } = require('child_process');
  const { promisify } = require('util');
  const execAsync = promisify(exec);
  
  try {
    const { stdout } = await execAsync(`clawdbot sessions history ${sessionKey} --json`);
    return JSON.parse(stdout);
  } catch (error) {
    console.error('Failed to fetch session history:', error.message);
    return null;
  }
}

/**
 * Extract agent response from session history
 * 
 * @param {Object} session - Session history
 * @returns {string|null} - Last agent message or null
 */
function extractAgentResponse(session) {
  if (!session || !session.messages) {
    return null;
  }
  
  // Find last assistant message
  const messages = session.messages.filter(m => m.role === 'assistant');
  
  if (messages.length === 0) {
    return null;
  }
  
  const lastMessage = messages[messages.length - 1];
  return lastMessage.content;
}

/**
 * Main execution
 */
async function main() {
  const sessionKey = process.argv[2];
  
  if (!sessionKey) {
    console.error('Usage: node post-response.js <sessionKey>');
    console.error('Example: node post-response.js linear:mason:issue-123');
    process.exit(1);
  }
  
  // Extract metadata from session key
  const match = sessionKey.match(/^linear:(\w+):(.+)$/);
  
  if (!match) {
    console.error('Invalid session key format. Expected: linear:agent:issueId');
    process.exit(1);
  }
  
  const [, agentName, issueId] = match;
  
  console.log(`Fetching session: ${sessionKey}`);
  const session = await getSessionHistory(sessionKey);
  
  if (!session) {
    console.error('Failed to fetch session');
    process.exit(1);
  }
  
  console.log(`Extracting agent response...`);
  const response = extractAgentResponse(session);
  
  if (!response) {
    console.error('No agent response found in session');
    process.exit(1);
  }
  
  console.log(`Agent response (${response.length} chars):`);
  console.log(response.slice(0, 200) + '...');
  console.log('');
  
  // Get agent display name
  const AGENT_NAMES = {
    'mason': 'Mason (Clawdbot)',
    'eureka': 'Eureka (Clawdbot)',
  };
  
  const agentDisplayName = AGENT_NAMES[agentName] || agentName;
  
  console.log(`Posting to Linear issue: ${issueId}`);
  await postLinearComment(issueId, response, agentDisplayName);
  
  console.log('Done!');
}

// Run if called directly
if (require.main === module) {
  main().catch(error => {
    console.error('Error:', error);
    process.exit(1);
  });
}

module.exports = { getSessionHistory, extractAgentResponse };


path: post-to-linear.sh
content: #!/bin/bash
# Post a response to Linear issue
# Usage: ./post-to-linear.sh <issue_id> <agent_name> <response_text>

ISSUE_ID="$1"
AGENT_NAME="$2"
RESPONSE="$3"

if [ -z "$ISSUE_ID" ] || [ -z "$AGENT_NAME" ] || [ -z "$RESPONSE" ]; then
  echo "Usage: ./post-to-linear.sh <issue_id> <agent_name> <response_text>"
  exit 1
fi

LINEAR_API_KEY=$(cat ~/.linear_api_key)

node -e "
const {postLinearComment} = require('/home/sven/clawd-mason/skills/linear-webhook/linear-transform.js');
postLinearComment('$ISSUE_ID', \`$RESPONSE\`, '$AGENT_NAME');
"


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)