Linear Ticket to Claude Code Worktree
Auto-spawn Claude Code in a git worktree when assigned a Linear ticket.
Introduction
30 Second Summary
When you get assigned a ticket, there's always a gap between receiving it and actually starting work. You context-switch, create a branch, read through the spec, and by the time you write your first line of code, minutes have passed.
In this project, you will build a local automation that spawns Claude Code in an isolated git worktree the moment a Linear ticket gets assigned to you. The AI agent reads the ticket, documents assumptions, plans its approach, and starts coding on a fresh branch.
What You'll Build
Assign yourself a ticket in Linear and watch your terminal light up as Claude Code automatically spins up in a new worktree, analyzes the ticket, writes a structured plan, and starts implementing code changes on an isolated branch.
By the end of this project, you'll have:
- A webhook listener that receives real-time issue events from Linear and filters for tickets assigned specifically to you.
- An ngrok tunnel that exposes your local server to the internet so Linear can reach your machine with live updates.
- A structured prompt template that makes Claude Code analyze tickets, document assumptions in a plan file, and implement changes in isolated git worktrees so multiple tickets can run in parallel without conflicts.
- Secret Mission: Post Claude's implementation plan back to the Linear ticket as a comment via the GraphQL API.
Are there any prerequisites?
This project assumes you have Node.js installed, a Claude Code Max subscription, and a Linear workspace where you have admin access. You also need a git repository at ~/Documents/projects-app that is also hosted on GitHub.
The guide walks you through installing ngrok and setting up everything else from scratch.
Before We Start
Before any hands-on work, this is the moment to lock in what you're building and why it matters. You're creating an automation that removes the context-switching gap between receiving a Linear ticket and starting work on it.
Set Up ngrok and Verify Tools
Linear needs a way to reach your machine whenever a ticket gets assigned to you. Your server will run on localhost, which is invisible to the outside world.
ngrok solves this by creating a public URL that tunnels traffic to your local port. In this step, you'll install ngrok, confirm Claude Code's worktree feature works in your repo, and prepare your .gitignore to keep worktree files out of version control.
In this step, get ready to:
- Install and configure ngrok with your authtoken.
- Verify Claude Code can spawn a worktree in your projects-app repo.
- Add worktree paths to .gitignore to keep your repo clean.
Install ngrok and configure your authtoken
ngrok creates a secure tunnel from a public URL to your local machine. Linear will use this tunnel to deliver webhook events to your server.
- Press Cmd+Space to open your search bar.
- Type Terminal and press Enter to open it.
- Install ngrok via Homebrew by running this command:
brew install ngrok
Seeing "already installed"?
That's fine. Homebrew is telling you ngrok is already on your system. Continue to the next step.
If you see a different error, make sure Homebrew itself is installed and up to date. Help me troubleshoot my brew install
- Sign up for a free ngrok account at https://ngrok.com/signup.
- After signing in, copy your authtoken from https://dashboard.ngrok.com/get-started/your-authtoken.
- Configure ngrok with your authtoken by running this command, replacing your-authtoken-here with the token you just copied:
ngrok config add-authtoken "your-authtoken-here"
What does this command do?
This saves your authtoken to ngrok's configuration file so it can authenticate with ngrok's servers. You only need to do this once.
Before you run the next command, what do you think you'll see if ngrok is working correctly?
- Verify ngrok works by starting a tunnel:
ngrok http 3000
✔️ I see a Forwarding URL
You should see a line like:
Forwarding https://abc123.ngrok-free.app -> http://localhost:3000
This confirms ngrok is tunnelling traffic from that public URL to your local port 3000.
- Press Ctrl+C to stop ngrok for now.
ⓧ I see an error or nothing happens
- If you see an authentication error, double-check that you ran ngrok config add-authtoken with the correct token from your dashboard.
- If the command is not found, close and reopen Terminal, then try again.
- Help me fix my ngrok setup
Verify Claude Code worktree support
Claude Code's --worktree flag creates an isolated copy of your repo on a separate branch. Each ticket will get its own worktree so multiple assignments can run in parallel without conflicts.
- Navigate to your projects-app repo by running:
cd ~/Documents/projects-app
- Test Claude Code's worktree feature by running:
claude --worktree test-setup -p "Say hello and list the files in this directory"
What does this command do?
The --worktree test-setup flag tells Claude Code to create a worktree at .claude/worktrees/test-setup/ on a branch called worktree-test-setup.
The -p flag runs Claude non-interactively with the given prompt, skipping any trust checks. This is the same mode your automation will use later.
Claude will run in the worktree and exit after completing the prompt. You should see output showing Claude listing files in the directory.
- Clean up the test worktree by running:
git worktree remove .claude/worktrees/test-setup
Why clean up?
Non-interactive Claude Code runs with -p do not clean up their worktrees automatically. You remove test worktrees manually with git worktree remove.
🙋♀️ Claude Code command not found?
Make sure Claude Code is installed and accessible from your terminal. If you recently installed it, close and reopen your terminal session.
Help me troubleshoot Claude Code in my terminal
Add worktrees to .gitignore
Worktree contents would show up as untracked files in your repo without a .gitignore entry. Adding the worktree path now prevents clutter from future automation runs.
- Open or create the file ~/Documents/projects-app/.gitignore in your text editor.
- Add this line to the file (if it is not already present):
.claude/worktrees/
What does this line do?
This tells Git to ignore everything inside the .claude/worktrees/ directory. Each worktree Claude creates will live there without polluting your git status output.
- Save the file.
- Confirm the line is present by running:
cat ~/Documents/projects-app/.gitignore
You should see .claude/worktrees/ listed in the output.
✔️ Awesome, I've got everything!
Great. Double check you've saved the file.
ⓧ I'd like to double check the full code
Your .gitignore in ~/Documents/projects-app/ should contain at minimum:
.claude/worktrees/
Not seeing the line in the output?
Make sure you saved the file after editing. Check that the path in your cat command points to the correct location: ~/Documents/projects-app/.gitignore.
Help me fix my .gitignore setup
Your tools are ready. Next up, you'll build the webhook listener that receives events from Linear when tickets change.
Build the Webhook Listener
Your tunnel and worktree tools are ready to go. Now you need something for Linear to actually talk to.
For Linear to notify your machine about ticket assignments, it needs an HTTP endpoint to send data to. In this step, you'll build a local server that receives and logs webhook payloads, proving the communication channel works before you wire up any automation logic.
In this step, get ready to:
- Initialize a new Node.js project with Express.
- Create a webhook endpoint that receives and logs POST requests.
- Test the server locally with a simulated webhook payload.
Initialize the project and create the server
Every Node.js project starts with a directory and a package.json file that tracks dependencies. You'll scaffold this project, install Express (a lightweight HTTP server framework), then write the server code.
- Create your project directory and move into it by running these commands:
mkdir ~/Documents/linear-agent && cd ~/Documents/linear-agent
- Initialize a new Node.js project by running:
npm init -y
You should see a JSON object printed in your terminal showing the generated package.json contents.
- Install Express as a dependency by running:
npm install express
You should see output confirming packages were added, ending with a line like added X packages.
Seeing permission errors?
If npm install fails with EACCES or permission denied, try running it with sudo npm install express. This can happen if your global npm directory has restrictive permissions.
help me fix npm permission errors on macOS
- Create a .gitignore file to keep generated files out of version control by running:
printf "node_modules/\n.env\n" > .gitignore
This prevents the bulky node_modules/ folder and any future .env secrets from being tracked by Git.
Now for the main event. You'll create the server file that listens for incoming webhook requests.
- Create the server.js file by running this command:
touch server.js
- Confirm the file was created by running:
ls
You should see server.js listed alongside package.json and node_modules.
- Open server.js in your code editor. If you're using VS Code, run:
code server.js
- Copy the code block below by clicking the copy icon in its top-right corner.
- Paste the code into the empty file by pressing Cmd+V (macOS) or Ctrl+V (Windows).
const express = require("express");
// Create the Express app and enable JSON body parsing
const app = express();
app.use(express.json());
const PORT = 3000;
// Handle incoming webhook POST requests
app.post("/webhook", (req, res) => {
res.status(200).send("OK");
console.log("Received webhook payload:", JSON.stringify(req.body, null, 2));
});
// Start the server
app.listen(PORT, () => {
console.log(`Linear webhook listener running on port ${PORT}`);
});
What does this code do?
- express.json() is middleware that automatically parses JSON request bodies so you can read them as JavaScript objects.
- The POST /webhook route immediately responds with 200 OK (so Linear knows the request was received), then logs the full payload.
- Responding quickly is important because webhook senders like Linear will retry if they don't get a response within a few seconds.
- Save server.js.
File not saving in the right place?
Make sure server.js is saved directly inside ~/Documents/linear-agent/ (the same folder as your package.json). If it ends up in a subfolder, the server won't find your installed packages.
help me check my file is in the right directory
✔️ Awesome, I've got everything!
Great. Make sure you've saved server.js before moving on.
ⓧ I'd like to double check the full code
Here's what your server.js should look like:
const express = require("express");
const app = express();
app.use(express.json());
const PORT = 3000;
app.post("/webhook", (req, res) => {
res.status(200).send("OK");
console.log("Received webhook payload:", JSON.stringify(req.body, null, 2));
});
app.listen(PORT, () => {
console.log(`Linear webhook listener running on port ${PORT}`);
});
And your .gitignore:
node_modules/
.env
Run and test locally
Before involving Linear or ngrok, you should confirm the server starts and can handle an incoming POST request on its own.
- Start your server by running:
node server.js
You should see this message printed:
Linear webhook listener running on port 3000
- Open a second terminal window. Press Cmd+T (macOS) to open a new tab, or Cmd+N for a new window.
Before you send the test request, what do you expect to see in the first terminal where your server is running?
- In your second terminal, simulate a webhook payload by running:
curl -X POST http://localhost:3000/webhook -H "Content-Type: application/json" -d '{"action":"update","type":"Issue","data":{"title":"Test ticket"}}'
Switch back to your first terminal (where the server is running). You should see the payload logged:
Received webhook payload: {
"action": "update",
"type": "Issue",
"data": {
"title": "Test ticket"
}
}
What just happened?
You simulated exactly what Linear will do in the next step. Linear sends a POST request containing JSON data about the event (like a ticket being updated). Your server received it, responded with 200 OK, and printed the contents.
🙋♀️ Don't see the payload logged?
- Make sure the first terminal still shows Linear webhook listener running on port 3000 and hasn't crashed.
- Check that your curl command targets http://localhost:3000/webhook (not just / or a different port).
- If you see Connection refused, the server isn't running. Go back to the first terminal and run node server.js again.
my server isn't logging the webhook payload when I send a curl request
Your webhook listener is alive and logging payloads. Next up, you'll connect it to the real internet with ngrok and tell Linear to start sending events to it.
Connect Linear to Your Listener
Your webhook listener can receive HTTP requests on your machine, but Linear has no way to send traffic to localhost. External services need a public URL to deliver webhooks.
You will use ngrok to create a tunnel that exposes your local server to the internet, then configure Linear to push issue events through that tunnel to your listener.
In this step, get ready to:
- Expose your local server through an ngrok tunnel.
- Create a webhook in Linear pointing to your public URL.
- Confirm real ticket events reach your server.
Expose your server with ngrok
Your server needs to be running before ngrok can forward traffic to it.
- In your terminal, make sure you are in the linear-agent folder by running:
cd ~/Documents/linear-agent
- Start your webhook listener by running:
node server.js
You should see Linear webhook listener running on port 3000 printed in the terminal.
- Open a second terminal window.
- Start the ngrok tunnel by running:
ngrok http 3000
ngrok prints a console UI showing that it is online and forwarding traffic. Look for the Forwarding line that shows a public HTTPS URL (for example, https://abc123.ngrok-free.app).
- Copy this HTTPS Forwarding URL. You will paste it into Linear in the next substep.
What is ngrok doing here?
ngrok opens a secure outbound connection from your machine to the ngrok cloud. It assigns a public URL and routes any incoming requests on that URL through the tunnel down to your localhost:3000.
This means Linear can send HTTP POST requests to the public ngrok URL, and they arrive at your Express server as if they were sent directly to localhost.
🙋♀️ ngrok not starting?
If you see an authentication error, make sure you ran ngrok config add-authtoken with your token in Step 1.
If port 3000 is not reachable, confirm node server.js is still running in your first terminal.
help me troubleshoot ngrok not starting on my machine
Create the webhook in Linear
Now you will tell Linear to send issue events to your public ngrok URL whenever something changes.
- In Linear, click on your workspace name in the top left corner to open the menu.
- Click Settings.
- In the Settings sidebar, find and click API.
- Click New webhook.
- In the URL field, paste your ngrok HTTPS Forwarding URL followed by /webhook (for example, https://abc123.ngrok-free.app/webhook).
- In the Label field, enter Claude Code Agent.
- Under resource types, make sure Issue is selected.
- Click Create webhook.
Why does this require admin access?
Only workspace admins can create webhooks in Linear. If you do not see the webhook option, confirm you have admin permissions in your workspace settings.
🙋♀️ Don't see the API option in settings?
The API section only appears for workspace admins. Ask your workspace admin to grant you admin access, or have them create the webhook on your behalf.
help me find the webhook settings in Linear
Test with a real assignment
The webhook is live. Time to confirm that assigning a ticket in Linear actually delivers the payload to your server.
- In Linear, create a new issue with any title (for example, Test webhook delivery).
- Assign the issue to yourself.
Before you look at your terminal, what do you expect to see logged by your server?
- Switch to the terminal where node server.js is running.
You should see the webhook payload printed as JSON, including fields like action, type, data (containing the issue title, description, assigneeId), and updatedFrom (showing previous values for the fields that changed).
What is updatedFrom?
Linear's webhook payload includes an updatedFrom object that holds the previous values of any fields that changed in this update. In the next step, you will compare data.assigneeId with updatedFrom.assigneeId to detect when a ticket is newly assigned to you.
🙋♀️ No payload appearing in your terminal?
Make sure both node server.js and ngrok http 3000 are still running. If either process stopped, restart it.
Confirm the webhook URL in Linear matches your current ngrok Forwarding URL exactly, including the /webhook path at the end.
Try assigning a different issue. The webhook triggers on issue updates, so re-assigning an already-assigned issue to yourself again may not fire a new event.
help me debug why my Linear webhook is not reaching my local server
Your local server is now receiving real-time events from Linear through the ngrok tunnel. Next up, you will filter these events to detect only YOUR assignments and spawn Claude Code in an isolated worktree.
Spawn Claude Code on Assignment
Your server receives every issue event from Linear, but most of those events are noise. Someone renaming a ticket, changing a label, or adjusting a due date all trigger webhooks that have nothing to do with you.
In this step, you will filter events down to only the ones that matter: the moment a ticket gets assigned to YOU. When that happens, your server will spawn Claude Code in an isolated git worktree with the ticket context so it can start working immediately.
In this step, get ready to:
- Get your Linear user ID via the GraphQL API.
- Add assignment filtering and Claude Code spawning to your server.
- Test the full automation by assigning yourself a ticket.
Get your Linear user ID
Your server needs to know which user ID is yours so it can ignore assignments to other people. You will query the Linear GraphQL API with a personal API key to retrieve your ID.
- In Linear, click the gear icon to open Settings.
- Select Account, then Security & Access.
- Scroll down to the Personal API keys section.
- Click Create key.
- Give the key a label like webhook-agent and select the Read permission.
- Copy the key and save it somewhere secure. You will not be able to see it again.
Why Read permission only?
You only need this key to look up your own user ID. Granting the minimum permission keeps your API key's access surface small.
- Query your user ID by running this command in a new terminal tab (replace YOUR_API_KEY with the key you just copied):
curl -s -X POST https://api.linear.app/graphql -H "Authorization: YOUR_API_KEY" -H "Content-Type: application/json" -d '{"query":"{ viewer { id name } }"}' | python3 -m json.tool
You should see a JSON response containing your id (a UUID string) and your name. Copy the id value. You will paste it into your server code next.
Seeing an authorization error?
- Make sure you replaced YOUR_API_KEY with the actual key string (no quotes around it beyond what is already in the command).
- Confirm the key has Read permission enabled.
- help me debug my Linear API key authorization error
Add assignment filtering and Claude Code spawning
Now you will rewrite your server to detect when a ticket is assigned to you and automatically spawn Claude Code in a worktree. Stop your running server first.
- In the terminal running node server.js, press Ctrl+C to stop the server.
- Open server.js in your editor. Replace the entire contents with the following code, starting with the imports and configuration:
const express = require("express");
const { spawn } = require("child_process");
const path = require("path");
const app = express();
app.use(express.json());
const PORT = 3000;
const MY_USER_ID = "your-linear-user-id-here";
const PROJECTS_APP_PATH = path.join(
process.env.HOME,
"Documents",
"projects-app"
);
What does this code do?
- spawn from child_process lets you launch external commands (like the claude CLI) as child processes.
- MY_USER_ID is the UUID you just retrieved. The server uses it to filter out events that target other people.
- PROJECTS_APP_PATH points to the repo where Claude Code will create worktrees.
- Replace "your-linear-user-id-here" with the UUID you copied from the API response.
- Below the constants, add the webhook handler with assignment filtering:
app.post("/webhook", (req, res) => {
res.status(200).send("OK");
const { action, type, data, updatedFrom } = req.body;
console.log(`Received event: ${action} ${type}`);
if (action !== "update" || type !== "Issue") {
return;
}
const wasAssignedToMe =
data.assigneeId === MY_USER_ID &&
updatedFrom &&
updatedFrom.assigneeId !== MY_USER_ID;
if (!wasAssignedToMe) {
return;
}
How does the filtering work?
Linear sends a webhook for every issue change. Your filter checks three things: the event is an update action, it is an Issue type, and the assigneeId just changed to your ID.
The updatedFrom field tells you what the previous values were. By comparing updatedFrom.assigneeId with data.assigneeId, you detect the exact moment assignment changes to you.
- Continue in the same handler. Add the Claude Code spawn logic directly below the filtering code:
console.log(`Spawning Claude Code for: ${data.title}`);
const ticketId = data.identifier || `ticket-${Date.now()}`;
const worktreeName = ticketId.toLowerCase().replace(/[^a-z0-9-]/g, "-");
const prompt = `You have been assigned this ticket: ${data.title}. ${data.description || ''}. Please work on it.`;
const claude = spawn(
"claude",
[
"--worktree",
worktreeName,
"-p",
prompt,
"--allowedTools",
"Read,Edit,Bash(git *),Bash(npm *),Bash(node *),Bash(cat *),Bash(ls *),Bash(mkdir *)",
],
{
cwd: PROJECTS_APP_PATH,
stdio: "inherit",
env: { ...process.env },
}
);
What does the spawn call do?
- --worktree creates a fresh git worktree under .claude/worktrees/<name>/ so each ticket gets its own isolated branch.
- -p runs Claude Code non-interactively with the given prompt. It skips trust checks in this mode.
- --allowedTools pre-approves the listed tools so Claude does not stop to ask for permission. The trailing * enables prefix matching (e.g. Bash(git *) allows any command starting with git ).
- Finally, add the event listeners and the app.listen call to close out the file:
claude.on("close", (code) => {
console.log(`Claude Code finished for ${ticketId} with exit code ${code}`);
});
claude.on("error", (err) => {
console.error(`Failed to spawn Claude Code: ${err.message}`);
});
});
app.listen(PORT, () => {
console.log(`Linear webhook listener running on port ${PORT}`);
console.log(`Watching for assignments to user: ${MY_USER_ID}`);
console.log(`Will spawn Claude Code in: ${PROJECTS_APP_PATH}`);
});
What do the event listeners do?
- The close event fires when Claude Code finishes its work. The exit code tells you if it succeeded (0) or errored.
- The error event catches failures to even launch the process (e.g. if the claude binary is not found).
- Save server.js.
- Start the server again to confirm it launches without errors:
node server.js
You should see three log lines: the listener running message, your user ID, and the projects-app path.
Seeing a syntax error on start?
- Check that every opening brace, bracket, and parenthesis has a matching closing one. The handler ends with }); (closing the app.post callback) and the file ends with another }); (closing app.listen).
- Confirm you pasted each chunk in order with nothing duplicated or missing.
- help me fix the syntax error in my server.js
✔️ Awesome, I've got everything!
Great. Double check you have saved server.js and that the three startup log lines appear in your terminal.
ⓧ I'd like to double check the full code
Your server.js should contain these sections in order. Scroll through your file and confirm each is present:
- Imports: express, spawn from child_process, path.
- Constants: PORT, MY_USER_ID (your UUID), PROJECTS_APP_PATH.
- Webhook handler (app.post("/webhook", ...)) with event destructuring, action/type guard, wasAssignedToMe check, spawn call, and close/error listeners.
- app.listen with three console.log lines.
Test the automation end-to-end
Everything is wired up. Time to assign a real ticket and watch the automation fire.
- Confirm ngrok http 3000 is still running in your other terminal. If not, restart it and update the webhook URL in Linear (your ngrok URL changes on each restart with the free plan).
- In Linear, create a new issue with a title like Add a health check endpoint and a brief description.
Before you assign the ticket, think about what you expect to see in your server terminal. Will you see the full webhook payload like before, or something different?
- Assign the issue to yourself.
Within seconds, your server terminal should log two things: Received event: update Issue followed by Spawning Claude Code for: Add a health check endpoint. Claude Code is now running in a worktree.
- Verify the worktree was created by opening a new terminal tab and running:
cd ~/Documents/projects-app && git worktree list
You should see your main worktree plus a new one under .claude/worktrees/ with a branch name matching your ticket identifier.
Not seeing the spawn log?
- Check that you pasted your actual Linear user ID into the MY_USER_ID constant. A mismatch means the filter rejects every event.
- Look at the Received event: line. If you see create Issue instead of update Issue, you assigned the ticket at creation time. Try assigning an existing unassigned ticket instead.
- If the ngrok terminal shows no requests, your webhook URL may have gone stale. Copy the current Forwarding URL from ngrok and update it in Linear under Settings > API > your webhook.
- help me debug why my Linear webhook is not triggering Claude Code
Your automation now spawns Claude Code whenever you get a ticket. But notice the prompt you are sending: it just says "please work on it" with no structure. In the next step, you will craft a prompt that makes Claude think before it codes.
Craft the Autonomous Agent Prompt
In the previous step, your automation spawned Claude Code with a bare-bones prompt that just said "work on it." The result is unfocused. Claude jumps straight into coding without analyzing the ticket or documenting its assumptions.
A structured prompt changes everything. By instructing Claude to analyze, plan, implement, and commit in distinct phases, you get a reviewable PLAN.md before any code is written. You come back to a branch you can actually evaluate.
In this step, get ready to:
- Replace the simple prompt with a structured multi-section template via a buildPrompt function.
- Create a .worktreeinclude file so worktrees automatically receive environment files.
- Test with a realistic ticket and confirm Claude produces a PLAN.md and aligned code changes.
Design the structured prompt template
The current prompt gives Claude no structure. You will extract prompt construction into its own function that formats ticket details and a step-by-step workflow for Claude to follow.
- In server.js, find this line inside the /webhook handler:
const prompt = `You have been assigned this ticket: ${data.title}. ${data.description || ''}. Please work on it.`;
- Replace that entire line with a call to the new buildPrompt function:
const prompt = buildPrompt(data);
Why a separate function?
Extracting prompt logic into buildPrompt keeps the webhook handler focused on routing. It also makes the prompt easy to iterate on without touching request-handling code.
- Now add the buildPrompt function below the closing }); of the app.post handler and above the app.listen call. Copy this code:
function buildPrompt(issueData) {
const title = issueData.title || "Untitled";
const description = issueData.description || "No description provided.";
const priority = issueData.priority || "No priority set";
const identifier = issueData.identifier || "unknown";
return `You have been assigned Linear ticket ${identifier}: "${title}"
## Ticket Details
- Title: ${title}
- Description: ${description}
- Priority: ${priority}
## Your Instructions
You are an autonomous coding agent. Follow these steps in order:
What does this first section do?
The function extracts four fields from the issue data, providing sensible defaults if any are missing. The opening of the prompt template gives Claude the ticket identity and structured details to reference.
- Continue the template literal (still inside the same return statement) with the instruction steps:
1. ANALYZE: Read the ticket carefully. Understand what is being asked.
2. PLAN: Create a file called PLAN.md at the root of this worktree with:
- A summary of what the ticket is asking for
- Any assumptions you are making (things not specified in the ticket)
- A numbered list of implementation steps
- Any questions you would ask the developer if you could (note these but proceed anyway)
3. IMPLEMENT: Write the code changes described in your plan.
- Follow existing code patterns and conventions in this repo
- Keep changes minimal and focused on the ticket scope
- If tests exist, make sure they still pass
4. COMMIT: Stage and commit your changes with a descriptive commit message referencing ${identifier}.
Do not ask questions. Make reasonable assumptions and document them in PLAN.md.`;
}
What do these four phases achieve?
- ANALYZE forces Claude to read before acting, preventing premature code.
- PLAN produces a reviewable PLAN.md so you can see Claude's reasoning and assumptions before checking any code.
- IMPLEMENT constrains scope to the ticket and existing patterns.
- COMMIT leaves a clean git history referencing the ticket identifier.
- Save server.js.
Seeing a syntax error when you save?
Make sure the template literal opens with a backtick on the return line and closes with a backtick followed by a semicolon on the Do not ask questions... line. The entire prompt is one continuous template string.
Also confirm the function sits between the app.post handler's closing }); and the app.listen call.
Help me fix a syntax error in my buildPrompt function
Add a .worktreeinclude for environment files
Git worktrees are fresh checkouts that only contain tracked files. If your projects-app repo uses a .env file, Claude's worktree would be missing it.
A .worktreeinclude file tells Claude Code to copy specific gitignored files into every new worktree automatically.
- Create the .worktreeinclude file in your projects-app repo root by running:
echo ".env" > ~/Documents/projects-app/.worktreeinclude
- Confirm the file was created by running:
cat ~/Documents/projects-app/.worktreeinclude
You should see .env printed on a single line.
What does .worktreeinclude do?
It uses .gitignore syntax. Any gitignored file matching a pattern in .worktreeinclude gets copied from your main checkout into the new worktree. This ensures Claude's isolated environment has the same config as your main branch.
🙋♀️ Don't have a .env in projects-app?
That is fine. The .worktreeinclude file won't break anything if the listed file doesn't exist yet. It only copies files that are actually present in your main checkout.
Help me understand .worktreeinclude
Test with a realistic ticket
Time to see the structured prompt in action. You will create a ticket with enough context for Claude to produce a meaningful plan.
- Return to the terminal running your server. Stop it with Ctrl+C, then restart by running:
node server.js
- Make sure ngrok http 3000 is still running in its own terminal. If you restarted ngrok since last step, update the webhook URL in Linear Settings > API with the new Forwarding URL.
- In Linear, create a new issue with this title: Add rate limiting to the API endpoints.
- Add a description like: "Limit each user to 100 requests per minute. Return a 429 status code when exceeded. Add a Retry-After header."
- Assign the ticket to yourself.
Before you check the terminal, predict what you expect to see differently this time compared to the simple prompt from Step 4.
- Watch your server terminal. You should see Spawning Claude Code for: Add rate limiting to the API endpoints followed by Claude's output as it works.
- Wait for Claude to finish. You will see Claude Code finished for [ticket-id] with exit code 0 in the terminal.
Claude may take 1-2 minutes
The structured prompt asks Claude to analyze, plan, and implement. This takes longer than the naive prompt but produces significantly better results.
- Once Claude finishes, check the worktree by running this in a new terminal:
cd ~/Documents/projects-app/.claude/worktrees
ls
You should see a folder named after the ticket identifier (something like proj-123).
- Navigate into the worktree and check for the plan file:
cd ~/Documents/projects-app/.claude/worktrees/<ticket-name>
cat PLAN.md
You should see a structured document with a summary of the ticket, listed assumptions, numbered implementation steps, and questions Claude would have asked.
- Check the commit history to confirm Claude committed its work:
git log --oneline
You should see one or more commits with messages referencing the ticket identifier. The code changes align with the plan documented in PLAN.md.
No PLAN.md in the worktree?
- Check the exit code logged in your server terminal. A non-zero exit code means Claude encountered an error during execution.
- Verify the template literal in buildPrompt is one continuous string with no broken backticks. The prompt text must reach Claude intact.
- Make sure your projects-app repo has a clean git state (no uncommitted changes on the main branch) so the worktree can be created without conflicts.
Help me debug why PLAN.md wasn't created
✔️ Awesome, I've got everything!
Great. Confirm you have saved server.js and that your .worktreeinclude file exists at ~/Documents/projects-app/.worktreeinclude.
ⓧ I'd like to double check the full code
Scroll up and confirm your server.js contains these pieces in order:
- The require statements for express, child_process, and path.
- The PORT, MY_USER_ID, and PROJECTS_APP_PATH constants.
- The app.post("/webhook", ...) handler with assignment filtering, const prompt = buildPrompt(data);, and the spawn call.
- The buildPrompt function returning a template with ANALYZE, PLAN, IMPLEMENT, and COMMIT sections.
- The app.listen call at the bottom.
Also confirm ~/Documents/projects-app/.worktreeinclude exists and contains .env on a single line.
Your automation now produces structured, reviewable output. Every ticket assignment generates a plan you can evaluate before even reading the code. In the Secret Mission, you will close the loop by posting that plan back to Linear so your whole team can see it.
Secret mission
Post the Plan Back to Linear
Your automation spawns Claude Code and leaves results in a worktree branch. But you still have to manually check the worktree to see what Claude decided. What if the implementation plan posted itself back to the Linear ticket as a comment, so anyone on your team could see what the agent is doing without leaving Linear?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs locally with a free ngrok account, so there are no ongoing costs while your server and tunnel are stopped.
Resources you used:
- Local Node.js project at ~/Documents/linear-agent.
- Linear webhook labeled "Claude Code Agent" in Settings > API.
- ngrok installed via Homebrew.
- Git worktrees created in ~/Documents/projects-app/.claude/worktrees/.
- Files added to ~/Documents/projects-app/ (.gitignore entry and .worktreeinclude).
Keep everything running
No action needed. Choose this if you want the automation to keep triggering whenever you get assigned Linear tickets.
- Leave node server.js and ngrok http 3000 running in their terminal windows.
- Your ngrok URL changes each time you restart ngrok on the free plan. You will need to update the webhook URL in Linear Settings > API after each restart.
- The ngrok free account remains active with no charges.
Pause - I'll come back to this later
Stop the running processes to free up terminal sessions. The Linear webhook will fail silently until you restart them. No cost while stopped.
- Press Ctrl+C in the terminal running node server.js to stop the webhook listener.
- Press Ctrl+C in the terminal running ngrok http 3000 to stop the tunnel.
- Your project files stay intact at ~/Documents/linear-agent. When you are ready to resume, start the server and ngrok again, then update the webhook URL in Linear with the new ngrok Forwarding URL.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow each step below.
- Stop the server and ngrok if they are still running (Ctrl+C in each terminal).
- Delete the linear-agent project folder by running:
rm -rf ~/Documents/linear-agent
- Remove any Claude Code worktrees created during the project:
rm -rf ~/Documents/projects-app/.claude/worktrees/
- Remove the .worktreeinclude file from your projects-app repo:
rm ~/Documents/projects-app/.worktreeinclude
- Open ~/Documents/projects-app/.gitignore and remove the .claude/worktrees/ line you added earlier.
- Remove the webhook in Linear. Open Settings, then API. Find the webhook labeled "Claude Code Agent" and delete it.
- Uninstall ngrok from your machine by running:
brew uninstall ngrok
- Your free ngrok account has no ongoing cost and does not need to be deleted.
Nice Work!
Nice Work!
Nice work! You just built a fully autonomous developer automation that bridges project management with AI-driven coding.
You learned how to:
- Built a local webhook listener with ngrok tunneling that receives real-time issue events from Linear the moment they happen.
- Filtered incoming payloads to detect when tickets are assigned specifically to you, ignoring all other team activity.
- Spawned Claude Code in isolated git worktrees with a structured autonomous prompt that analyzes tickets, documents assumptions in PLAN.md, and implements code changes on isolated branches.
- Secret Mission: Posted the implementation plan back to the Linear ticket as a comment via the GraphQL API, closing the feedback loop for your whole team.
Ready to quiz yourself?