Skip to content
gwmgwmgwmv1.10.0

Worktree lifecycle

Create a worktree and matching branch.

Terminal window
gwm create feat 123 user-authentication
# → branch feat/#123-user-authentication
# → worktree ~/cc-worktree/<repo>/feat-123-user-authentication
gwm create feat 123 foo --no-bootstrap # skip the bootstrap pipeline
Flag Action
--no-bootstrap Skip the .gwm.toml bootstrap stages (copies / guards / commands / hooks)
--reuse-branch Attach to an already-existing local branch of the same name instead of refusing (issue #99)
--skip-hooks <PHASES> Skip the comma-separated lifecycle hook phases (e.g. pre_create,post_create)
--name <NAME> Name the worktree freely instead of the <type> <issue> <desc> triple (issue #416). Exclusive with the positionals
--issue <N> Derive the triple from an issue that already exists on the forge (issue #617). Exclusive with the positionals and with --name
--type <TYPE> With --issue, the branch type to use when the issue’s labels do not select exactly one. --issue only
--force With --issue, open a worktree for a closed issue instead of refusing. --issue only
--repo <NAME> In workspace mode, which child repo gets the worktree, required there to disambiguate; ignored in single-repo mode (issue #36)

By default gwm create refuses to silently reuse a stale local branch: it ends with an error naming the stale tip so you can audit it; --reuse-branch is the opt-in escape hatch.

gwm new covers the issue that does not exist yet. --issue <N> covers the other half: the issue a teammate, a bot, or you last week already filed.

Terminal window
gwm create --issue 594
# → resolving issue #594 on kbrdn1/gwm-cli
# → labels: feature → type: feat
# → branch feat/#594-modals-should-follow-tui-layout

Nothing about the branch is retyped. The two halves come from the issue:

  • <desc> from the title. The branch type’s title_prefix ([Feature]: , [Bug]: ) comes back off, and the rest is normalised through the same kebab-case path a hand-typed <desc> goes through. A long title is truncated on a word boundary rather than mid-word. The prefix is resolved exactly as gwm new resolves it when writing the title, so both halves of the flow produce the same slug for the same title.
  • <type> from the labels. [issue_template.by_type.*].labels is the type-to-labels map gwm new creates issues with; --issue reads it backwards. A type that declares no labels is never a candidate: an empty list says nothing about which issues belong to it.

Labels that select nothing, or that select two types, are not guessed at. The command exits naming the labels it saw and the types they could not separate, and --type supplies the answer:

Terminal window
gwm create --issue 597 --type fix

A repo that has never configured [issue_template.by_type.*].labels gets no derivation at all, and the error says so rather than defaulting to a type.

Three refusals, each naming its way out:

Situation Behaviour
a worktree already carries that issue number prints its path and exits 0, so the command is safe to re-run
the issue is closed refuses; --force proceeds. A worktree for a closed issue is usually a wrong number
the issue number does not exist the forge’s own error, unwrapped

The already-exists check runs before the closed-issue refusal: an issue closes while its worktree is still alive, and re-running must not start failing on a worktree that is right there. It reads the same link gwm list shows, so a worktree attached by hand with gwm link --issue counts too.

Everything after the derivation is the ordinary gwm create path, #{issue} in branch_pattern included, so the issue link resolves on its own.

Not every worktree is an issue. --name skips the convention entirely:

Terminal window
gwm create --name spike-redis
# → branch spike-redis
# → worktree ~/cc-worktree/<repo>/spike-redis

The name becomes the branch verbatim: it is validated exactly as typed, so --name " spike" is refused rather than trimmed into a different branch than the one you asked for. branch_pattern and path_pattern do not apply: they are written in terms of {type} / {issue} / {desc}, and a free-form name has none of them. [worktree].base still applies, so free-form worktrees land beside the structured ones, for the placeholders it documents ({home}, {repo}, {repo_path}, {repo_parent}); a base written with {type} / {issue} / {desc} is refused instead, since there is nothing to resolve it against and it would otherwise end up literal in the path. A / is legal in the branch and flattens to - in the directory name, the same relationship the default pattern pair already has.

What you give up. Everything that reads the branch name back goes quiet. That is the deal, not a bug. This table describes a name that does not match the branch convention; nothing records how a worktree was named, only what its branch is, so a free-form name that happens to look structured (--name 'feat/#42-x') is read back as structured and keeps every row below:

Feature On a free-form worktree
issue auto-linking inactive; use gwm link --issue <N> to attach one by hand
PR/MR detection still works, it queries the forge with the whole branch name
gitmoji / gwm commit-prefix errors: a prefix is derived from the branch type, and there is none
doctor orphan check treats it as a user-managed branch and never flags it
hook placeholders (create / remove / bootstrap) {type} / {issue} / {desc} resolve empty
TUI edit form not applicable; that form rebuilds the triple, rename with git

Accepted names. A free-form name has to be three things at once, and the rules follow from that rather than from a hand-written list of bad examples.

It is a git branch, checked against libgit2’s own branch-level rule set, which is stricter than the reference-level one (refs/heads/HEAD is a valid reference name, HEAD is not a usable branch name). It is a single filesystem path component, the worktree directory, which a branch name is not: no . or .. component (a directory named .. would escape the base), and at most 255 bytes: a×130/b×130 is a legal ref and an illegal directory name, and without the cap the branch gets created before the directory fails, leaving it orphaned. And it is a literal value during hook expansion, so no { or }: placeholders are substituted in sequence, and a branch called spike-{issue} would have its own name rewritten inside the {branch} value a hook receives.

One more rule belongs to none of those: no leading -. Git accepts it; gwm remove and git branch -d would read it as a flag.

Spike_Redis, 2026.07.27 and réécriture are all fine.

The directory has to be hostable on Windows too, so three more rules apply on every platform, not just there (#475). No <, >, " or |, which Win32 forbids in a path component and git accepts. No path component that is a reserved device name (CON, PRN, AUX, NUL, COM1–COM9, LPT1–LPT9), case-insensitively and with or without an extension, since Win32 reads NUL.tar.gz as NUL. And no path component ending in ., which Windows refuses as a directory name.

The last two are checked per /-separated segment, because a loose ref is a file at .git/refs/heads/<name>, which makes every segment a path component there: spike/CON flattens to the perfectly legal directory spike-CON and is still an unwritable ref on Windows. The trailing period is the case worth knowing about, because git almost covers it: its own rule applies to the whole branch name, so spike. is refused by git while foo./bar is not. v1.2/spike stays legal, a period inside a segment not being a trailing one.

The rules are not gated to Windows because a branch travels to a teammate’s machine through the forge. A name no Windows checkout can host is a hazard for the whole team, not a local one. The residual set is exactly what libgit2 does not already reject: git itself refuses :, \, ?, * and a space in every position, so those need no rule of their own. COM0 and LPT0 stay legal, being absent from the Win32 reserved list.

In the TUI, Ctrl-T toggles the create form (and only that form) between the structured triple and a single free-form Name field.

End-to-end walkthrough lives in Getting started → First worktree.

Create a GitHub issue from the repo’s configured issue form, then create the matching worktree from the returned issue number.

Terminal window
gwm new feat add-config-types
# → created issue #142 [Feature]: add-config-types
# → branch feat/#142-add-config-types

gwm new reads [issue_template] from .gwm.toml, renders the selected .github/ISSUE_TEMPLATE/*.yml file to markdown, calls gh issue create --body-file, then hands off to the same worktree creation path as gwm create.

Flag Action
--no-bootstrap Create the worktree without running bootstrap
--reuse-branch Attach to an existing local branch after the issue is created
--skip-hooks Skip comma-separated lifecycle hook phases

gwm list [--format=table|names|json] [--detect-pr] [--workspace <dir>]

Section titled “gwm list [--format=table|names|json] [--detect-pr] [--workspace <dir>]”

List the worktrees in the current repo.

Terminal window
gwm list # human-readable table
gwm list --format=names # one worktree name per line (for shell completion)
gwm list --format=json # machine-readable JSON array (issue #38)
gwm list --detect-pr # add a PR column, auto-detecting each branch's PR via gh
gwm list --workspace ~/Projects # merged table across every child repo, leading REPO column

The names format excludes the main workdir: gwm path / remove / bootstrap never accept it as a target, so emitting it as a completion candidate would be misleading.

--format=json (issue #38) emits a stable JSON array, with the schema documented at docs/schema/worktree-list.schema.json. Unlike names, it includes the main worktree: a JSON consumer (an editor, a statusbar) wants the full set and resolves the active worktree from it. Each entry carries name, id, path, branch, head, is_main / is_locked / is_prunable, a status object (is_dirty, has_upstream, ahead, behind, unknown), age_seconds, and linked issue / pr numbers. Pipe into jq:

Terminal window
gwm list --format=json | jq '.[] | select(.status.is_dirty) | .name'

--detect-pr adds a PR column populated by PR auto-detection (gh pr list --head <branch> per worktree). It is off by default so the plain listing stays network-free: one gh call per worktree is only paid when the flag is set. Ignored with --format=names.

--workspace <dir> (issue #36) prints the merged worktree table across every git repo one level below <dir>, with a leading REPO column naming each row’s repo. See Workspace mode for the full behaviour.

gwm path <pattern> [--format=text|json] (alias: gwm cd <pattern>)

Section titled “gwm path <pattern> [--format=text|json] (alias: gwm cd <pattern>)”

Print the on-disk path of a worktree matching <pattern> (fuzzy). Use with $(...) to cd:

Terminal window
cd "$(gwm path auth)"
cd "$(gwm cd auth)" # same — framing for the cd flow
gwm path auth --format=json # { "name": ..., "path": ..., "branch": ... }

The default text form prints the bare path for $(...) consumption. --format=json (issue #38) emits the { name, path, branch } triple, with the schema at docs/schema/path.schema.json.

Both forms share semantics: fuzzy resolve, exit 0 on a unique hit, 1 on miss / ambiguous / not in a repo. Pair with gwm shell-init for the gcd one-liner. See Getting started → Shell init.

Print a worktree’s note on stdout, verbatim. Without a slug the note of the worktree the CWD sits in is printed.

Terminal window
gwm note show # the current worktree's note
gwm note show auth # a worktree resolved by fuzzy pattern
gwm note show >/dev/null # exit 0 = there is a note, 1 = there is none

A note is plain Markdown stored at <main-checkout>/.git/gwm/notes/<branch>.md, written from the TUI’s N (an editable modal, Ctrl+e there hands it to $EDITOR) or by editing the file directly. It is never committed, it survives gwm remove, and it is readable from the main checkout. See TUI → notes.

The subcommand is read-only: the note is prose, and prose is written in an editor.

Presence means non-blank: a file that is absent, unreadable or contains only whitespace all print nothing and exit 1. A blank file is what an editor leaves behind when you open a note and save without typing, so treating it as a note would light the table marker up for nothing.

Exit 1 also covers a detached HEAD, which carries no note at all: a note is keyed on the branch, so a worktree without one has nothing to key on. The reason goes to stderr in both cases, which keeps stdout clean for $(...).

The same text rides the --format=json list rows as an additive note field (experimental tier, omitted when absent), so a statusline or an editor plugin does not have to shell out per row.

Open the TUI in picker mode: same table as bare gwm, fuzzy filter bar pre-open, create / delete / bootstrap disabled. Press Enter to print the highlighted worktree’s path on stdout, Esc / q / Ctrl-C to cancel with exit code 1.

Terminal window
cd "$(gwm switch)" # open picker, type to narrow, Enter to commit
gcd # same, via the `gwm shell-init` wrapper

Re-run the .gwm.toml bootstrap pipeline on a worktree without recreating it.

Terminal window
gwm bootstrap # on the CWD worktree
gwm bootstrap auth # on a fuzzy-matched name

Useful after editing .gwm.toml or adding new [[bootstrap.copy]] rules. Same ✓ / ! / ✗ report as gwm create.

Flag Action
--skip-hooks <PHASES> Skip the comma-separated lifecycle hook phases (e.g. pre_bootstrap,post_bootstrap)

Fetch a worktree’s upstream and bring its branch up to date: rebase by default, or merge with --merge.

Terminal window
gwm sync # the CWD worktree, rebase onto upstream
gwm sync auth # a fuzzy-matched worktree
gwm sync auth --merge # merge the upstream instead of rebasing

Resolves the target like gwm bootstrap (fuzzy pattern, defaults to the worktree containing the CWD, which may be the main worktree, so you can sync trunk too). It runs git fetch for the upstream’s remote, recomputes how far behind the branch is, then integrates only when there’s something to integrate. Reports a single ✓ line (already up to date / rebased N commit(s) / merged N commit(s)).

Guard rails:

  • Dirty working tree → refuses before touching the remote (commit or stash). A rebase/merge on top of uncommitted work is how changes get lost.
  • No upstream configured → errors with the git branch --set-upstream-to=<remote>/<branch> fix.
  • Conflict → the rebase/merge is aborted so the worktree is left usable, and you’re told to reconcile by hand.

The fetch / rebase / merge steps shell out to your git (so SSH keys, credential helpers, and insteadOf rules all apply); the dirty / upstream / ahead-behind inspection uses libgit2.

gwm remove <PATTERN>... [--delete-branch] [--dry-run]

Section titled “gwm remove <PATTERN>... [--delete-branch] [--dry-run]”

Remove one or more worktrees by fuzzy match. The branch survives by default.

Terminal window
gwm remove auth # remove the worktree, keep the branch
gwm remove auth --delete-branch # remove the worktree AND drop the branch
gwm remove auth --dry-run # preview the plan, destroy nothing
gwm remove auth --dry-run --delete-branch # preview, including the branch drop
gwm remove auth docs spike --delete-branch # remove a batch in one command

Every pattern is resolved before anything is touched (issue #484), so a typo in the middle of a batch fails the whole command with nothing removed, instead of leaving the first half gone. Patterns naming the same worktree are removed once. The destructive loop itself does not stop at the first error: each failure is named on stderr and the command still exits non-zero. The TUI counterpart is Space to mark rows, d to delete the batch.

The CLI form has no countdown (the TUI’s d confirm-overlay countdown is TUI-only). --delete-branch is destructive: only git reflog can resurrect a dropped branch.

Flag Action
--delete-branch Also drop the local branch after removing the worktree (destructive)
--dry-run Print the would-remove plan (name + path + branch) and exit 0 without touching anything (issue #31)
--force Emergency removal mode: skip the pre_remove / post_remove lifecycle hooks
--skip-hooks <PHASES> Skip the comma-separated lifecycle hook phases (e.g. pre_remove,post_remove)

--dry-run resolves the fuzzy pattern, prints the plan, and exits 0: no destruction, no journal write (see gwm undo / gwm history). With --delete-branch it tags the branch line (would be deleted); on a detached-HEAD worktree it prints branch: - (no branch to delete) instead, mirroring the destructive path’s behaviour. An ambiguous pattern fires the same non-zero candidate-list error as the destructive form: --dry-run only suppresses destruction, not resolution failures.

Clear stale entries in .git/worktrees/ whose working directory was removed manually (e.g. rm -rf outside gwm).

Terminal window
gwm prune # prune every stale entry
gwm prune --dry-run # list the prunable entries, touch nothing

gwm doctor flags prunable entries as a Warning; gwm prune is the documented remediation.

Flag Action
--dry-run List every prunable entry (name + path + reason) and exit 0 without touching the admin files (issue #31)

--dry-run output is sorted by name for deterministic stdout diffing; the empty case prints 0 worktree(s) to prune so scripted callers get a stable signal. Column widths are computed in Unicode characters so non-ASCII names stay aligned. The preview and the destructive pass share the same scanner, so they can never drift on what “prunable” means.