What are Git Worktrees?
Git worktrees allow you to have multiple working directories attached to the same repository. Each worktree can have a different branch checked out, enabling you to work on multiple features simultaneously without switching branches.
Think of worktrees as separate workspaces for the same Git repository. Each workspace has its own files and branch, but they all share the same Git history and objects.
Why Uzi Uses Worktrees
Uzi leverages Git worktrees to provide:
1. Complete Isolation
Each agent works in its own worktree, preventing conflicts:
# Agent "sarah" works in her own directory
~ /.local/share/uzi/worktrees/sarah-myapp-a1b2c3d-1234/
# Agent "john" works independently
~ /.local/share/uzi/worktrees/john-myapp-a1b2c3d-5678/
2. Branch Independence
Each worktree has its own branch:
Base branch : Automatically detected from git symbolic-ref refs/remotes/origin/HEAD
Agent branch : Unique branch created for each agent
No switching : Agents don’t interfere with your current working directory
3. Parallel Development
Multiple agents can work simultaneously on different features without any coordination or locking.
Worktree Creation Process
When you launch an agent with uzi prompt, here’s what happens:
Step 1: Generate Unique Names
Uzi creates unique identifiers for the worktree:
// From prompt.go:180-185
timestamp := time . Now (). Unix ()
uniqueId := fmt . Sprintf ( " %d - %d " , timestamp , i )
branchName := fmt . Sprintf ( " %s - %s - %s - %s " , randomAgentName , projectDir , gitHash , uniqueId )
worktreeName := fmt . Sprintf ( " %s - %s - %s - %s " , randomAgentName , projectDir , gitHash , uniqueId )
Example : sarah-myapp-a1b2c3d-1709472000-0
Step 2: Create Worktree Directory
Uzi ensures the worktrees directory exists:
// From prompt.go:197-201
worktreesDir := filepath . Join ( homeDir , ".local" , "share" , "uzi" , "worktrees" )
if err := os . MkdirAll ( worktreesDir , 0755 ); err != nil {
log . Error ( "Error creating worktrees directory" , "error" , err )
}
worktreePath := filepath . Join ( worktreesDir , worktreeName )
Step 3: Add Git Worktree
Uzi creates the worktree with its own branch:
// From prompt.go:206-212
cmd := fmt . Sprintf ( "git worktree add -b %s %s " , branchName , worktreePath )
cmdExec := exec . CommandContext ( ctx , "sh" , "-c" , cmd )
if err := cmdExec . Run (); err != nil {
log . Error ( "Error creating git worktree" , "command" , cmd , "error" , err )
}
This runs: git worktree add -b {branchName} {worktreePath}
The -b flag creates a new branch and checks it out in the worktree simultaneously.
Worktree Storage Location
All worktrees are stored in a centralized location:
~/.local/share/uzi/
├── worktrees/
│ ├── sarah-myapp-a1b2c3d-1234/ # Agent sarah's worktree
│ │ ├── .git # Git metadata (linked to main repo)
│ │ └── [project files]
│ ├── john-myapp-a1b2c3d-5678/ # Agent john's worktree
│ └── emily-myapp-a1b2c3d-9012/ # Agent emily's worktree
├── worktree/
│ └── agent-myapp-a1b2c3d-sarah/ # Session metadata
│ └── tree # Current branch name
└── state.json # Global state
Uzi stores additional metadata about each worktree:
// From state.go:169-187
func ( sm * StateManager ) storeWorktreeBranch ( sessionName string ) error {
homeDir , err := os . UserHomeDir ()
agentDir := filepath . Join ( homeDir , ".local" , "share" , "uzi" , "worktree" , sessionName )
if err := os . MkdirAll ( agentDir , 0755 ); err != nil {
return err
}
branchFile := filepath . Join ( agentDir , "tree" )
currentBranch := sm . getCurrentBranch ()
return os . WriteFile ( branchFile , [] byte ( currentBranch ), 0644 )
}
This stores the current branch name in ~/.local/share/uzi/worktree/{sessionName}/tree.
State Tracking
Each worktree’s state is tracked in state.json:
{
"agent-myapp-a1b2c3d-sarah" : {
"git_repo" : "[email protected] :user/myapp.git" ,
"branch_from" : "main" ,
"branch_name" : "sarah-myapp-a1b2c3d-1234" ,
"prompt" : "Add authentication to the API" ,
"worktree_path" : "/home/user/.local/share/uzi/worktrees/sarah-myapp-a1b2c3d-1234" ,
"port" : 3000 ,
"model" : "claude" ,
"created_at" : "2024-03-15T10:30:00Z" ,
"updated_at" : "2024-03-15T10:45:00Z"
}
}
Viewing Worktree Changes
You can see what each agent has changed using uzi ls:
$ uzi ls
AGENT MODEL STATUS DIFF ADDR PROMPT
sarah claude running +45/-12 http://localhost:3000 Add authentication
john codex ready +23/-8 http://localhost:3001 Fix bug in parser
The diff statistics are calculated by:
// From ls.go:53
shellCmdString := "git add -A . && git diff --cached --shortstat HEAD && git reset HEAD > /dev/null"
Use uzi ls -w to watch agent progress in real-time and see their diff stats update.
Checkpointing Worktree Changes
When an agent completes a task, you can merge its changes back:
uzi checkpoint sarah "Implemented authentication"
This process:
Commits changes in the agent’s worktree
Finds the merge base between your current branch and the agent’s branch
Rebases the agent’s commits onto your branch
// From checkpoint.go:146-152
rebaseCmd := exec . CommandContext ( ctx , "git" , "rebase" , agentBranchName )
rebaseCmd . Dir = currentDir
if err := rebaseCmd . Run (); err != nil {
return fmt . Errorf ( "error rebasing agent changes: %w " , err )
}
Cleanup
When you kill an agent with uzi kill, Uzi removes:
The Git worktree
The Git branch
The worktree metadata directory
The state entry
// From kill.go:56-62
removeCmd := exec . CommandContext ( ctx , "git" , "worktree" , "remove" , "--force" , worktreeInfo . WorktreePath )
if err := removeCmd . Run (); err != nil {
log . Error ( "Error removing git worktree" , "path" , worktreeInfo . WorktreePath , "error" , err )
}
deleteBranchCmd := exec . CommandContext ( ctx , "git" , "branch" , "-D" , agentName )
Worktree removal is permanent. Make sure to checkpoint any changes you want to keep before killing an agent.
Best Practices
Keep Your Main Branch Clean
Always work from a stable base branch (main/master). Uzi automatically detects your default branch:
// From state.go:57-72
func ( sm * StateManager ) getBranchFrom () string {
cmd := exec . Command ( "git" , "symbolic-ref" , "refs/remotes/origin/HEAD" )
output , err := cmd . Output ()
if err != nil {
return "main" // Fallback to main
}
// Extract branch name from refs/remotes/origin/HEAD
}
Checkpoint Regularly
Checkpoint agent work frequently to:
Integrate incremental progress
Test changes in your main environment
Reduce the risk of losing work
Clean Up Old Agents
Remove completed or failed agents to free up disk space:
# Kill a specific agent
uzi kill sarah
# Kill all agents for the current repo
uzi kill all
Limitations
Disk space : Each worktree contains a full copy of your project files
Same repository : All worktrees must be from the same Git repository
No nested worktrees : You can’t create a worktree inside another worktree
Next Steps
Sessions Learn how Tmux sessions manage agent environments
Checkpoint Command Deep dive into merging agent changes