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:
- Stop coding
- Open Linear in browser
- Find the ticket (scroll, search, or remember the ID)
- Click “Edit”
- Update status
- Add comment describing what you did
- Save
- 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:
- Create a CSV with all issues (title, description, priority, milestone, labels)
- Write a Node.js script using
@linear/sdk - 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:
- Read ticket status and details
- Update ticket status
- Add comments with completion notes
- 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 DoneRelates 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:
- Commit pushed to GitHub
- GitHub notifies Linear (via integration)
- Linear finds “Fixes SAPB-20”
- Issue automatically marked as Done
- Commit linked in issue activity
- 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:
- ✅ Programmatic import (37 issues in ~60 seconds)
- ✅ Direct API integration (Claude updates tickets)
- ✅ PowerShell wrappers (Windows environment)
- ✅ Git commit automation (automatic linking)
- ✅ 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:
- Import script → Foundation for everything else
- API integration → Claude can update tickets
- Git keywords → Commits automatically close issues
- 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:
- Test results → ticket updates - Failed tests automatically comment on related tickets
- Milestone progress tracking - Daily summary of progress toward each milestone
- Automated prioritization - Critical failures bump ticket priority
- 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/:
- update-linear-issue.js (195 lines)
- update-issue.ps1 (PowerShell wrapper)
- check-all-issues.js (audit script)
- check-all-issues.ps1 (PowerShell wrapper)
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.