Worktree namespaces

This is an opinionated naming convention that I build into the tool. It is entirely optional — you can use slice wt (or its alias slice branch) to create a worktree with any path you like, and the rest of the tool works the same. But I like this convention enough to build shortcuts for it, and here is my pitch for why you might like it too.

The problem

I found that in a bare+worktrees layout I accumulate worktrees quickly — a feature branch, a throwaway experiment, a colleague's PR I'm reviewing. After a week they all look the same: a flat list of directories with no indication of which ones I can safely blow away and which ones have uncommitted work I care about.

The convention

I use three prefixes — local/, tmp/, and pr/ — to signal my intent for each worktree:

  • tmp/ — stuff I don't intend to keep. Quick experiments, ideas I launched a coding agent to run down, things I tried and forgot about. Not necessarily safe to delete right now — an agent might still be running — but this is work I haven't invested care into and don't plan to maintain.
  • local/ — work I plan to ship. These are my in-progress feature branches. I should clean them up after they merge, but deleting one prematurely means losing real work, so slice rm prompts for confirmation.
  • pr/ — someone else's code. I fetched a pull request to review it locally. There are no local commits here, just a detached snapshot of the remote branch. Safe to delete in bulk once I'm done reviewing.

The distinction between tmp/ and local/ is the one I care about most. It lets me bucket "here are throwaway ideas I launched a coding agent to run down for me" separately from "here are things I've put thought and care and attention into." That difference matters to me when I'm cleaning up, and it matters when I'm demoing to colleagues — they can immediately tell whether I'm showing a throwaway prototype or something I plan to ship.

Shared branches are naturally top-level

Branches that everyone on the team uses — main, dev — sit at the top level as siblings of these directories. Most teams have a small fixed number of shared branches, two or three, so they don't add noise. My project directory looks like this:

~/src/foo/
    +clone+/
    +state+/
    main/           shared branch
    dev/            shared branch
    local/          my in-progress work (may have several inside)
    tmp/            throwaway experiments (may have many inside)
    pr/             reviews (come and go)

The top-level entry count stays bounded no matter how many worktrees I have open, because my worktrees grow inside subdirectories rather than alongside the shared branches.

Why this is visible, not hidden

I like my software to be easily inspectable. Worktrees are plain directories, not tucked into a dotfile or a database. ls is my UI for seeing what workstreams exist, and du -sh */ tells me where disk is going. There is no metadata to query; the filesystem is the source of truth.

Why "local" and not my name

I could use my name as the prefix — steve/feature-one — and everything would work fine via slice wt. I chose local because it means the same thing on every machine and every project without leaking a personal name into branch listings. If you prefer a name-based convention, slice wt is right there.

Why it matters

I find the convention pays off in three situations:

  1. Coming back after a week. I open the project directory and the layout tells me what I was doing. The tmp/ stuff I can probably ignore. The local/ stuff is my active work. The pr/ entries are reviews I haven't finished.

  2. Cleanup. The naming makes cleanup mechanical. tmp/ is almost certainly safe to delete. pr/ has no local commits and can be re-fetched. local/ is the only category that requires judgment. This is what makes slice gc possible — it can confidently target tmp/ worktrees without asking me about each one.

  3. Demos and pairing. When I'm sharing my screen, a colleague can immediately see the difference between a throwaway prototype and something I've put thought into. It manages expectations without me having to explain.

The same clarity carries over into git itself — git branch shows local/feature-auth, tmp/try-sqlx, pr/4521 — so the signal follows me into any tool that lists branches, not just the filesystem.

Creating worktrees

slice local, slice tmp, and slice pr are shortcuts that create worktrees following this convention. The directory path and the git branch name are always the same.

slice local

Create a worktree for your own work.

slice local <name> [-b <ref>] [--from <state>] [hook] [args...]
$ slice local feature-auth                      # branch local/feature-auth from HEAD
$ slice local feature-auth -b main              # branch from main
$ slice local feature-auth -b v2.1.0            # branch from a tag

slice tmp

Create a throwaway worktree. Same mechanics as slice local, but signals that this branch is disposable.

slice tmp <name> [-b <ref>] [--from <state>] [hook] [args...]
$ slice tmp try-sqlx                            # quick experiment
$ slice tmp fix-login --from current claude -p "fix the redirect"

Throwaway worktrees are cleaned up with slice rm tmp/<name> or in bulk with slice gc.

slice pr checkout

Fetch a pull request and create a worktree for reviewing it.

slice pr checkout <number> [--from <state>] [hook] [args...]
$ slice pr checkout 4521
$ slice pr checkout 4521 --from template-clean review

Internally this runs:

git fetch origin pull/<number>/head:pr/<number>
git worktree add pr/<number> pr/<number>

The --from flag

When creating a worktree, --from specifies which state to bind it to:

--from template-clean     # clone the template-clean state directory
--from current            # clone whatever state the current worktree is using
--from dev-signed-in      # clone a specific named state

When --from is used, a new state directory is created as a clone of the source, the worktree is bound to the new state, and (if a hook is provided) everything is started.

When --from is omitted, the worktree is created but no state is attached. You'll need to run slice use <state> before slice up.

Hooks on creation

If a hook name is provided after the flags, it's executed after the worktree and state are set up. Everything after the hook name is passed through as arguments.

slice tmp fix-login --from current claude -p "fix the redirect bug"
#                                  ^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^
#                                  hook   args ($@ inside the hook)

The hook must be defined in hooks.new-worktree in your config. See Hooks.

Branch naming

The worktree path is the branch name. This gives you a clean branch namespace for free:

$ git branch
  dev
  local/feature-auth
  local/refactor-db
* main
  pr/4521
  tmp/fix-login
  tmp/try-sqlx

At a glance you know whose branches are whose, what's disposable, and what's under review. Top-level names (main, dev) track shared remote branches. Namespaced names are local work.

Removing worktrees

slice rm <worktree> [--yes|-y]

This stops any running processes, removes the git worktree, deletes the branch, and removes any throwaway state that was created with --from during worktree creation.

$ slice rm tmp/fix-login         # clean up throwaway work
$ slice rm pr/4521               # done reviewing
$ slice rm local/feature-auth    # prompts for confirmation

Before removing any worktree, slice rm checks whether the working tree is clean and the branch tip exists on at least one remote (git branch -r --contains). If both hold, removal proceeds silently. Otherwise it shows the reasons and prompts for confirmation. Pass --yes (-y) to skip. The remote check looks at all configured remotes, not just the tracking remote.

Garbage collection

slice gc

Removes all tmp/ worktrees that aren't currently running. This is the "my laptop is getting cluttered" escape valve after firing off several agents.