libra worktree
Command reference for `libra worktree`
Manage multiple working trees attached to this repository.
Alias: wt
Synopsis
libra worktree add <path>
libra worktree list
libra worktree lock <path> [--reason <text>]
libra worktree unlock <path>
libra worktree move <src> <dest>
libra worktree prune
libra worktree remove <path>
libra worktree umount <path> [--cleanup]
libra worktree repairDescription
libra worktree manages multiple working trees that share a single repository database and object store. This allows you to have several checkouts of the same repository simultaneously, which is useful for working on multiple branches at once, running builds while editing code, or testing changes in isolation.
Each linked worktree is a directory containing a .libra symlink pointing back to the shared storage directory. The main worktree is the original repository directory. All worktrees share the same SQLite database, object store, and configuration.
Worktree metadata is persisted in a worktrees.json file inside the .libra storage directory. Each entry tracks the filesystem path, whether it is the main worktree, its lock status, and an optional lock reason. The state file is written atomically via a temporary file rename to prevent corruption.
When a new worktree is added and HEAD points to a commit, the worktree is automatically populated with the committed content from HEAD (not staged index changes).
Options
Subcommand: add
Create a new linked worktree at the given filesystem path.
| Argument | Description |
|---|---|
<path> | Filesystem path for the new worktree. Can be relative or absolute. The directory is created if it does not exist. Must not be inside .libra storage, must not already be registered, and must be empty if it exists. |
# Create a new worktree for a feature branch
libra worktree add ../my-feature
libra --json worktree add ../my-feature
# Create using absolute path
libra worktree add /tmp/libra-testSubcommand: list
List all registered worktrees and their state. --porcelain emits a stable,
machine-readable format: for each worktree a worktree <path> line, the shared
HEAD <sha> line (when the repository has any commit), and a locked [<reason>]
line when locked, with a blank line between worktrees. Because Libra worktrees
share one HEAD/index/refs, Git's per-worktree branch/detached lines are
intentionally omitted (Libra has no per-worktree HEAD).
libra worktree list
libra worktree list --porcelain
libra --json worktree list
libra --machine worktree listStructured output uses the worktree.list command envelope. Each entry reports
kind, path, is_main, locked, lock_reason, and whether the path currently
exists on disk.
Subcommand: lock
Mark a worktree as locked to prevent it from being pruned or removed.
| Argument / Flag | Description |
|---|---|
<path> | Filesystem path of the worktree to lock. |
--reason | Optional human-readable explanation for why the worktree is locked. |
# Lock a worktree
libra worktree lock ../my-feature
# Lock with a reason
libra worktree lock ../my-feature --reason "long-running experiment"
libra --json worktree lock ../my-feature --reason "long-running experiment"Subcommand: unlock
Remove the lock from a previously locked worktree. Idempotent: unlocking an already-unlocked worktree is a no-op.
| Argument | Description |
|---|---|
<path> | Filesystem path of the worktree to unlock. |
libra worktree unlock ../my-feature
libra --machine worktree unlock ../my-featureSubcommand: move
Move or rename an existing linked worktree. The directory is renamed on disk and the registry is updated. Cannot move the main worktree or a locked worktree.
| Argument | Description |
|---|---|
<src> | Current filesystem path of the worktree. |
<dest> | New filesystem path. Must not already exist on disk or in the registry. Cannot be inside .libra storage. |
libra worktree move ../my-feature ../my-feature-v2
libra --json worktree move ../my-feature ../my-feature-v2Subcommand: prune
Remove worktrees from the registry whose directories no longer exist on disk. The main worktree and locked worktrees are never pruned.
libra worktree prune
libra --machine worktree pruneSubcommand: remove
Unregister a worktree from the state file. By default the directory on disk
is intentionally left untouched to avoid destructive behavior. Pass
--delete-dir for Git-style behavior — the directory is removed only after
a dirty-state check passes. Cannot remove the main worktree or a locked
worktree.
| Argument / Flag | Description |
|---|---|
<path> | Filesystem path of the worktree to unregister. |
--delete-dir | After unregistering, also delete the directory on disk. Refused when the worktree contains uncommitted changes (staged or unstaged). |
# Default — keep the directory on disk
libra worktree remove ../my-feature
libra --json worktree remove ../my-feature
# Git-style — also delete the directory (clean worktree only)
libra worktree remove --delete-dir ../my-feature
libra --machine worktree remove --delete-dir ../my-feature
# Refused when dirty:
$ libra worktree remove --delete-dir ../dirty-feature
fatal: cannot delete dirty worktree '../dirty-feature' (uncommitted changes)
Hint: commit or stash changes, or remove without --delete-dir to keep the directoryBehavior intentionally differs from Git: Git's default deletes the directory.
Libra keeps it by default to prevent accidental data loss; --delete-dir
restores Git-like semantics opt-in. See
COMPATIBILITY.md and
compatibility/worktree-surface.md
for the rationale.
Subcommand: umount
Unmount a FUSE worktree mountpoint. This is primarily useful for cleaning up
stale Agent task worktrees when the operating system reports a path as busy.
The command also accepts a Libra task worktree root and resolves its
workspace mountpoint automatically.
Alias: unmount
| Argument / Flag | Description |
|---|---|
<path> | FUSE mountpoint path, or a Libra task worktree root containing a workspace mountpoint. |
--cleanup | After unmounting, remove the Libra task worktree root. Only task FUSE worktree paths are accepted. |
libra worktree umount /repo/.libra/worktrees/tasks/libra-task-worktree-fuse-29353-id/workspace --cleanup
libra --json worktree umount /repo/.libra/worktrees/tasks/libra-task-worktree-fuse-29353-id --cleanupJSON / machine output envelope:
{
"ok": true,
"command": "worktree.umount",
"data": {
"mountpoint": "/repo/.libra/worktrees/tasks/libra-task-worktree-fuse-29353-id/workspace",
"unmounted": true,
"cleanup_requested": true,
"cleanup_root": "/repo/.libra/worktrees/tasks/libra-task-worktree-fuse-29353-id",
"cleanup_root_removed": true
}
}Subcommand: repair
Repair worktree metadata by removing duplicate entries (same canonical path) and ensuring exactly one main worktree entry exists. Only writes the state file if changes are actually made.
libra worktree repair
libra --json worktree repairCommon Commands
# Create a new worktree
libra worktree add ../experiment
# List all worktrees
libra wt list
# Lock a worktree to protect it
libra wt lock ../experiment --reason "production hotfix in progress"
# Unlock when done
libra wt unlock ../experiment
# Move a worktree to a new location
libra wt move ../experiment ../experiment-v2
# Clean up worktrees whose directories were deleted
libra wt prune
# Unregister a worktree (keeps files on disk)
libra wt remove ../experiment-v2
# Fix inconsistent worktree metadata
libra wt repairHuman Output
worktree add:
/Users/alice/projects/my-featureworktree list:
main /Users/alice/projects/my-repo
worktree /Users/alice/projects/my-feature
worktree /Users/alice/projects/hotfix [locked: production hotfix in progress]worktree remove:
Removed worktree '/Users/alice/projects/my-feature' from registry. Directory kept on disk.
Removed worktree '/Users/alice/projects/my-feature' from registry and deleted directory.worktree prune (with stale entries):
Will prune 2 worktrees:
/Users/alice/projects/old-experiment
/Users/alice/projects/deleted-branch
Pruned 2 worktreesworktree prune (nothing to prune):
No worktrees to pruneJSON Output
worktree add, lock, unlock, move, prune, remove, and repair
use command-specific envelopes. --machine emits the same schemas as compact
single-line JSON.
worktree.add:
{
"ok": true,
"command": "worktree.add",
"data": {
"path": "/Users/alice/projects/my-feature",
"already_exists": false
}
}worktree.list:
{
"ok": true,
"command": "worktree.list",
"data": {
"worktrees": [
{
"kind": "main",
"path": "/Users/alice/projects/my-repo",
"is_main": true,
"locked": false,
"lock_reason": null,
"exists": true
}
]
}
}worktree.lock:
{
"ok": true,
"command": "worktree.lock",
"data": {
"path": "/Users/alice/projects/my-feature",
"locked": true,
"lock_reason": "long-running experiment",
"changed": true
}
}worktree.unlock:
{
"ok": true,
"command": "worktree.unlock",
"data": {
"path": "/Users/alice/projects/my-feature",
"locked": false,
"changed": true
}
}worktree.move:
{
"ok": true,
"command": "worktree.move",
"data": {
"source": "/Users/alice/projects/my-feature",
"destination": "/Users/alice/projects/my-feature-v2",
"registry_updated": true,
"disk_directory_moved": true
}
}worktree.prune:
{
"ok": true,
"command": "worktree.prune",
"data": {
"pruned": ["/Users/alice/projects/old-experiment"],
"pruned_count": 1
}
}worktree.remove:
{
"ok": true,
"command": "worktree.remove",
"data": {
"path": "/Users/alice/projects/my-feature",
"registry_removed": true,
"disk_directory_deleted": false
}
}worktree.repair:
{
"ok": true,
"command": "worktree.repair",
"data": {
"changed": true
}
}Design Rationale
Why JSON-file persistence instead of filesystem links like Git?
Git tracks worktrees through a combination of filesystem structure: the main .git/worktrees/ directory contains per-worktree directories with gitdir, HEAD, and commondir files, and each linked worktree has a .git file (not directory) pointing back. This approach is tightly coupled to Git's file-based architecture and requires careful cross-referencing between multiple locations.
Libra uses a single worktrees.json file in the shared storage directory. This provides several advantages: all worktree metadata is in one queryable location, state is written atomically (via temp-file rename), and the format is trivially inspectable by both humans and AI agents. The symlink from each linked worktree's .libra back to the shared storage is simpler than Git's bidirectional pointer system. The trade-off is that the JSON file is a single point of truth that must be kept consistent, which is why repair exists.
Why --reason on lock?
Git's git worktree lock also supports --reason, and Libra preserves this. Lock reasons are valuable in team environments and when AI agents manage worktrees: they provide context about why a worktree should not be pruned or removed. Without a reason, a locked worktree is opaque, and another user (or agent) cannot determine whether the lock is still relevant. The reason is displayed in list output, making lock status self-documenting.
Why does remove not delete directories on disk?
Deleting files is a destructive operation that cannot be undone. Libra's remove only unregisters the worktree from the JSON state file, leaving the directory intact. This is a deliberate safety choice: the user can inspect and manually delete the directory when they are confident it is no longer needed. This also prevents accidental data loss if a worktree contains uncommitted work. Git's git worktree remove does delete the directory by default, which has been a source of lost work.
Why does move reject locked worktrees?
A locked worktree signals that it should not be modified. Moving it would change its filesystem path, which could break references to that path in other tools, scripts, or agent configurations. The user must explicitly unlock the worktree before moving it, ensuring the action is intentional.
Why does add populate from HEAD instead of the index?
When creating a linked worktree, Libra restores content from the HEAD commit rather than the current index state. This ensures the new worktree reflects the last committed state, not any staged-but-uncommitted changes that exist only in the original worktree's context. This matches user expectations: a new worktree starts from a known good state.
Parameter Comparison: Libra vs Git vs jj
| Operation | Libra | Git | jj |
|---|---|---|---|
| Create worktree | worktree add <path> | worktree add <path> [<branch>] | workspace add <path> |
| Create on branch | Not supported | worktree add <path> <branch> | workspace add <path> (then jj edit) |
| Create detached | Not supported | worktree add --detach <path> <commit> | N/A |
| List worktrees | worktree list | worktree list [--porcelain] | workspace list |
| Lock | worktree lock <path> [--reason] | worktree lock [--reason] <worktree> | N/A |
| Unlock | worktree unlock <path> | worktree unlock <worktree> | N/A |
| Move | worktree move <src> <dest> | worktree move <worktree> <new-path> | N/A |
| Prune | worktree prune | worktree prune [--dry-run] | N/A (automatic) |
| Remove | worktree remove <path> (registry only) | worktree remove [--force] <worktree> (deletes dir) | workspace forget <name> |
| Repair | worktree repair | worktree repair [<path>...] | N/A |
| Alias | wt | N/A | N/A |
| Branch per worktree | Not supported | Automatic (new branch or existing) | Automatic (new working copy commit) |
| Storage | JSON file (worktrees.json) | Filesystem structure (.git/worktrees/) | Operation log |
| Worktree link | Symlink to shared .libra | .git file pointing to gitdir | Symlink to shared .jj |
Note: jj uses the term "workspace" instead of "worktree". Each workspace automatically gets its own working copy commit, and workspaces are tracked in the operation log. jj workspaces are simpler than Git worktrees because jj's change-based model does not require separate branch management per workspace.
Error Handling
| Code | Condition |
|---|---|
LBR-REPO-001 | Not a libra repository |
LBR-REPO-002 | worktrees.json is corrupt |
LBR-CLI-003 | Worktree path cannot be inside .libra storage |
LBR-CLI-003 | Target exists and is not a directory |
LBR-CLI-003 | No such worktree (for lock, unlock, move, remove) |
LBR-CLI-003 | Cannot move or remove main worktree |
LBR-CLI-003 | Cannot move or remove locked worktree |
LBR-CLI-003 | worktree umount --cleanup was requested for a non-task FUSE worktree path |
LBR-CONFLICT-002 | Target directory exists and is not empty |
LBR-CONFLICT-002 | Target already contains a .libra entry |
LBR-CONFLICT-002 | Destination already exists (for move) |
LBR-CONFLICT-002 | Destination already registered as worktree (for move) |
LBR-CONFLICT-002 | --delete-dir refused because the worktree is dirty |
LBR-IO-001 | Failed to read or inspect worktree paths/state/status |
LBR-IO-002 | Failed to write worktrees.json |
LBR-IO-002 | Failed to populate worktree from HEAD |