Worktree lifecycle
gwm create <type> <issue> <desc>
Section titled “gwm create <type> <issue> <desc>”Create a worktree and matching branch.
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.
From an existing issue (--issue)
Section titled “From an existing issue (--issue)”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.
gwm create --issue 594# → resolving issue #594 on kbrdn1/gwm-cli# → labels: feature → type: feat# → branch feat/#594-modals-should-follow-tui-layoutNothing about the branch is retyped. The two halves come from the issue:
<desc>from the title. The branch type’stitle_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 asgwm newresolves 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.*].labelsis the type-to-labels mapgwm newcreates issues with;--issuereads 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:
gwm create --issue 597 --type fixA 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.
Free-form naming (--name)
Section titled “Free-form naming (--name)”Not every worktree is an issue. --name skips the convention entirely:
gwm create --name spike-redis# → branch spike-redis# → worktree ~/cc-worktree/<repo>/spike-redisThe 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.
gwm new <type> <desc>
Section titled “gwm new <type> <desc>”Create a GitHub issue from the repo’s configured issue form, then create the matching worktree from the returned issue number.
gwm new feat add-config-types# → created issue #142 [Feature]: add-config-types# → branch feat/#142-add-config-typesgwm 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.
gwm list # human-readable tablegwm 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 ghgwm list --workspace ~/Projects # merged table across every child repo, leading REPO columnThe 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:
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:
cd "$(gwm path auth)"cd "$(gwm cd auth)" # same — framing for the cd flowgwm 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.
gwm note show [<slug>] (issue #515)
Section titled “gwm note show [<slug>] (issue #515)”Print a worktree’s note on stdout, verbatim. Without a slug the note of the worktree the CWD sits in is printed.
gwm note show # the current worktree's notegwm note show auth # a worktree resolved by fuzzy patterngwm note show >/dev/null # exit 0 = there is a note, 1 = there is noneA 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.
gwm switch (alias: gwm s)
Section titled “gwm switch (alias: gwm s)”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.
cd "$(gwm switch)" # open picker, type to narrow, Enter to commitgcd # same, via the `gwm shell-init` wrappergwm bootstrap [<pattern>]
Section titled “gwm bootstrap [<pattern>]”Re-run the .gwm.toml bootstrap pipeline on a worktree without recreating it.
gwm bootstrap # on the CWD worktreegwm bootstrap auth # on a fuzzy-matched nameUseful 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) |
gwm sync [<pattern>] [--merge]
Section titled “gwm sync [<pattern>] [--merge]”Fetch a worktree’s upstream and bring its branch up to date: rebase by default, or merge with --merge.
gwm sync # the CWD worktree, rebase onto upstreamgwm sync auth # a fuzzy-matched worktreegwm sync auth --merge # merge the upstream instead of rebasingResolves 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.
gwm remove auth # remove the worktree, keep the branchgwm remove auth --delete-branch # remove the worktree AND drop the branchgwm remove auth --dry-run # preview the plan, destroy nothinggwm remove auth --dry-run --delete-branch # preview, including the branch dropgwm remove auth docs spike --delete-branch # remove a batch in one commandEvery 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.
gwm prune [--dry-run]
Section titled “gwm prune [--dry-run]”Clear stale entries in .git/worktrees/ whose working directory was removed manually (e.g. rm -rf outside gwm).
gwm prune # prune every stale entrygwm prune --dry-run # list the prunable entries, touch nothinggwm 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.