Skip to main content
The WorktreeService manages Git worktrees in Emdash, enabling each task to run in an isolated Git environment. It handles worktree creation, deletion, file preservation, and integrates with the WorktreePoolService for instant task starts.

Overview

Worktrees are created at ../worktrees/{slugged-name}-{hash} on branch {prefix}/{slugged-name}-{hash}. The branch prefix defaults to emdash and is configurable in app settings. Key features:
  • Isolated environments: Each task gets its own worktree with a unique branch
  • File preservation: Automatically copies gitignored files (.env, .envrc, etc.) from main repo to worktree
  • Configurable patterns: Custom preserve patterns via .emdash.json at project root
  • Safety checks: Prevents accidental removal of main repository
  • Worktree pooling: Integrates with WorktreePoolService for instant worktree claiming

Core Methods

createWorktree

Create a new Git worktree for an agent task.
async createWorktree(
  projectPath: string,
  taskName: string,
  projectId: string,
  baseRef?: string
): Promise<WorktreeInfo>
projectPath
string
required
Absolute path to the main Git repository
taskName
string
required
Human-readable task name (will be slugified for branch/directory names)
projectId
string
required
Database project ID
baseRef
string
Optional base branch override (e.g., origin/develop). Falls back to project settings if not provided.
WorktreeInfo
object
id
string
Stable worktree ID (derived from absolute path hash)
name
string
Original task name
branch
string
Created branch name (e.g., emdash/fix-bug-a7f)
path
string
Absolute path to worktree directory
projectId
string
Associated project ID
status
'active' | 'paused' | 'completed' | 'error'
Worktree status
createdAt
string
ISO 8601 timestamp
Process:
  1. Generates slugified branch name: {prefix}/{slugged-name}-{3-char-hash}
  2. Resolves base ref from project settings or parameter
  3. Fetches latest base ref from remote (with fallback)
  4. Creates worktree via git worktree add -b {branch} {path} {baseRef}
  5. Preserves gitignored files from main repo to worktree
  6. Pushes new branch to remote (if pushOnCreate setting is enabled)
Example:
const worktree = await worktreeService.createWorktree(
  '/Users/dev/my-project',
  'Fix login bug',
  'proj_abc123',
  'origin/main'
);

// Result:
// {
//   id: 'wt-a1b2c3d4e5f6',
//   name: 'Fix login bug',
//   branch: 'emdash/fix-login-bug-a7f',
//   path: '/Users/dev/worktrees/fix-login-bug-a7f',
//   projectId: 'proj_abc123',
//   status: 'active',
//   createdAt: '2024-01-15T10:30:00Z'
// }

removeWorktree

Remove a worktree and delete its branch (local and remote).
async removeWorktree(
  projectPath: string,
  worktreeId: string,
  worktreePath?: string,
  branch?: string
): Promise<void>
projectPath
string
required
Main repository path
worktreeId
string
required
Worktree ID from createWorktree
worktreePath
string
Optional explicit worktree path (used if worktree not in registry)
branch
string
Optional branch name to delete
Safety checks:
  • Verifies worktree path is not the main repository
  • Confirms path is actually a Git worktree (via git worktree list --porcelain)
  • Prevents removal of main worktree
  • Only removes directories matching worktree path patterns
Process:
  1. Validates worktree is safe to remove
  2. Runs git worktree remove --force {path}
  3. Prunes stale worktree metadata: git worktree prune
  4. Deletes local branch: git branch -D {branch}
  5. Deletes remote branch: git push origin --delete {branch} (if remote exists)
  6. Cleans up filesystem directory

listWorktrees

List all worktrees for a project, filtered by managed branch prefixes.
async listWorktrees(projectPath: string): Promise<WorktreeInfo[]>
projectPath
string
required
Main repository path
WorktreeInfo[]
array
Array of worktree metadata objects
Example:
const worktrees = await worktreeService.listWorktrees('/Users/dev/my-project');
// [
//   { id: 'wt-abc', name: 'feature-x', branch: 'emdash/feature-x-a1b', ... },
//   { id: 'wt-def', name: 'bugfix-y', branch: 'emdash/bugfix-y-c2d', ... }
// ]

getWorktreeStatus

Get the current Git status of a worktree (staged, unstaged, untracked files).
async getWorktreeStatus(worktreePath: string): Promise<{
  hasChanges: boolean;
  stagedFiles: string[];
  unstagedFiles: string[];
  untrackedFiles: string[];
}>
worktreePath
string
required
Absolute path to worktree
Example:
const status = await worktreeService.getWorktreeStatus(
  '/Users/dev/worktrees/fix-login-bug-a7f'
);
// {
//   hasChanges: true,
//   stagedFiles: ['src/auth.ts'],
//   unstagedFiles: ['src/login.ts'],
//   untrackedFiles: ['test.log']
// }

File Preservation

preserveProjectFilesToWorktree

