AI Managing AI Projects: How We Automated Linear with Claude Code

by Alien Brain Trust AI Learning
AI Managing AI Projects: How We Automated Linear with Claude Code

AI Managing AI Projects: How We Automated Linear with Claude Code

Meta Description: We built full Linear automation with Claude Code—37 issues imported, API integration, Git sync, one encoding disaster. Manual updates: 74 min → 0 sec.

There’s something beautifully meta about using AI to automate the management of an AI project. We were building the Secure AI Prompt Builder course, tracking 37 issues across 4 milestones in Linear. Every status update, every comment, every ticket check was manual work. Until we taught Claude to do it for us.

The result: Full automation. Claude can now read tickets, update statuses, add comments, and link Git commits automatically. Manual ticket management time: 74 minutes → 0 seconds. But getting there involved one spectacular failure, a complete do-over, and some creative problem-solving.

The Problem: Manual Ticket Management is a Tax on Focus

Before automation, updating a Linear ticket looked like this:

  1. Stop coding
  2. Open Linear in browser
  3. Find the ticket (scroll, search, or remember the ID)
  4. Click “Edit”
  5. Update status
  6. Add comment describing what you did
  7. Save
  8. Return to code (wait, what was I doing?)

Time per update: ~2 minutes Updates per day: ~5-10 Context switches: Constant Impact on flow state: Devastating

We had 37 tickets to manage. That’s 74 minutes of pure overhead if you update each one once. Plus the hidden cost: every context switch breaks your flow.

Attempt 1: Programmatic Import via Linear SDK

We started with the basics: Can we import all 37 issues programmatically instead of clicking through the Linear UI?

The plan:

  1. Create a CSV with all issues (title, description, priority, milestone, labels)
  2. Write a Node.js script using @linear/sdk
  3. Import everything in one go

The implementation:

const { LinearClient } = require('@linear/sdk');

const client = new LinearClient({ apiKey: process.env.LINEAR_API_KEY });

// Create the team
const team = await client.createTeam({
  name: "Secure AI Prompt Builder",
  key: "SAPB"
});

// Create labels
const labels = ['testing', 'content', 'product', 'launch', 'documentation'];
for (const labelName of labels) {
  await client.createIssueLabel({
    teamId: team.id,
    name: labelName
  });
}

// Create milestones (projects in Linear)
const milestones = {
  'M1': await client.createProject({ teamId: team.id, name: "Testing Complete" }),
  'M2': await client.createProject({ teamId: team.id, name: "Course Ready" }),
  'M3': await client.createProject({ teamId: team.id, name: "Soft Launch" }),
  'M4': await client.createProject({ teamId: team.id, name: "Public Launch" })
};

// Import all 37 issues
for (const issue of issues) {
  await client.createIssue({
    teamId: team.id,
    title: issue.title,
    description: issue.description,
    priority: issue.priority,
    projectId: milestones[issue.milestone].id,
    // ... more fields
  });
}

What could go wrong?

The Spectacular Failure: Special Characters Strike

Everything looked perfect. The script ran. Linear showed… garbage.

The problem: Issue descriptions contained special characters—quotes, em dashes, bullets copied from Word docs. The CSV parsing broke silently. Issues imported with:

  • Truncated descriptions
  • Broken formatting
  • Random character mojibake
  • Missing critical details

The decision: Delete everything and start over.

Yes, all 37 issues. The entire team. Every label, every milestone. Gone.

Why not fix in place? Because partial corruption is worse than starting clean. You don’t know what’s broken until you hit it. Better to rebuild correctly than debug 37 corrupted tickets.

Attempt 2: Proper String Escaping

The fix was straightforward but critical:

// Before (broken)
const description = row.description;

// After (works)
const description = row.description
  .replace(/"/g, '\\"')          // Escape quotes
  .replace(/\n/g, '\\n')          // Preserve newlines
  .replace(/\r/g, '')             // Remove carriage returns
  .trim();

Lesson learned: Test with edge cases first. Create one issue with every special character you can think of. If that works, the rest will work.

Second attempt result: Perfect import. All 37 issues, clean formatting, proper milestone assignments, labels intact.

Time saved vs. manual creation: 2-3 hours

Building the API Integration: Claude Writes Its Own Tickets

Importing issues was step one. Real power comes from runtime integration—Claude updating tickets as it works.

Goal: Claude should be able to:

  1. Read ticket status and details
  2. Update ticket status
  3. Add comments with completion notes
  4. Change priority if needed

Implementation: update-linear-issue.js (195 lines)

#!/usr/bin/env node

const { LinearClient } = require('@linear/sdk');

const LINEAR_API_KEY = process.env.LINEAR_API_KEY;
const client = new LinearClient({ apiKey: LINEAR_API_KEY });

// Parse command line arguments
const issueId = args[0]; // e.g., "SAPB-20"
const options = {
  status: args.find(a => a === '--status') ? args[args.indexOf('--status') + 1] : null,
  comment: args.find(a => a === '--comment') ? args[args.indexOf('--comment') + 1] : null,
  priority: args.find(a => a === '--priority') ? args[args.indexOf('--priority') + 1] : null
};

// Find the issue
const issues = await client.issues({
  filter: { number: { eq: parseInt(issueId.split('-')[1]) }}
});

const issue = issues.nodes[0];

// Update status
if (options.status) {
  const team = await issue.team;
  const states = await team.states();
  const targetState = states.nodes.find(s => s.name === options.status);
  await client.updateIssue(issue.id, { stateId: targetState.id });
}

// Add comment
if (options.comment) {
  await client.createComment({
    issueId: issue.id,
    body: options.comment
  });
}

Usage:

node update-linear-issue.js SAPB-20 --status "Done" --comment "Completed API key guide"

The Windows Challenge: Environment variables work differently on Windows. PowerShell doesn’t pass them to Node.js automatically.

Solution: PowerShell wrapper (update-issue.ps1)

param(
    [Parameter(Mandatory=$true)][string]$IssueId,
    [string]$Status,
    [string]$Comment
)

# Load LINEAR_API_KEY from user environment
$env:LINEAR_API_KEY = [System.Environment]::GetEnvironmentVariable('LINEAR_API_KEY', 'User')

# Get the directory where this script is located (for path resolution)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path

# Run the Node.js script
& node (Join-Path $scriptDir "update-linear-issue.js") @args

Now it works:

powershell -File update-issue.ps1 SAPB-20 -Status "Done" -Comment "API integration complete"

Result: Claude can update Linear tickets directly during work sessions.

Git Commit Integration: Automatic Issue Linking

API integration is powerful, but we can do better. What if commits automatically updated tickets?

GitHub + Linear integration supports keywords:

  • Fixes SAPB-20 → Marks issue as Done
  • Relates to SAPB-20 → Links without closing

Example commit:

git commit -m "Add cross-platform credential manager

Implements secure storage for Windows, macOS, Linux.

**Features:**
- Windows: DPAPI encryption
- macOS: Keychain Access
- Linux: Secret Service + AES-256-GCM fallback

Fixes SAPB-20

🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>"

What happens:

  1. Commit pushed to GitHub
  2. GitHub notifies Linear (via integration)
  3. Linear finds “Fixes SAPB-20”
  4. Issue automatically marked as Done
  5. Commit linked in issue activity
  6. Files changed visible in Linear

Best practice: Use both methods

  • API updates during work (progress comments)
  • Git keywords on completion (final commit links)

Auditing All Tickets: check-all-issues.js

With 37 tickets, how do you know the current state? Click through all of them? Not anymore.

Created: check-all-issues.js + PowerShell wrapper

const issues = await client.issues({
  filter: { team: { key: { eq: 'SAPB' }}}
});

console.log(`Found ${issues.nodes.length} issues:\n`);

for (const issue of issues.nodes) {
  const state = await issue.state;
  const assignee = await issue.assignee;
  const comments = await issue.comments();

  console.log(`${issue.identifier}: ${issue.title}`);
  console.log(`  Status: ${state.name}`);
  console.log(`  Priority: ${getPriorityEmoji(issue.priority)}`);
  console.log(`  Comments: ${comments.nodes.length}`);
  console.log(`  Updated: ${new Date(issue.updatedAt).toLocaleString()}`);
  console.log('');
}

Output:

Found 37 issues:

SAPB-1: Fix test scoring logic
  Status: Done
  Priority: 🟠 High
  Comments: 0
  Updated: 12/26/2025, 8:44:37 PM

SAPB-20: API key configuration guide
  Status: Done
  Priority: 🟡 Medium
  Comments: 4
  Updated: 12/28/2025, 9:19:00 AM

[... 35 more issues ...]

Time to audit all tickets:

  • Before: ~5-10 minutes (clicking through Linear UI)
  • After: 30 seconds (run script, scan output)

Results: Time Saved and Flow Preserved

Automation built:

  1. ✅ Programmatic import (37 issues in ~60 seconds)
  2. ✅ Direct API integration (Claude updates tickets)
  3. ✅ PowerShell wrappers (Windows environment)
  4. ✅ Git commit automation (automatic linking)
  5. ✅ Full audit script (all ticket status at a glance)

Time saved per session:

  • Manual ticket updates: 10-20 minutes
  • With automation: 0 seconds (happens automatically)

Time saved over 2-week sprint:

  • Daily updates: ~10 min × 10 days = 100 minutes
  • Plus context switching: ~20 minutes
  • Total: ~2 hours saved per sprint

Flow state preservation: Priceless. No more “wait, what was I working on?”

Key Lessons Learned

1. Test Edge Cases First

Don’t import 37 issues and hope for the best. Create one test issue with:

  • Special characters: "quotes", em-dashes—, bullets •
  • Newlines and formatting
  • Long descriptions
  • All the weird stuff

If that one works, the rest will work. If not, fix it before importing everything.

2. Platform-Specific Solutions Are OK

The PowerShell wrapper feels hacky. It is. That’s fine.

Windows environment variables work differently than Unix. Fighting it wastes time. Embrace platform-specific solutions:

  • PowerShell wrapper for Windows
  • Bash wrapper for macOS/Linux
  • Document both approaches

Principle: Solve the user’s problem, don’t fight the platform.

3. Automation Compounds

Each automation enables the next:

  1. Import script → Foundation for everything else
  2. API integration → Claude can update tickets
  3. Git keywords → Commits automatically close issues
  4. Audit script → Full visibility in 30 seconds

The fifth automation (not built yet): Automatic testing that updates tickets based on test results. Possible because #2 exists.

Lesson: Build automation in layers. Each layer makes the next easier.

4. Deleting and Starting Over Is Often Faster

When the encoding failed, we could have:

  • Manually fixed 37 corrupted descriptions
  • Written a script to patch in-place
  • Lived with partial corruption

Instead: Delete everything, fix the root cause, re-import cleanly.

Time to manually fix: 1-2 hours (error-prone) Time to delete + fix + re-import: 20 minutes (guaranteed correct)

Principle: Don’t throw good time after bad. If the foundation is broken, rebuild it.

5. Meta-Work Automation Multiplies Productivity

Direct work: Writing code, fixing bugs, building features. Meta-work: Updating tickets, switching contexts, manual tracking.

Automating meta-work doesn’t just save time—it preserves focus. Every eliminated context switch keeps you in flow state longer.

ROI calculation:

  • 2 hours saved per sprint (direct time)
  • ~5 hours of additional productive time (preserved flow state)
  • Total: ~7 hours gained per 2-week sprint

Over a year (26 sprints): ~180 hours or 4.5 weeks of productive time.

What’s Next: The Automation Roadmap

Current state: Manual ticket updates eliminated.

Next automations:

  1. Test results → ticket updates - Failed tests automatically comment on related tickets
  2. Milestone progress tracking - Daily summary of progress toward each milestone
  3. Automated prioritization - Critical failures bump ticket priority
  4. Release automation - Completed milestones trigger deployment workflows

The foundation is built. Now we stack automation on automation.

The Code

All automation scripts live in 00-Project-Management/:

Skills documentation:

Bottom Line

AI managing AI projects isn’t just efficient—it’s fitting. We’re building tools to automate AI implementation. Why not use AI to automate managing that work?

The encoding failure taught us to test edge cases first. The PowerShell wrapper taught us to embrace platform-specific solutions. The time savings taught us that meta-work automation compounds.

Manual ticket updates: 74 minutes Automated: 0 seconds Learning to automate it: Worth every minute


Next post: How we built a cross-platform API key manager with Windows DPAPI, macOS Keychain, and Linux Secret Service—zero plaintext secrets.