What are git worktrees?
Git worktrees allow you to check out multiple branches of the same repository simultaneously, each in its own working directory. Emdash leverages this feature to give each agent task its own isolated workspace.
When you create a task in Emdash, it creates a new git worktree at ../worktrees/{task-name}-{hash} with a corresponding branch named {prefix}/{task-name}-{hash} (where prefix defaults to emdash).
Why Emdash uses worktrees
Complete isolation
Each agent runs in its own worktree, which means:
- No interference between concurrent agent sessions
- Each agent can modify files without affecting other tasks
- Clean separation of changes per task
- Safe parallel execution of multiple agents
Instant context switching
You can switch between tasks without losing any work. Each worktree maintains its own:
- Working directory state
- Staged changes
- Untracked files
- File modifications
Native git integration
Since worktrees are a native git feature, all git operations work seamlessly:
- View diffs between tasks
- Create pull requests directly from task branches
- Merge or rebase task branches
- Push changes to remote
Worktree directory structure
Emdash creates worktrees in a predictable structure:
your-project/
├── .git/
├── src/
└── ...
worktrees/
├── add-authentication-x7k/ # Task worktree
│ ├── .git # Points to main repo
│ ├── src/
│ └── ...
├── fix-login-bug-m3p/ # Another task worktree
└── _reserve-a4f2c9/ # Pre-created reserve (pooling)
The naming convention is {slugified-task-name}-{3-char-hash} where:
- Task names are slugified (lowercase, alphanumeric, hyphens)
- A 3-character hash ensures uniqueness
Worktree pooling
Emdash uses a worktree pool to eliminate the 3-7 second delay of creating worktrees on demand.
How pooling works
-
Pre-creation: When you open a project, Emdash creates a “reserve” worktree in the background at
worktrees/_reserve-{hash} on branch _reserve/{hash}
-
Instant claiming: When you create a task, Emdash:
- Instantly renames the reserve worktree via
git worktree move
- Renames the branch via
git branch -m
- Replenishes the pool in the background
-
Background refresh: Reserves are kept fresh by periodically running
git fetch --all --prune
From WorktreePoolService.ts:211-220:
// Keep reserve refs fresh in the background so claim remains instant.
await this.refreshRefsForReserveCreation(projectPath, projectId);
// Resolve HEAD/local refs to remote tracking refs (freshly fetched)
// so the worktree is created from up-to-date code, not a stale local branch.
const resolvedRef = await this.resolveToRemoteRef(projectPath, baseRef);
// Create the worktree
await execFileAsync('git', ['worktree', 'add', '-b', reserveBranch, reservePath, resolvedRef], {
cwd: projectPath,
});
Reserve lifecycle
- Reserves expire after 30 minutes to prevent stale code
- Stale reserves are cleaned up and recreated automatically
- Orphaned reserves from crashes are cleaned up on app startup
File preservation
Emdash preserves gitignored configuration files from your main repository to each worktree, ensuring agents have access to necessary secrets and config.
Default preserved patterns
From WorktreeService.ts:32-39:
const DEFAULT_PRESERVE_PATTERNS = [
'.env',
'.env.keys',
'.env.local',
'.env.*.local',
'.envrc',
'docker-compose.override.yml',
];
These files are copied (not symlinked) from your main repo to each worktree when it’s created.
Custom preservation patterns
You can configure which files to preserve by creating a .emdash.json file at your project root:
{
"preservePatterns": [
".env*",
".claude/**",
".vscode/settings.json",
"docker-compose.override.yml"
]
}
From WorktreeService.ts:119-141:
/**
* Read .emdash.json config from project root
*/
private readProjectConfig(projectPath: string): EmdashConfig | null {
try {
const configPath = path.join(projectPath, '.emdash.json');
if (!fs.existsSync(configPath)) {
return null;
}
const content = fs.readFileSync(configPath, 'utf8');
return JSON.parse(content) as EmdashConfig;
} catch {
return null;
}
}
/**
* Get preserve patterns for a project (config or defaults)
*/
private getPreservePatterns(projectPath: string): string[] {
const config = this.readProjectConfig(projectPath);
if (config?.preservePatterns && Array.isArray(config.preservePatterns)) {
return config.preservePatterns;
}
return DEFAULT_PRESERVE_PATTERNS;
}
Excluded paths
Certain directories are never preserved to avoid copying large dependencies:
const DEFAULT_EXCLUDE_PATTERNS = [
'node_modules',
'.git',
'vendor',
'.cache',
'dist',
'build',
'.next',
'.nuxt',
'__pycache__',
'.venv',
'venv',
];
Branch management
Branch naming
By default, task branches use the emdash prefix: emdash/task-name-x7k
You can customize this prefix in settings:
const { getAppSettings } = await import('../settings');
const settings = getAppSettings();
const prefix = settings?.repository?.branchPrefix || 'emdash';
branchName = `${prefix}/${sluggedName}-${hash}`;
Automatic push
When you create a task, Emdash automatically:
- Creates the local branch
- Pushes it to
origin with upstream tracking
- Sets up the branch for easy PR creation
This is configurable via settings.repository.pushOnCreate.
From WorktreeService.ts:273-289:
// Push the new branch to origin and set upstream so PRs work out of the box
// Only if a remote exists
if (settings?.repository?.pushOnCreate !== false && fetchedBaseRef.remote) {
try {
await execFileAsync(
'git',
['push', '--set-upstream', fetchedBaseRef.remote, branchName],
{
cwd: worktreePath,
}
);
log.info(
`Pushed branch ${branchName} to ${fetchedBaseRef.remote} with upstream tracking`
);
} catch (pushErr) {
log.warn('Initial push of worktree branch failed:', pushErr as any);
// Don't fail worktree creation if push fails - user can push manually later
}
}
Cleanup
When you delete a task, Emdash:
- Removes the worktree via
git worktree remove --force
- Deletes the local branch via
git branch -D
- Deletes the remote branch via
git push origin --delete
- Removes the worktree directory from disk
Safety checks
Emdash includes multiple safety checks to prevent accidental deletion of your main repository:
// CRITICAL SAFETY CHECK: Prevent removing the main repository
const normalizedPathToRemove = path.resolve(pathToRemove);
const normalizedProjectPath = path.resolve(projectPath);
if (normalizedPathToRemove === normalizedProjectPath) {
log.error(
`CRITICAL: Attempted to remove main repository! Path: ${pathToRemove}, Project: ${projectPath}`
);
throw new Error('Cannot remove main repository - this is not a worktree');
}
Local-only repositories
Emdash fully supports repositories without remotes. For local-only repos:
- Worktrees are created from local branches
- No automatic pushing occurs
- All operations work offline
- Branch cleanup is local-only
You can switch between tasks instantly without worrying about losing work. Each worktree maintains its own state independently.
Never manually delete worktree directories. Always use Emdash’s task deletion or git worktree remove to ensure proper cleanup.