Copy gitignored files from main repo to worktree using project-specific or default patterns.
async preserveProjectFilesToWorktree(
  projectPath: string,
  worktreePath: string
): Promise<PreserveResult>
projectPath
string
required
Source directory (main repository)
worktreePath
string
required
Destination directory (worktree)
PreserveResult
object
copied
string[]
Files successfully copied
skipped
string[]
Files skipped (already exist in destination)
Default preserve patterns:
[
  '.env',
  '.env.keys',
  '.env.local',
  '.env.*.local',
  '.envrc',
  'docker-compose.override.yml',
]
Excluded paths (never preserved):
[
  'node_modules',
  '.git',
  'vendor',
  '.cache',
  'dist',
  'build',
  '.next',
  '.nuxt',
  '__pycache__',
  '.venv',
  'venv',
]

Custom Preserve Patterns

Create .emdash.json at project root to customize:
{
  "preservePatterns": [
    ".env",
    ".claude/**",
    "config/*.local.json"
  ]
}
Pattern matching:
  • Uses minimatch for glob patterns
  • Matches against both filename and full relative path
  • Automatically adds **/ prefix for nested matches
  • Dot files (e.g., .env) are matched by default

Worktree Naming

Branch Name Format

{prefix}/{slugged-name}-{hash}
  • Prefix: Configurable via repository.branchPrefix setting (default: emdash)
  • Slugged name: Task name converted to lowercase kebab-case
  • Hash: 3-character alphanumeric hash for uniqueness
Example:
// Task name: "Fix Login Bug"
// Result: "emdash/fix-login-bug-a7f"

Directory Name Format

../worktrees/{slugged-name}-{hash}
Example:
Project: /Users/dev/my-project
Worktree: /Users/dev/worktrees/fix-login-bug-a7f

Stable Worktree IDs

Worktree IDs are generated from the absolute path hash:
private stableIdFromPath(worktreePath: string): string {
  const abs = path.resolve(worktreePath);
  const h = crypto.createHash('sha1').update(abs).digest('hex').slice(0, 12);
  return `wt-${h}`;
}

WorktreePoolService Integration

createWorktreeFromBranch

Create a worktree from an existing branch (used by WorktreePoolService for instant claiming).
async createWorktreeFromBranch(
  projectPath: string,
  taskName: string,
  branchName: string,
  projectId: string,
  options?: { worktreePath?: string }
): Promise<WorktreeInfo>
branchName
string
required
Existing Git branch to checkout
options.worktreePath
string
Optional explicit worktree path (for pool claiming)
Used for:
  • Claiming pre-created worktrees from the pool
  • Restoring worktrees after app restart
  • Checking out existing PR branches

registerWorktree

Register an externally-created worktree (e.g., from WorktreePoolService).
registerWorktree(worktree: WorktreeInfo): void
worktree
WorktreeInfo
required
Worktree metadata to register

Base Branch Resolution

fetchLatestBaseRef

Resolve and fetch the latest base branch for a project.
async fetchLatestBaseRef(
  projectPath: string,
  projectId: string
): Promise<BaseRefInfo>
BaseRefInfo
object
remote
string
Remote name (e.g., origin) or empty string for local-only repos
branch
string
Branch name (e.g., main)
fullRef
string
Full ref (e.g., origin/main or main for local-only)
Resolution order:
  1. Explicit baseRef from project settings (e.g., origin/develop)
  2. Project’s current branch from settings
  3. Remote default branch (via git remote show origin)
  4. Fallback: origin/main (or main for local-only repos)
Local-only repositories: For repos without a remote, the service automatically:
  • Returns empty string for remote
  • Uses local branch names (e.g., main instead of origin/main)
  • Skips remote fetch/push operations
  • Validates local branch existence with git rev-parse

Error Handling

Common Errors

Worktree path already exists:
throw new Error(`Worktree directory already exists: ${worktreePath}`);
Base branch not found:
throw new Error(`Failed to fetch ${baseRef}: remote ref does not exist`);
Attempted to remove main repository:
log.error(`CRITICAL: Prevented removal of main repository! Path: ${pathToRemove}`);
throw new Error('Cannot remove main repository - this is not a worktree');

Error Tracking

Worktree operations are tracked via PostHog telemetry:
await errorTracking.captureWorktreeError(error, 'create', worktreePath, branchName, {
  project_id: projectId,
  project_path: projectPath,
  task_name: taskName,
  hash: hash,
});

Location

Source: src/main/services/WorktreeService.ts Singleton export:
export const worktreeService = new WorktreeService();
  • WorktreePoolService (src/main/services/WorktreePoolService.ts): Pre-creates worktrees for instant task starts
  • DatabaseService (src/main/services/DatabaseService.ts): Persists project/task metadata
  • ProjectSettingsService (src/main/services/ProjectSettingsService.ts): Stores base branch preferences

See Also

Build docs developers (and LLMs) love