Skip to content

Instantly share code, notes, and snippets.

@jrejaud
Created February 22, 2026 16:20
Show Gist options
  • Select an option

  • Save jrejaud/5d8c0faa22ecc861d98a07a469f27664 to your computer and use it in GitHub Desktop.

Select an option

Save jrejaud/5d8c0faa22ecc861d98a07a469f27664 to your computer and use it in GitHub Desktop.
Claude Code worktree workflow - Linear integration + MCP intelligence layer
#!/usr/bin/env zx
import {chalk, fs, $, argv, path} from "zx"
import {checkbox} from "@inquirer/prompts"
/**
* Worktree Cleanup Script
*
* This script safely removes a git worktree after work is complete.
* It performs these steps:
* 1. Verifies the worktree exists
* 2. Checks if the branch has been merged (prevents accidental data loss)
* 3. Syncs any new Claude Code permissions back to parent repo
* 4. Removes the worktree directory
* 5. Prunes git worktree references
* 6. Cleans up ~/.claude.json trust entry
* 7. Deletes the git branch
*
* Safety features:
* - Won't delete unmerged branches without confirmation or --force
* - Interactive permission sync (choose which to keep)
* - Cleans up all traces of the worktree
*/
// Parse arguments
const branchName = argv._[0]
const force = argv.force || false
if (!branchName) {
console.error(chalk.red("Error: Branch name is required"))
console.log(chalk.yellow("Usage: npm run worktree-cleanup <branch-name> [--force]"))
console.log(chalk.yellow(" --force: Force delete even if branch is not merged"))
process.exit(1)
}
const projectRoot = process.cwd()
const worktreePath = path.join(projectRoot, ".trees", branchName)
// Determine master branch from package.json
const masterBranch = (() => {
try {
const packageJson = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8"))
const masterBranch = packageJson.masterBranch
if (!masterBranch) {
console.error(chalk.red("Error: masterBranch not defined in package.json"))
process.exit(1)
}
return masterBranch
} catch (error) {
console.error(chalk.red(`Error: Could not read package.json: ${error.message}`))
process.exit(1)
}
})()
console.log(chalk.blue(`Cleaning up worktree for branch: ${branchName}`))
console.log(chalk.gray(`Worktree path: ${worktreePath}`))
// Step 1: Check if worktree exists
console.log(chalk.blue("\n1. Checking worktree exists..."))
if (!fs.existsSync(worktreePath)) {
console.error(chalk.red(` ✗ Worktree not found at ${worktreePath}`))
process.exit(1)
}
console.log(chalk.green(" ✓ Worktree exists"))
// Step 2: Check if branch is merged into master
console.log(chalk.blue("\n2. Checking if branch is merged..."))
try {
await $`git fetch origin ${masterBranch}`
} catch (error) {
console.error(chalk.red(` ✗ Failed to fetch origin/${masterBranch}: ${error.message}`))
process.exit(1)
}
let isMerged = false
try {
const mergedBranches = await $`git branch --merged origin/${masterBranch}`
isMerged = mergedBranches.stdout.includes(branchName)
} catch (error) {
console.log(chalk.yellow(` ⚠ Could not check merge status: ${error.message}`))
}
if (!isMerged) {
if (!force) {
console.log(chalk.yellow(` ⚠ Branch '${branchName}' is NOT merged into ${masterBranch}`))
// Check if running in interactive mode
if (process.stdin.isTTY) {
const readline = await import("readline")
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
const answer = await new Promise((resolve, reject) => {
try {
rl.question(chalk.yellow(" Continue anyway? (y/N) "), resolve)
} catch (error) {
reject(error)
}
})
rl.close()
if (!answer.toLowerCase().startsWith("y")) {
console.log(chalk.gray(" Aborted."))
process.exit(1)
}
} else {
console.error(chalk.red(" ✗ Branch not merged and not in interactive mode. Use --force to override."))
process.exit(1)
}
} else {
console.log(chalk.yellow(" ⚠ Branch not merged, but --force specified. Continuing..."))
}
} else {
console.log(chalk.green(` ✓ Branch is merged into ${masterBranch}`))
}
// Step 3: Sync permissions back to parent
// During development in the worktree, you may have approved new permissions for Claude.
// This step copies those new permissions back to the parent repo so they're available
// in future worktrees and regular development sessions.
console.log(chalk.blue("\n3. Syncing permissions back to parent..."))
const parentSettings = path.join(projectRoot, ".claude", "settings.local.json")
const worktreeSettings = path.join(worktreePath, ".claude", "settings.local.json")
if (fs.existsSync(worktreeSettings) && fs.existsSync(parentSettings)) {
try {
const parent = JSON.parse(fs.readFileSync(parentSettings, "utf8"))
const worktree = JSON.parse(fs.readFileSync(worktreeSettings, "utf8"))
const parentAllow = new Set(parent?.permissions?.allow || [])
const worktreeAllow = worktree?.permissions?.allow || []
// Find new permissions that worktree has but parent doesn't
const newPermissions = worktreeAllow.filter(p => !parentAllow.has(p))
if (newPermissions.length === 0) {
console.log(chalk.gray(" No new permissions to sync"))
} else if (!process.stdin.isTTY) {
console.log(chalk.yellow(` ⚠ Non-interactive session — skipping permission sync (${newPermissions.length} new permissions not synced)`))
} else {
// Interactive: let user pick which permissions to sync
console.log(chalk.gray(` Found ${newPermissions.length} new permission(s) in worktree\n`))
const permissionsToSync = await checkbox({
message: "Select permissions to sync to parent:",
choices: newPermissions.map(p => ({name: p, value: p}))
})
if (permissionsToSync.length > 0) {
parent.permissions = parent.permissions || {}
parent.permissions.allow = [...parentAllow, ...permissionsToSync]
fs.writeFileSync(parentSettings, JSON.stringify(parent, null, 2))
console.log(chalk.green(` ✓ Added ${permissionsToSync.length} new permissions to parent:`))
permissionsToSync.forEach(p => console.log(chalk.gray(` - ${p}`)))
} else {
console.log(chalk.gray(" No permissions selected — skipping sync"))
}
}
} catch (error) {
console.log(chalk.yellow(` ⚠ Warning: Failed to sync permissions: ${error.message}`))
}
} else {
console.log(chalk.gray(" No worktree settings to sync"))
}
// Step 4: Remove worktree directory
console.log(chalk.blue("\n4. Removing worktree directory..."))
try {
await $`rm -rf ${worktreePath}`
console.log(chalk.green(" ✓ Worktree directory removed"))
} catch (error) {
console.error(chalk.red(` ✗ Failed to remove worktree directory: ${error.message}`))
process.exit(1)
}
// Step 5: Prune git worktree references
console.log(chalk.blue("\n5. Pruning git worktree references..."))
try {
await $`git worktree prune`
console.log(chalk.green(" ✓ Git worktree references pruned"))
} catch (error) {
console.log(chalk.yellow(` ⚠ Warning: Failed to prune worktrees: ${error.message}`))
}
// Step 6: Clean up ~/.claude.json trust entry
console.log(chalk.blue("\n6. Cleaning up ~/.claude.json trust entry..."))
const claudeJsonPath = path.join(process.env.HOME, ".claude.json")
if (fs.existsSync(claudeJsonPath)) {
try {
const config = JSON.parse(fs.readFileSync(claudeJsonPath, "utf8"))
if (config.projects && config.projects[worktreePath]) {
delete config.projects[worktreePath]
fs.writeFileSync(claudeJsonPath, JSON.stringify(config, null, 2))
console.log(chalk.green(" ✓ Removed trust entry for worktree"))
} else {
console.log(chalk.gray(" No trust entry to remove"))
}
} catch (error) {
console.log(chalk.yellow(` ⚠ Warning: Failed to clean up trust entry: ${error.message}`))
}
} else {
console.log(chalk.gray(" No ~/.claude.json found"))
}
// Step 7: Delete the branch
console.log(chalk.blue("\n7. Deleting branch..."))
try {
if (force) {
await $`git branch -D ${branchName}`
} else {
await $`git branch -d ${branchName}`
}
console.log(chalk.green(` ✓ Branch '${branchName}' deleted`))
} catch (error) {
console.error(chalk.red(` ✗ Failed to delete branch: ${error.message}`))
console.log(chalk.yellow(" Manual fix: git branch -D " + branchName))
process.exit(1)
}
// Done!
console.log(chalk.green.bold("\n✓ Worktree cleanup complete!"))
#!/usr/bin/env zx
import {chalk, fs, $, argv, path} from "zx"
/**
* Worktree Setup Script for Claude Code Development
*
* This script automates the setup of git worktrees for parallel feature development.
* It creates an isolated git worktree and configures it with:
* - Project dependencies (node_modules)
* - Environment files
* - Claude Code permissions (auto-allow edits)
* - MCP server configs (Serena targets this worktree, not parent)
* - Build caches (shared via symlinks for speed)
*
* Why worktrees? Work on multiple features simultaneously without branch-switching conflicts.
*/
// Parse arguments
const branchName = argv._[0]
const installOnly = argv.install || false
const copyOnly = argv.copy || false
const force = argv.force || false
const baseBranchOverride = argv["base-branch"] || null
if (!branchName) {
console.error(chalk.red("Error: Branch name is required"))
console.log(chalk.yellow("Usage: npm run worktree-setup <branch-name> [--install] [--copy] [--force] [--base-branch=<branch>]"))
console.log(chalk.yellow(" --install: Run npm install only (skip copy)"))
console.log(chalk.yellow(" --copy: Copy node_modules only (skip npm install)"))
console.log(chalk.yellow(" --force: Force reinstall/copy node_modules even if they already exist"))
console.log(chalk.yellow(" --base-branch: Base branch to create new branch from (default: masterBranch from package.json)"))
process.exit(1)
}
const projectRoot = process.cwd()
const worktreePath = path.join(projectRoot, ".trees", branchName)
// Determine base branch from package.json or --base-branch flag
const baseBranch = (() => {
if (baseBranchOverride) return baseBranchOverride
try {
const packageJson = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8"))
const masterBranch = packageJson.masterBranch
if (!masterBranch) {
console.error(chalk.red("Error: masterBranch not defined in package.json"))
console.log(chalk.yellow("Please add 'masterBranch' field to package.json or use --base-branch flag"))
process.exit(1)
}
return masterBranch
} catch (error) {
console.error(chalk.red(`Error: Could not read package.json: ${error.message}`))
process.exit(1)
}
})()
console.log(chalk.blue(`Setting up worktree for branch: ${branchName}`))
console.log(chalk.gray(`Worktree path: ${worktreePath}`))
console.log(chalk.gray(`Base branch: ${baseBranch}`))
// Step 1: Verify worktree exists, create if needed
console.log(chalk.blue("\n1. Checking worktree..."))
if (!fs.existsSync(worktreePath)) {
console.log(chalk.yellow(" Worktree does not exist, creating it..."))
try {
// Ensure .trees directory exists
const treesDir = path.join(projectRoot, ".trees")
if (!fs.existsSync(treesDir)) {
fs.mkdirSync(treesDir, {recursive: true})
}
// Check if branch exists
let branchExists = false
try {
await $`git show-ref --verify --quiet refs/heads/${branchName}`
branchExists = true
} catch {
// Branch doesn't exist, will create it
}
// Create branch if it doesn't exist
if (!branchExists) {
console.log(chalk.yellow(` Branch '${branchName}' does not exist, creating it from ${baseBranch}...`))
try {
await $`git branch ${branchName} ${baseBranch}`
console.log(chalk.green(` ✓ Created branch: ${branchName} from ${baseBranch}`))
} catch (error) {
console.error(chalk.red(` ✗ Failed to create branch: ${error.message}`))
process.exit(1)
}
}
// Create the worktree
await $`git worktree add ${worktreePath} ${branchName}`
console.log(chalk.green(` ✓ Created worktree for branch: ${branchName}`))
} catch (error) {
console.error(chalk.red(` ✗ Failed to create worktree: ${error.message}`))
console.log(chalk.yellow(` Manual fix: git worktree add .trees/${branchName} ${branchName}`))
process.exit(1)
}
} else {
console.log(chalk.green("✓ Worktree exists"))
}
// Step 2: Handle .env files (symlink most, copy .env.vault)
// Why symlinks? Changes to .env in main repo auto-propagate to all worktrees.
// Why copy .env.vault? Git sees symlinks as file type changes, which creates noise in diffs.
console.log(chalk.blue("\n2. Setting up .env files..."))
const envDirs = [".", "server", "client"]
for (const dir of envDirs) {
const sourceDir = path.join(projectRoot, dir)
const targetDir = path.join(worktreePath, dir)
if (!fs.existsSync(sourceDir)) {
console.log(chalk.yellow(` ⚠ Directory not found: ${dir}/`))
continue
}
const envFiles = fs.readdirSync(sourceDir).filter(f => f.startsWith(".env"))
if (envFiles.length === 0) {
console.log(chalk.gray(` No .env files in ${dir}/`))
continue
}
for (const envFile of envFiles) {
const sourcePath = path.join(sourceDir, envFile)
const targetPath = path.join(targetDir, envFile)
try {
// Remove existing file/symlink if it exists
if (fs.existsSync(targetPath)) {
fs.unlinkSync(targetPath)
}
// Copy .env.vault files to avoid git seeing them as type changes
// Symlink all other .env files
if (envFile === ".env.vault") {
fs.copyFileSync(sourcePath, targetPath)
console.log(chalk.green(` ✓ Copied ${dir}/${envFile}`))
} else {
fs.symlinkSync(sourcePath, targetPath)
console.log(chalk.green(` ✓ Symlinked ${dir}/${envFile}`))
}
} catch (error) {
console.error(chalk.red(` ✗ Failed to setup ${dir}/${envFile}: ${error.message}`))
process.exit(1)
}
}
}
// Step 3: Copy node_modules (unless --install flag is set)
// Why copy? Faster than npm install (~30s vs ~5min for large projects).
// The copy gets you 95% there, then Step 4's npm install fixes version mismatches.
if (!installOnly) {
// Check if node_modules already exists in worktree
const rootNodeModules = path.join(worktreePath, "node_modules")
const nodeModulesExists = fs.existsSync(rootNodeModules)
if (nodeModulesExists && !force) {
console.log(chalk.blue("\n3. Skipping node_modules copy (already exists, use --force to override)"))
} else {
console.log(chalk.blue("\n3. Copying node_modules directories..."))
const moduleDirs = [".", "server", "client", "shared"]
for (const dir of moduleDirs) {
const sourceModules = path.join(projectRoot, dir, "node_modules")
const targetModules = path.join(worktreePath, dir, "node_modules")
if (!fs.existsSync(sourceModules)) {
console.log(chalk.gray(` No node_modules in ${dir}/`))
continue
}
console.log(chalk.gray(` Copying ${dir}/node_modules...`))
try {
// Remove target if it exists
if (fs.existsSync(targetModules)) {
await $`rm -rf ${targetModules}`
}
// Copy with rsync for efficiency
await $`rsync -aq ${sourceModules}/ ${targetModules}/`
console.log(chalk.green(` ✓ Copied ${dir}/node_modules`))
} catch (error) {
console.log(chalk.yellow(` ⚠ Warning: Failed to copy ${dir}/node_modules: ${error.message}`))
}
}
}
} else {
console.log(chalk.gray("\n3. Skipping node_modules copy (--install flag set)"))
}
// Step 4: Run npm install (unless --copy flag is set)
if (!copyOnly) {
// Check if node_modules already exists in worktree
const rootNodeModules = path.join(worktreePath, "node_modules")
const nodeModulesExists = fs.existsSync(rootNodeModules)
if (nodeModulesExists && !force) {
console.log(chalk.blue("\n4. Skipping npm install (node_modules already exists, use --force to override)"))
} else {
console.log(chalk.blue("\n4. Running npm install to fix version mismatches..."))
try {
// Change to worktree directory
process.chdir(worktreePath)
// Run npm install in root
console.log(chalk.gray(" Installing root dependencies..."))
await $`npm install`
console.log(chalk.green(" ✓ Root dependencies installed"))
} catch (error) {
console.error(chalk.red(` ✗ npm install failed: ${error.message}`))
console.log(chalk.yellow(" Manual fix: cd to worktree and run 'npm install'"))
process.exit(1)
} finally {
// Change back to project root
process.chdir(projectRoot)
}
}
} else {
console.log(chalk.gray("\n4. Skipping npm install (--copy flag set)"))
}
// Step 5: Copy .claude/settings.local.json from parent and ensure Edit/Write permissions
// Git-tracked .claude/ files (commands, hooks, skills) already exist from git worktree add.
// Only settings.local.json is gitignored and needs to be copied.
//
// Auto-add Edit:** and Write:** permissions so Claude can modify files without prompting.
// Worktrees are for active development, so we want permissive defaults.
console.log(chalk.blue("\n5. Setting up .claude/settings.local.json..."))
const sourceSettings = path.join(projectRoot, ".claude", "settings.local.json")
const targetSettings = path.join(worktreePath, ".claude", "settings.local.json")
// Start with parent settings or empty object
let settings = {}
if (fs.existsSync(sourceSettings)) {
try {
fs.copyFileSync(sourceSettings, targetSettings)
settings = JSON.parse(fs.readFileSync(targetSettings, "utf8"))
const permCount = settings?.permissions?.allow?.length || 0
console.log(chalk.green(` ✓ Copied settings.local.json (${permCount} permissions)`))
} catch (error) {
console.log(chalk.yellow(` ⚠ Warning: Failed to copy settings.local.json: ${error.message}`))
}
} else {
console.log(chalk.yellow(" ⚠ No settings.local.json found in parent, creating new one"))
}
// Ensure Edit:** and Write:** permissions exist (auto-allow edits in worktrees)
const requiredPerms = ["Edit:**", "Write:**"]
if (!settings.permissions) settings.permissions = {}
if (!Array.isArray(settings.permissions.allow)) settings.permissions.allow = []
const added = []
for (const perm of requiredPerms) {
if (!settings.permissions.allow.includes(perm)) {
settings.permissions.allow.unshift(perm)
added.push(perm)
}
}
if (added.length > 0) {
try {
fs.writeFileSync(targetSettings, JSON.stringify(settings, null, 2))
console.log(chalk.green(` ✓ Added permissions: ${added.join(", ")}`))
} catch (error) {
console.log(chalk.yellow(` ⚠ Warning: Failed to write permissions: ${error.message}`))
}
} else {
console.log(chalk.gray(" Edit/Write permissions already present"))
}
// Step 6: Install Husky and copy internal files
console.log(chalk.blue("\n6. Installing Husky git hooks..."))
try {
// Copy .husky/_/ directory which contains husky.sh (gitignored)
const sourceHuskyInternal = path.join(projectRoot, ".husky", "_")
const targetHuskyInternal = path.join(worktreePath, ".husky", "_")
if (fs.existsSync(sourceHuskyInternal)) {
if (!fs.existsSync(targetHuskyInternal)) {
fs.mkdirSync(targetHuskyInternal, {recursive: true})
}
// Copy husky.sh and other internal files
await $`cp -r ${sourceHuskyInternal}/* ${targetHuskyInternal}/`
console.log(chalk.green(" ✓ Copied Husky internal files"))
}
process.chdir(worktreePath)
await $`npx husky install`
console.log(chalk.green(" ✓ Husky hooks installed"))
} catch (error) {
console.error(chalk.red(` ✗ Husky install failed: ${error.message}`))
console.log(chalk.yellow(" Manual fix: cd to worktree and run 'npx husky install'"))
process.exit(1)
} finally {
process.chdir(projectRoot)
}
// Step 7: Symlink .turbo/ cache directory
// Why symlink? Turbo is the monorepo build tool. Sharing the cache across worktrees
// means builds stay fast even when switching between worktrees.
console.log(chalk.blue("\n7. Symlinking .turbo/ cache directory..."))
const sourceTurboDir = path.join(projectRoot, ".turbo")
const targetTurboDir = path.join(worktreePath, ".turbo")
if (!fs.existsSync(sourceTurboDir)) {
console.log(chalk.yellow(" ⚠ .turbo/ directory not found (will be created on first build)"))
} else {
try {
// Remove existing directory/symlink if it exists
if (fs.existsSync(targetTurboDir)) {
if (fs.lstatSync(targetTurboDir).isSymbolicLink()) {
fs.unlinkSync(targetTurboDir)
} else {
await $`rm -rf ${targetTurboDir}`
}
}
// Create symlink
fs.symlinkSync(sourceTurboDir, targetTurboDir)
console.log(chalk.green(" ✓ Symlinked .turbo/ directory"))
} catch (error) {
console.log(chalk.yellow(` ⚠ Warning: Failed to symlink .turbo/: ${error.message}`))
}
}
// Step 8: Setup MCP configuration (inherit from parent + add worktree-specific Serena)
//
// THE KEY INTELLIGENCE LAYER:
// - Inherits all MCP servers from parent (Postgres, Linear, etc.)
// - Adds worktree-specific Serena config that targets THIS worktree's path
// - This ensures Serena's code intelligence reads/indexes the worktree code, not the parent repo
//
// Without this, Serena would analyze the wrong codebase!
console.log(chalk.blue("\n8. Setting up MCP configuration..."))
// Create .mcp.json by inheriting from parent project and adding worktree-specific config
const mcpConfigPath = path.join(worktreePath, ".mcp.json")
// Start with worktree-specific Serena config
const worktreeSerenaConfig = {
"serena-worktree": {
type: "stdio",
command: "uv",
args: [
"run",
"--directory",
path.join(process.env.HOME, "Development/serena"),
"serena",
"start-mcp-server",
"--context",
"ide-assistant",
"--project",
worktreePath
],
env: {}
}
}
// Read parent's .mcp.json if it exists
let parentMcpJsonServers = {}
const parentMcpJsonPath = path.join(projectRoot, ".mcp.json")
if (fs.existsSync(parentMcpJsonPath)) {
try {
const parentMcpJson = JSON.parse(fs.readFileSync(parentMcpJsonPath, "utf8"))
if (parentMcpJson.mcpServers) {
parentMcpJsonServers = parentMcpJson.mcpServers
console.log(chalk.green(` ✓ Found ${Object.keys(parentMcpJsonServers).length} MCP server(s) in parent .mcp.json`))
}
} catch (error) {
console.log(chalk.yellow(` ⚠ Warning: Failed to read parent .mcp.json: ${error.message}`))
}
} else {
console.log(chalk.gray(" No parent .mcp.json found"))
}
// Read parent project's MCP servers from ~/.claude.json
let parentClaudeConfigServers = {}
const userClaudeConfigPath = path.join(process.env.HOME, ".claude.json")
if (fs.existsSync(userClaudeConfigPath)) {
try {
const userClaudeConfig = JSON.parse(fs.readFileSync(userClaudeConfigPath, "utf8"))
if (userClaudeConfig.projects && userClaudeConfig.projects[projectRoot]) {
const projectConfig = userClaudeConfig.projects[projectRoot]
if (projectConfig.mcpServers) {
parentClaudeConfigServers = projectConfig.mcpServers
console.log(chalk.green(` ✓ Found ${Object.keys(parentClaudeConfigServers).length} MCP server(s) in ~/.claude.json for parent project`))
}
}
} catch (error) {
console.log(chalk.yellow(` ⚠ Warning: Failed to read ~/.claude.json: ${error.message}`))
}
} else {
console.log(chalk.gray(" No ~/.claude.json found"))
}
// Merge all MCP servers: parent .mcp.json + parent ~/.claude.json + worktree serena
// Order matters: later entries override earlier ones, so worktree serena takes precedence
const mcpConfig = {
mcpServers: {
...parentMcpJsonServers, // from parent's .mcp.json
...parentClaudeConfigServers, // from ~/.claude.json project entry
...worktreeSerenaConfig // worktree-specific serena (overrides if exists)
}
}
const totalServers = Object.keys(mcpConfig.mcpServers).length
try {
fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2))
console.log(chalk.green(` ✓ Created .mcp.json with ${totalServers} MCP server(s)`))
// List inherited servers for visibility
const inheritedServers = Object.keys(mcpConfig.mcpServers).filter(s => s !== "serena-worktree")
if (inheritedServers.length > 0) {
console.log(chalk.gray(` Inherited: ${inheritedServers.join(", ")}`))
}
} catch (error) {
console.error(chalk.red(` ✗ Failed to create .mcp.json: ${error.message}`))
console.log(chalk.yellow(" Warning: Serena will target main repo instead of worktree"))
}
// Copy .serena/cache directory from main repo (as recommended by Serena docs)
// Why? Serena indexes the entire codebase on first run (~2-5 minutes).
// Copying the cache skips re-indexing since the code is mostly identical.
const sourceSerenaCache = path.join(projectRoot, ".serena", "cache")
const targetSerenaDir = path.join(worktreePath, ".serena")
const targetSerenaCache = path.join(targetSerenaDir, "cache")
if (!fs.existsSync(sourceSerenaCache)) {
console.log(chalk.yellow(" ⚠ .serena/cache not found in main repo (will be created on first use)"))
} else {
try {
// Create .serena directory if it doesn't exist
if (!fs.existsSync(targetSerenaDir)) {
fs.mkdirSync(targetSerenaDir, {recursive: true})
}
// Remove existing cache directory if it exists
if (fs.existsSync(targetSerenaCache)) {
await $`rm -rf ${targetSerenaCache}`
}
// Copy cache using rsync for efficiency
console.log(chalk.gray(" Copying .serena/cache (this may take a moment)..."))
await $`rsync -aq ${sourceSerenaCache}/ ${targetSerenaCache}/`
console.log(chalk.green(" ✓ Copied .serena/cache directory (avoids re-indexing)"))
} catch (error) {
console.log(chalk.yellow(` ⚠ Warning: Failed to copy .serena/cache: ${error.message}`))
}
}
// Step 9: Pre-approve Claude Code trust for worktree
// Claude Code asks "Do you trust this directory?" on first launch.
// Pre-approving it here eliminates that prompt since we know this is our own code.
console.log(chalk.blue("\n9. Pre-approving Claude Code trust for worktree..."))
const claudeConfigPath = path.join(process.env.HOME, ".claude.json")
try {
let claudeConfig = {}
if (fs.existsSync(claudeConfigPath)) {
claudeConfig = JSON.parse(fs.readFileSync(claudeConfigPath, "utf8"))
}
// Ensure projects object exists
if (!claudeConfig.projects) {
claudeConfig.projects = {}
}
// Add or update worktree entry with trust pre-approved
if (!claudeConfig.projects[worktreePath]) {
claudeConfig.projects[worktreePath] = {}
}
claudeConfig.projects[worktreePath].hasTrustDialogAccepted = true
// Write back
fs.writeFileSync(claudeConfigPath, JSON.stringify(claudeConfig, null, 2))
console.log(chalk.green(" ✓ Pre-approved Claude Code trust for worktree"))
} catch (error) {
console.log(chalk.yellow(` ⚠ Warning: Failed to pre-approve trust: ${error.message}`))
console.log(chalk.yellow(" You may see a trust dialog when starting Claude in this worktree"))
}
// Done!
console.log(chalk.green.bold("\n✓ Worktree setup complete!"))
console.log(chalk.blue("\nTo switch to the worktree, run:"))
console.log(chalk.gray(` cd ${worktreePath}`))
#!/bin/bash
set -e
##
## Worktree Start Script - Entry point for worktree-based development
##
## This script:
## 1. Calls worktree-setup.mjs to create and configure the worktree
## 2. Extracts the Linear issue ID from the branch name (e.g., sc-2535-feature -> SC-2535)
## 3. Creates .context/ directory with task tracking files that survive context compaction
## 4. Launches Claude with a prompt to implement the Linear issue
##
## Usage: ./scripts/worktree-start.sh sc-2535-add-feature [base-branch]
##
BRANCH=""
BASE_BRANCH=""
# Parse arguments
for arg in "$@"; do
if [[ -z "$BRANCH" ]]; then
BRANCH="$arg"
elif [[ -z "$BASE_BRANCH" ]]; then
BASE_BRANCH="$arg"
fi
done
if [[ -z "$BRANCH" ]]; then
echo "Usage: ./scripts/worktree-start.sh <branch-name> [base-branch]"
echo " base-branch: Optional. Branch to create from (default: masterBranch from package.json)"
exit 1
fi
if [[ -n "$BASE_BRANCH" ]]; then
npm run worktree-setup "$BRANCH" -- --base-branch="$BASE_BRANCH"
else
npm run worktree-setup "$BRANCH"
fi
cd ".trees/$BRANCH"
# Extract Linear issue ID from branch name (e.g., sc-2535-some-description -> SC-2535)
ISSUE_ID=$(echo "$BRANCH" | grep -oE '^[sS][cC]-[0-9]+' | tr '[:lower:]' '[:upper:]')
# Initialize .context/ for task tracking (survives context compaction)
# These files persist across Claude Code sessions and context compaction, providing continuity.
# - context.md: Links to Linear, tracks key files, decisions, discoveries
# - tasks.md: Checklist of pending/in-progress/completed tasks
# - progress.log: Session timestamps and major milestones
mkdir -p .context
cat > .context/context.md << EOF
# Task: $ISSUE_ID
**Branch:** $BRANCH
**Linear:** https://linear.app/studio-corsair/issue/$ISSUE_ID
**Last Updated:** $(date '+%Y-%m-%d %H:%M')
## Key Files
<!-- Files critical to this task -->
## Decisions Made
<!-- Decisions and rationale -->
## Discoveries
<!-- Things learned during implementation -->
EOF
cat > .context/tasks.md << EOF
# Tasks: $ISSUE_ID
**Last Updated:** $(date '+%Y-%m-%d %H:%M')
## In Progress
## Pending
- [ ] Fetch Linear issue and understand requirements
- [ ] Implement solution
- [ ] Write tests
- [ ] Create PR
## Completed
## Next Steps
1. Read Linear issue $ISSUE_ID
EOF
echo "[$(date '+%Y-%m-%d %H:%M')] SESSION START - $ISSUE_ID" > .context/progress.log
# Select CLI tool based on --mobile flag
if [[ "$USE_MOBILE" == "true" ]]; then
CLI_TOOL="happy"
else
CLI_TOOL="claude"
fi
# Build the Claude command with initial prompt
# The prompt is loaded from ~/.claude/commands/linear-implement.md and customized with the issue ID.
# This tells Claude to read the Linear issue and implement it autonomously.
CLAUDE_CMD="$CLI_TOOL --permission-mode acceptEdits"
if [[ -z "$ISSUE_ID" ]]; then
echo "Warning: Could not extract Linear issue ID from branch name"
else
# Read template from linear-implement.md and substitute issue_id
TEMPLATE_FILE="$HOME/.claude/commands/linear-implement.md"
if [[ -f "$TEMPLATE_FILE" ]]; then
# Extract content after the YAML frontmatter (after second ---)
PROMPT=$(sed -n '/^---$/,/^---$/d; p' "$TEMPLATE_FILE" | sed "s/{{issue_id}}/${ISSUE_ID}/g")
else
PROMPT="Please implement the Linear issue ${ISSUE_ID}."
fi
CLAUDE_CMD="$CLAUDE_CMD \"$PROMPT\""
fi
# Run the CLI tool
eval "$CLAUDE_CMD"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment