gwm-cli 1.5.0

git worktree manager — TUI + CLI, native libgit2, per-repo bootstrap
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
---
title: Subcommand reference
description: Every `gwm` subcommand with synopsis, flags, exit codes, and examples.
---

# Subcommand reference

`gwm <subcommand>` is the scriptable face of gwm. Every command exits with a meaningful code (`0` ok, `1` warning, `2` failure) so you can wire `gwm doctor` into CI without parsing stdout.

Bare `gwm` (no subcommand) opens the [TUI](/tui) on the current repo.

## `gwm init [--preset <name>] [--list-presets] [--show]`

Write a `.gwm.toml` to the current repo — the generic documented template by default, or an opinionated stack preset.

```bash
gwm init
# → wrote /path/to/repo/.gwm.toml

gwm init --preset rust          # seed a Rust-flavoured .gwm.toml
gwm init --list-presets         # enumerate the built-ins, write nothing
gwm init --preset node --show   # print the resolved TOML, write nothing
```

Refuses to overwrite an existing `.gwm.toml`. Tweak the generated file and re-run `gwm doctor` to validate.

| Flag              | Action                                                                                          |
|:------------------|:------------------------------------------------------------------------------------------------|
| `--preset <NAME>` | Seed an opinionated `.gwm.toml` for a known stack instead of the generic template (issue #37)    |
| `--list-presets`  | List the built-in presets with one-line descriptions and exit (writes nothing, needs no git repo) |
| `--show`          | Print the resolved preset (or template) to stdout instead of writing — handy for diffing         |

### Stack presets (issue #37)

`--preset <name>` seeds an opinionated `.gwm.toml` for a known stack instead of the generic template — env copies, no-symlink invariants, and the right install command pre-wired. `gwm init --list-presets` enumerates them:

```text
generic    The fully-documented default template (same as `gwm init`).
go         Go module: bin/ no-symlink + `go mod download`.
laravel    Laravel: env copies + AWS-RDS guard + vendor/ no-symlink + composer install.
node       Node / Nuxt: node_modules/ no-symlink + bun-or-npm install. (alias: nuxt)
python-uv  Python (uv): .venv/ no-symlink + `uv sync`.
rust       Rust crate: target/ no-symlink + `cargo fetch`.
```

`nuxt` is an alias for `node` (same body), and `generic` is the documented default — `gwm init` with no flag writes it byte-for-byte. Preset bodies are embedded in the binary and kept in sync with [`examples/presets/<name>.toml`](https://github.com/kbrdn1/gwm-cli/tree/main/examples/presets). `--show` prints the resolved TOML to stdout without touching disk, so you can diff a preset against an existing config:

```bash
gwm init --preset laravel --show | diff - .gwm.toml
```

## `gwm config`

Read, edit, and validate `.gwm.toml` without opening the file by hand.

```bash
gwm config get tui.confirm_countdown_secs
gwm config set tui.confirm_countdown_secs 5
gwm config set 'labels[+].name=bug'
gwm config unset review.tool
gwm config list --prefix worktree
gwm config validate
vi "$(gwm config path)"
gwm config edit
```

Keys use dot-path notation. Array tables use indexes (`labels[0].name`), and `set` also accepts `[+]` to append the next table entry. Writes use `toml_edit`, so existing comments and formatting are preserved; after every write, gwm reloads the same runtime schema used by the rest of the CLI.

## `gwm types [--gitmoji]`

List the supported branch types (the `<type>` slot of `gwm create`).

```bash
gwm types
# → feat
#   fix
#   hotfix
#   docs
#   …

gwm types --gitmoji
# → feat      ✨  :sparkles:
#   fix       🐛  :bug:
#   …
```

Override per repo via `[worktree].branch_types` in `.gwm.toml`.

| Flag         | Action                                                                                            |
|:-------------|:--------------------------------------------------------------------------------------------------|
| `--gitmoji`  | Extend the list with two columns — the unicode emoji and its `:shortcode:` form (issue #85)        |

`--gitmoji` resolves each type's emoji from the built-in defaults plus any per-repo `[gitmoji]` overrides in `.gwm.toml`. Custom branch types are supported — `migration = ":truck:"` round-trips through `gwm types --gitmoji`. See [`gwm commit-prefix`](#gwm-commit-prefix---branch-name---unicode) for the matching commit-prefix surface.

## `gwm commit-prefix [--branch <name>] [--unicode]`

Print the canonical Gitmoji + Conventional Commits prefix for the current (or named) branch — handy for shell prompts, AI assistants, and scripted commit composition.

```bash
gwm commit-prefix
# → :sparkles: feat(#41):

gwm commit-prefix --unicode
# → ✨ feat(#41):

gwm commit-prefix --branch feat/#41-tui-search
# → :sparkles: feat(#41):
```

Without `--branch`, reads the current branch from `HEAD` via libgit2 (requires the CWD to be inside a git repo). `--branch` resolves the prefix for an explicit branch name; it still reads `.gwm.toml` for the `[gitmoji]` overrides.

| Flag             | Action                                                                                                   |
|:-----------------|:---------------------------------------------------------------------------------------------------------|
| `--branch <name>`| Resolve the prefix for a named branch (e.g. `feat/#41-tui-search`) instead of `HEAD`                     |
| `--unicode`      | Emit the real emoji character (`✨`) instead of the `:shortcode:` form (`:sparkles:`)                     |

Under `--unicode`, known `:shortcode:` overrides are normalised to their emoji (e.g. a `[gitmoji]` `feat = ":rocket:"` prints `🚀 feat(#1):`); unknown shortcodes fall through verbatim — no panic, no substitution. The emoji mapping is the built-in default plus any `[gitmoji]` overrides in `.gwm.toml`.

## `gwm hooks install commit-msg [--force]`

Install an opt-in `commit-msg` git hook that auto-prepends the resolved commit prefix when a message doesn't already start with one.

```bash
gwm hooks install commit-msg            # install into .git/hooks/commit-msg
gwm hooks install commit-msg --force    # overwrite an existing commit-msg hook
```

The hook is **never installed implicitly** — you opt in once per repo. It is non-destructive by default: it refuses to overwrite a pre-existing `commit-msg` hook (husky / commitlint / pre-commit) and exits non-zero with the conflicting path unless `--force` is passed. The hook honours `core.hooksPath`, resolves a linked-worktree `.git` file via `Repository::discover`, and degrades gracefully if `gwm` is not on `$PATH` at commit time (the message is left untouched). `commit-msg` is the only hook kind today; clap rejects any other value at parse time.

| Flag        | Action                                                                       |
|:------------|:-----------------------------------------------------------------------------|
| `--force`   | Replace an existing hook of the same name (otherwise the install refuses)     |

The prefix it prepends is exactly what [`gwm commit-prefix`](#gwm-commit-prefix---branch-name---unicode) prints for the worktree's branch.

## `gwm create <type> <issue> <desc>`

Create a worktree and matching branch.

```bash
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`)                |
| `--repo <NAME>`         | In [workspace mode](#workspace-mode-global---workspace-issue-36), 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.

End-to-end walkthrough lives in [Getting Started → First worktree](/getting-started/first-worktree).

## `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.

```bash
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 review <PR#> [--name <branch>] [--bootstrap] [--skip-hooks <phases>]` (issue #308)

Materialise an existing GitHub PR into an isolated worktree — fetch the PR head, attach a worktree, link the PR, and you're reviewing the contributor's code in seconds.

```bash
gwm review 310                          # fetch PR #310 into review/pr-310-<author>-<slug>
gwm review 310 --name pr-310            # override the local review branch name
gwm review 310 --bootstrap              # ...and run bootstrap + lifecycle hooks (opt-in)
```

`gwm review` resolves the PR head via `gh` and fetches origin's universal `refs/pull/<N>/head` ref, so it is **cross-fork aware** and valid for PRs in any state (open / draft / closed / merged). It creates a local `review/pr-<N>-<author>-<slug>` branch, attaches a worktree (the directory is derived from the branch name, slashes become dashes), and links the PR so the [TUI sidebar / CI indicator](/tui) light up immediately. Tear down like any worktree: `gwm remove <dir> --delete-branch`.

| Flag                    | Action                                                                                       |
|:------------------------|:---------------------------------------------------------------------------------------------|
| `--name <BRANCH>`       | Override the local review branch name (default `review/pr-<N>-<author>-<slug>`); the worktree dir is derived from it |
| `--bootstrap`           | Run bootstrap + lifecycle hooks against the PR's code after creation (off by default)          |
| `--skip-hooks <PHASES>` | Skip the comma-separated lifecycle hook phases (e.g. `pre_create,post_create`)                 |

**Safe-by-default:** bootstrap and lifecycle hooks are **not** run. A review worktree holds a contributor's (possibly fork) code, and those steps execute commands against it (`npm install`, `composer install`, `direnv allow`, `post_create` hooks …) — i.e. arbitrary code. Pass `--bootstrap` to opt in once you trust the PR enough to set it up. This is distinct from the `[review]` config block (which drives the TUI `R` review-launcher key); `gwm review` is the worktree-materialisation subcommand.

## `gwm pr [--draft] [--base <ref>] [--render]`

Render the PR body from `[pr_template]` and shell out to `gh pr create` against the resolved trunk.

```bash
gwm pr                                # creates the PR
gwm pr --draft                        # creates a draft PR
gwm pr --base develop                 # diff against develop instead of [doctor].trunks
gwm pr --render                       # prints the rendered body to stdout (no PR created)
gwm pr --render | gh pr create --body-file -
```

`gwm pr` reads the current branch (parsing `<type>/#<N>-<desc>` for the `{type}` / `{issue}` / `{desc}` placeholders), picks the first existing trunk from `[doctor].trunks` (or falls back to `main`), then renders the per-branch-type template under `[pr_template.by_type.<type>]` (inline `body` wins over `path`), with `[pr_template].default` as the fallback.

| Flag                | Action                                                              |
|:--------------------|:--------------------------------------------------------------------|
| `--render`          | Print the rendered Markdown to stdout; never shell out to `gh`      |
| `--draft`           | Forward `--draft` to `gh pr create` so the PR opens as a draft      |
| `--base <REF>`      | Override the comparison base instead of the first matching trunk    |

On success the new PR number is recorded under `branch.<head>.gwm-pr` (same key `gwm link pr` writes), so `gwm status` and `gwm open pr` resolve the link without a separate `gwm link` call.

Body resolution and placeholder semantics are documented under [Configuration → `[pr_template]`](/configuration/gwm-toml#pr_template-issue-84).

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

List the worktrees in the current repo.

```bash
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 — schema documented at [`docs/schema/worktree-list.schema.json`](https://github.com/kbrdn1/gwm-cli/blob/main/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`:

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

`--detect-pr` adds a `PR` column populated by [PR auto-detection](/integrations/github-linking#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](#workspace-mode-global---workspace-issue-36) for the full behaviour.

## `gwm agents [attach|detach] [--format=table|json]` (issue #408)

List the AI-agent sessions detection matched to each worktree, or pin one
manually. Detection reads each agent's on-disk session artefacts (Claude
Code, Codex, opencode, Mistral Vibe) — no process enumeration, same code
path on Linux, macOS and Windows. One refinement on Unix: when Claude
Code's live registry records a session's PID and that process is gone,
the session drops to idle immediately instead of riding out the activity
window; elsewhere (and for the other agents) classification stays
artefact-only. The same data feeds the TUI's AGENT
column and `a` overlay, `gwm list` (table column + JSON `agents` field), the
daemon and `gwm statusline` (fed by the daemon transport on every
platform — unix socket, or a named pipe on Windows, #439).

```bash
gwm agents                       # sessions per worktree: agent, freshness, last activity, id, name
                                 # + an `unmatched` section for sessions no worktree matched
gwm agents --format=json         # the same worktree rows as `gwm list --format=json`
gwm agents attach . 019f6b95-…   # pin session <id> to the enclosing worktree
gwm agents attach feat-42 <id>   # …or to the worktree matching a name substring
gwm agents detach feat-42 <id>   # remove that one pin
gwm agents detach feat-42        # remove every pin — back to pure auto-detection
```

**Auto-detection is the default; a pin is an override.** A pin covers the
cases the recorded working directory cannot: an agent launched from a
subdirectory, a moved worktree, a session you want on a specific worktree
regardless of where it was started. Pins **accumulate** — several agents
can work one worktree, so attach adds a pin and `detach <wt> <id>` removes
just that one (bare `detach <wt>` clears them all). Stored in git branch
config (multi-valued `gwm-agent-pin`), never committed. The sidebar's
Agents pane shows **only pinned** sessions — pinning is how a session earns
its place there.
Attaching a session id that detection cannot resolve exits with status 1; a pin whose
artefacts later disappear degrades silently back to detection. Detached-HEAD
worktrees cannot hold a pin. Sessions matched to **no** worktree — launched
in another repo, a subdirectory, an old path — are listed under an
`unmatched` section: precisely the ids `attach` takes.

`GWM_AGENTS_HOME` overrides the home directory the artefact scans read —
mainly a deterministic seam for tests and CI.

## `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`:

```bash
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 — schema at [`docs/schema/path.schema.json`](https://github.com/kbrdn1/gwm-cli/blob/main/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](/getting-started/shell-init).

## `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`.

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

## `gwm bootstrap [<pattern>]`

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

```bash
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`) |

## `gwm sync [<pattern>] [--merge]`

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

```bash
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]`

Remove a worktree by fuzzy match. The branch survives by default.

```bash
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
```

The CLI form has no countdown (the TUI's [`d` confirm-overlay countdown](/tui/confirm-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](/configuration/gwm-toml)      |
| `--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`](#gwm-undo---bootstrap)). 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]`

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

```bash
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.

## `gwm undo [--bootstrap]`

Recover from a misfired `gwm remove` without `git reflog` archaeology. Pops the most recent destructive op recorded for the current repo, recreates `refs/heads/<branch>` at the saved OID, and re-adds the worktree at the saved path (with `reuse_branch` so the resurrected branch attaches cleanly).

```bash
gwm undo                       # bring back the last removed worktree + branch
gwm undo --bootstrap           # ...and re-run the per-worktree bootstrap
```

| Flag           | Action                                                                          |
|:---------------|:--------------------------------------------------------------------------------|
| `--bootstrap`  | Re-run the per-worktree bootstrap after the resurrection (off by default)        |

The journal entry is consumed **only after** a successful resurrection — a mid-flight failure leaves the recovery anchor intact so you can retry. A detached-HEAD entry (no branch to recreate) is refused with an explicit error rather than silently doing nothing. The journal is shared with [`gwm history`](#gwm-history---limit-n---all); see it for the file location and rotation policy.

## `gwm history [--limit N] [--all]`

List the recent destructive operations recorded by gwm, newest first.

```bash
gwm history                    # last 20 ops for the current repo
gwm history --limit 50         # last 50
gwm history --all              # every op across every repo (forensic / multi-repo)
```

| Flag           | Action                                                                                   |
|:---------------|:-----------------------------------------------------------------------------------------|
| `--limit N`    | Maximum entries to print, newest first. Default `20`                                      |
| `--all`        | List ops across every repo, not just the current one                                      |

By default it filters to the current repo's canonicalised workdir; `--all` surfaces every entry. An empty result prints `no operations recorded` as a stable scripted signal. The journal lives at `$XDG_DATA_HOME/gwm/history.toml` (override with `$GWM_HISTORY_FILE`; macOS falls back under `Application Support`, Windows under `%LOCALAPPDATA%`) and is capped at 100 entries — the oldest is dropped on overflow. Every `gwm remove` (with or without `--delete-branch`) appends an entry; `gwm remove --dry-run` does **not** write the journal, so previewing a destruction can never let you "undo" something that never happened.

## `gwm completions <shell>`

Print a static completion script. Supported shells: `zsh`, `bash`, `fish`, `powershell`, `elvish`. See [Shell completions](/cli/completions) for installation per shell.

## `gwm shell-init <shell>`

Print the `gcd` shell wrapper. Supported shells: `zsh`, `bash`, `fish`, `powershell`. See [Getting Started → Shell init](/getting-started/shell-init).

## `gwm tmux <pattern> [-p|--split]`

Open the matched worktree in a new tmux window of the **current** session. `--split` substitutes `split-window` for `new-window`. Requires `$TMUX` to be set.

```bash
gwm tmux auth                  # new tmux window inside the matched worktree
gwm tmux auth -p               # split the current pane instead
```

Outside a tmux session, exits non-zero with a clear error (does not spawn a stray server).

## `gwm zellij <pattern> [-p|--split]`

Same as `gwm tmux` but for zellij. Uses `zellij action new-tab --cwd <path>` (requires zellij ≥ 0.40 for the `--cwd` flag) or `new-pane --cwd` with `-p`. Requires `$ZELLIJ`.

See [CLI → Multiplexer integration](/cli/multiplexer) for the full surface and edge cases.

## `gwm link {issue|pr} <N> [--worktree <pattern>]`

Link the current (or named) worktree to a GitHub issue or PR.

```bash
gwm link issue 42              # link the current worktree to issue #42
gwm link pr 61                 # link a PR
gwm link issue 42 --worktree feat-auth      # ...or to a fuzzy-matched worktree
```

The link is stored in `git config branch.<name>.gwm-issue` / `gwm-pr` — local, per-branch, no extra file. Issue numbers are **auto-detected** from `<type>/#<N>-<slug>` branches, so `gwm link issue <N>` is only needed for explicit overrides. PR numbers are not auto-detected.

## `gwm unlink {issue|pr} [--worktree <pattern>]`

Remove the explicit link override on the current (or named) worktree.

```bash
gwm unlink issue               # remove the issue link (auto-detect resurfaces)
gwm unlink pr                  # remove the PR link
```

Idempotent — safe to run when nothing is linked.

## `gwm open {issue|pr} [--worktree <pattern>] [--print-url]`

Open the linked issue / PR in the browser via the OS opener.

```bash
gwm open issue                 # spawn the OS opener on the linked issue URL
gwm open pr --print-url        # print the URL on stdout, no spawn
```

Useful in headless shells and tests with `--print-url`.

## `gwm status [--worktree <pattern>] [--json]`

Show the link plus (when `gh` is available) live GitHub state.

```bash
gwm status
# → Issue #42 [open] TUI: fuzzy search
# → PR #61 [draft] · checks 2/3

gwm status --json              # stable schema for scripts
```

Degrades gracefully to local-link-only output when `gh` is missing or the repo has no GitHub remote.

## `gwm labels {list|push}`

Manage the declarative GitHub label set from `.gwm.toml`. Declare the labels you want once in `[[labels]]`, push them to the upstream `origin` remote as needed — no more drift across repos. Without a `[[labels]]` block in `.gwm.toml`, both subcommands are no-ops (`0 labels declared, nothing to push`) and never shell out to `gh`.

```bash
gwm labels list                       # show the diff against the remote
gwm labels push                       # apply create + update
gwm labels push --dry-run             # plan only, no remote mutations
gwm labels push --prune               # also delete labels not in config
gwm labels push --random-colors       # random pastel for entries with no `color`
```

| Flag             | Action                                                                                                                                          |
|:-----------------|:------------------------------------------------------------------------------------------------------------------------------------------------|
| `--dry-run`      | Print the plan without mutating the remote. Still reads remote labels via `gh label list` to compute the diff; only create / update / delete calls are skipped. |
| `--prune`        | Delete labels on the remote that aren't declared in `.gwm.toml` (destructive — opt-in)                                                          |
| `--random-colors`| Use a random pastel for entries with no `color` field (overrides the deterministic hash)                                                        |

`list` output sigils mirror the diff buckets:

```
+ bug                  (will create — color #d73a4a)
~ good first issue     (color #008672 → #7057ff)
= documentation        (match)
- wontfix              (on remote, not in config)
```

Requires `gh` on `$PATH` (the same soft dependency as `gwm status`). Schema reference and authoring tips: [Configuration → `.gwm.toml`](/configuration/gwm-toml#labels-issue-81).

## `gwm milestones {list|push}`

Manage the declarative GitHub milestone set from `.gwm.toml`. Same shape as `gwm labels`; the REST endpoint is used because `gh` has no native `gh milestone` subcommand. Without a `[[milestones]]` block in `.gwm.toml`, both subcommands are no-ops (`0 milestones declared, nothing to push`) and never shell out to `gh`.

```bash
gwm milestones list                     # show the diff against the remote
gwm milestones push                     # apply create + update
gwm milestones push --dry-run           # plan only, no remote mutations
gwm milestones push --prune             # also delete milestones not in config
```

| Flag        | Action                                                                                                                                              |
|:------------|:----------------------------------------------------------------------------------------------------------------------------------------------------|
| `--dry-run` | Print the plan without mutating the remote. Still reads remote milestones via `gh api …/milestones` to compute the diff; only create / update / delete calls are skipped. |
| `--prune`   | Delete milestones on the remote that aren't declared in `.gwm.toml` (destructive — opt-in)                                                          |

`list` output sigils mirror the diff buckets:

```
+ v0.7.0               (will create — state open, due 2026-07-15T23:59:59Z)
~ v0.6.0               (due 2026-07-01T23:59:59Z → 2026-07-15T23:59:59Z)
= v0.5.0               (match)
- old-sprint           (#3 on remote, not in config)
```

Requires `gh` on `$PATH`. Schema reference and authoring tips: [Configuration → `.gwm.toml`](/configuration/gwm-toml#milestones-issue-82).

## `gwm doctor [--format=text|json]`

Run 8 health checks; report each with `✓ / ! / ✗`; exit `0 / 1 / 2`. Designed for CI and pre-commit hooks. See [Integrations → `gwm doctor`](/integrations/doctor) for the per-check breakdown.

`--format=json` (issue #38) emits the checks array plus aggregate `severity` and `exit_code` — schema at [`docs/schema/doctor.schema.json`](https://github.com/kbrdn1/gwm-cli/blob/main/docs/schema/doctor.schema.json). The **process exit code is identical** to the text form (the JSON also carries it as a field), so `gwm doctor --format=json` still works in an `if`-guard:

```bash
gwm doctor --format=json | jq '.checks[] | select(.status == "failed")'
```

## `gwm daemon [--socket <path>] [--poll-ms <ms>]` (issue #38)

Run gwm as a long-running **JSON-RPC 2.0 daemon** over a unix domain socket (a named pipe on Windows, #439), so editors / statusbars / tooling connect once instead of spawning `gwm` per query.

```bash
gwm daemon                              # bind $XDG_RUNTIME_DIR/gwm.sock (→ $TMPDIR → /tmp)
gwm daemon --socket /tmp/gwm.sock       # explicit socket path
gwm daemon --poll-ms 500                # faster subscribe push, more git scans
```

**Wire format:** newline-delimited JSON (NDJSON) — one request object per line, one response per line.

| Method | Params | Result |
|--------|--------|--------|
| `list` | — | array of worktrees ([schema](https://github.com/kbrdn1/gwm-cli/blob/main/docs/schema/worktree-list.schema.json)) |
| `doctor` | — | doctor report ([schema](https://github.com/kbrdn1/gwm-cli/blob/main/docs/schema/doctor.schema.json)) |
| `path` | `{ "pattern": "<str>" }` | `{ name, path, branch }` ([schema](https://github.com/kbrdn1/gwm-cli/blob/main/docs/schema/path.schema.json)) |
| `subscribe` | — | stream of `worktrees.changed` notifications (first = current snapshot) |

```bash
# request/response (one line in, one line out)
printf '{"jsonrpc":"2.0","method":"list","id":1}\n' | nc -U "${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/gwm.sock"
```

`subscribe` turns the connection into a one-way push stream: the daemon sends a `worktrees.changed` notification with the current snapshot, then one on every change. Change detection is **interval polling** of the worktree set (tuned by `--poll-ms`, default `1000`) — a deliberate MVP choice over a filesystem watch so there's no extra dependency and the behaviour is deterministic; update latency is bounded by the poll interval.

**Platform / build:** behind the default-on `daemon` Cargo feature. Unix binds a unix domain socket (`--socket` is a filesystem path); Windows binds a named pipe (#439, `--socket` is the pipe NAME under `\\.\pipe\`, default `gwm-<user>.sock`, restricted to the owner by its security descriptor). On a `--no-default-features` build the subcommand exits with an explanatory error (it stays listed so `--help` is identical everywhere).

`--poll-ms` must be `≥ 1` (`0` is rejected — it would spin the `subscribe` loop with no wait, re-scanning git as fast as the CPU allows).

## `gwm statusline [--socket <path>] [--watch]` (issue #309)

Print a compact one-line worktree summary for a shell prompt — the first bundled **consumer** of `gwm daemon`. It connects to the daemon socket, asks for the worktree set, and renders the active branch, worktree count, dirty / ahead / behind, and the linked issue / PR.

```bash
gwm statusline                          # one-shot, prints one line and exits
gwm statusline --watch                  # subscribe; reprint on every change
gwm statusline --socket /tmp/gwm.sock   # explicit daemon socket
```

```text
feat/#309-daemon-consumer · 3 wt · * ↑1 · #309 · PR #310
```

Tokens: branch (or worktree name when detached), `N wt` count, `*` dirty, `↑n`/`↓n` ahead/behind, `#N` issue, `PR #N`. The **active** worktree is the one enclosing the current directory; outside any worktree only the count is shown. A CI rollup is intentionally not included (not part of the daemon's stable schema).

**Graceful degradation:** when no daemon is reachable, `gwm statusline` prints an empty line and exits `0`, so a prompt substitution degrades to nothing instead of erroring. Same `--socket` resolution as `gwm daemon`. See [Integrations → Daemon consumers](/integrations/daemon-consumers) for prompt recipes (zsh / tmux / starship) and an editor recipe (Zed / VS Code).

## Workspace mode (global `--workspace`) (issue #36)

`--workspace <dir>` is a **global** flag (`global = true`, so it is accepted before *or* after the subcommand) that operates across every git repo one level below `<dir>` instead of a single repo. It is an orthogonal dimension on top of single-repo mode.

```bash
gwm --workspace ~/Projects               # open the TUI over every direct-child repo
gwm list --workspace ~/Projects          # merged worktree table, leading REPO column
gwm --workspace ~/Projects list          # same — the global flag may precede the subcommand
gwm --workspace ~/Projects create feat 12 search --repo my-api   # disambiguate the target
gwm exec --workspace ~/Projects -- git fetch   # fan out across every child repo's worktrees
gwm clean --workspace ~/Projects --yes         # reclaim artifacts across every child repo
```

- **TUI / `gwm list`** gain a leading **REPO** column naming each row's repo. In the TUI the active repo follows the selection, so every selection-driven action (lazygit, terminal, sync, delete, link, open, …) operates on the highlighted worktree's own repo.
- **`gwm create`** in workspace mode requires `--repo <name>` to disambiguate which child repo gets the new worktree; an absent or unknown name lists the candidates.
- **`gwm exec` / `gwm clean`** (issue #326) fan out across every child repo. Each repo's command / dir-set is resolved upfront (a missing `--profile`, a malformed `[exec]`/`[clean]`, or an unopenable child errors before anything runs), then repos run **sequentially** (parallelism stays bounded *within* a repo) under a `══ <repo>` header, with a `<repo>/<worktree>`-tagged rollup / report and an aggregated exit code. `--profile` resolves per repo against that repo's own `.gwm.toml`; a slug matching nothing in a repo contributes nothing there. `gwm clean --workspace` aggregates one report and one `--yes` decision across all repos; a delete failure in one worktree is reported but doesn't abort the rest. **`--workspace` is still refused on commands that don't implement it.**
- **Bare `gwm`** in a directory that is *not* itself a git repo but holds child repos prompts `No git repo here. Open <dir> as a workspace? [Y/n]`. The prompt is **declined silently when stdin is not a terminal**, so pipes / CI keep the old single-repo behaviour.
- **`.gwm.toml` stays per-repo** — each row inherits its own repo's config. There is no workspace-level config in this version; the keymap and theme resolve once from the first repo, matching the single-repo "resolved once, relaunch to change" contract.

## `gwm trust {list|revoke|show}` (issue #95)

Manage the TOFU trust ledger that gates `.gwm.toml` bootstrap on `gwm create` / `gwm bootstrap`. Ledger lives at `~/.config/gwm/trust.toml` by default; override with `$GWM_TRUST_LEDGER`.

- `gwm trust list` — print every recorded `(origin, sha-prefix, trusted_at, trusted_by)` tuple. Empty ledger prints `0 entries in trust ledger (<path>)` and exits 0.
- `gwm trust revoke <origin>` — drop every entry matching `<origin>` verbatim (SSH and HTTPS flavours of the same GitHub repo are distinct trust paths). Reports `0 entries matched` when nothing changes.
- `gwm trust show` — print the active ledger path and its raw TOML body (or a "file does not exist yet" notice on fresh installs). Useful for triaging "why is gwm re-prompting?" — eyeball the recorded hash vs. `sha256sum .gwm.toml`.

Two **global** flags interact with the ledger on every subcommand that runs bootstrap (`gwm create`, `gwm bootstrap`):

- `--allow-bootstrap` (also `GWM_ALLOW_BOOTSTRAP=1`) skips the trust prompt without recording. Use in CI runners and other non-interactive contexts.
- `--deny-bootstrap` refuses to run bootstrap even if the ledger says trusted. Forensic mode for first-look inspection of an unfamiliar repo.

Threat model and full rationale: see the module-level comment in [`src/trust.rs`](https://github.com/kbrdn1/gwm-cli/blob/main/src/trust.rs).

## `gwm aliases list` (issue #86)

Print the resolved CLI alias chain — every alias reachable from `gwm <name>`, grouped by source. Read-only; declarative editing happens directly in `.gwm.toml` (repo-level) and `~/.config/gwm/aliases.toml` (user-level).

```bash
gwm aliases list
```

Sample output:

```text
built-in:
  cd → path
  s  → switch
repo (.gwm.toml):
  ll  → list --format names
  wip → create feat 0 wip
user (~/.config/gwm/aliases.toml):
  copy → path  (shadowed by repo)
```

Resolution order (highest precedence first):

1. **Built-in subcommands** (`gwm list`, `gwm switch`, …) — never shadowable.
2. **Built-in visible aliases** (`s → switch`, `cd → path`) — also never shadowable.
3. **Repo (`.gwm.toml` `[aliases]`)** — follows the repo across machines.
4. **User (`~/.config/gwm/aliases.toml` `[aliases]`)** — survives machine reinstalls; invisible to teammates.

Aliases are **argv substitution only** — `wip = "create feat 0 wip"` makes `gwm wip` behave as `gwm create feat 0 wip`. The expansion happens BEFORE clap parses argv. Shell pipelines (`&&`, `||`, `|`, `;`, backticks) in values are refused at load time — use a shell alias instead.

Names that shadow a built-in subcommand or visible alias are a hard config error surfaced by `Config::load_for_repo` (e.g. you can't define `list = "..."`). Single-pass expansion — chained aliases don't recurse.

## `gwm theme {list|show <name>}` (issue #33)

Inspect the built-in TUI colour presets that back the `[theme]` block in `.gwm.toml`.

```bash
gwm theme list                 # print every built-in preset name
gwm theme show catppuccin      # dump the preset as a [theme] block
gwm theme show claude-dark | tee -a .gwm.toml   # paste a preset into config
```

- `gwm theme list` — print the names of every built-in preset: `catppuccin`, `gruvbox`, `tokyo-night`, `claude-dark` (the last also resolves under the alias `claude`).
- `gwm theme show <name>` — dump the named preset as a copy-pasteable, round-trippable `[theme]` TOML block you can drop into `.gwm.toml` and tweak per role.

Schema, role list, and per-role overrides: [Configuration → `[theme]`](/configuration/gwm-toml#theme). The TUI keymap-aware help overlay and modal frames pull their colours from the resolved theme — see the [TUI keybindings page](/tui/keybindings).

## `gwm tui keys` (issue #87)

Print the resolved TUI keymap — built-in defaults layered with the `[tui.keys]` overrides from `.gwm.toml` — with the source per row.

```bash
gwm tui keys
# → action            keys              source
#   down              j, Down           default
#   up                Ctrl+n            .gwm.toml
#   top               g g               default
#   …
```

The action column lists the slugs accepted in `[tui.keys]`; the keys column shows every chord bound to that action (comma-separated). An empty keys column means the action is currently unbound (the user explicitly cleared it). Reserved as a sub-tree (`gwm tui …`) so future TUI knobs land without crowding the top-level surface.

Keymap reference and chord grammar: [TUI → Keybindings](/tui/keybindings); the `[tui.keys]` schema: [Configuration → `[tui.keys]`](/configuration/gwm-toml#tuikeys).

## `gwm exec [<slug>...] [--profile <name>] [--jobs <n>] -- <cmd>` (issues #313, #324)

Run a command in each worktree — sequentially by default, or with bounded parallelism — a fleet chore across every worktree of the repo.

```bash
gwm exec -- git fetch                    # run `git fetch` in every non-main worktree
gwm exec feat-1 fix-2 -- cargo check     # scope to two fuzzy-matched worktrees
gwm exec -- git log --oneline -5         # everything after `--` is forwarded verbatim
gwm exec --profile test                  # run the saved [exec.profiles.test] command
gwm exec --jobs 4 -- cargo build         # fan out 4 worktrees at a time
```

Positional slugs **before** `--` scope the set (fuzzy-matched, same matcher as `gwm path` / `remove`); with none, it targets every non-main worktree. Everything **after** `--` is the command, forwarded verbatim — flags and all. gwm prints a `━━ <name> (<path>)` header per worktree, then a per-worktree `✓ / ✗` rollup, and exits non-zero if any worktree's command failed (so you can gate CI on it). An empty target set prints `no worktrees to run in` and exits `0`.

`--profile <name>` runs a saved [`[exec.profiles.<name>]`](/configuration/gwm-toml#exec-and-clean) command instead of an inline one. The profile's `command` is an argv **array** run with **no shell** — the same contract as the inline form, and a deliberate divergence from the shell-line `command` of `[git_tui]` / `[review]`. `--profile` and an inline `-- <cmd>` are **mutually exclusive** (passing both exits `1`); an **unknown** profile name exits `1`.

`--jobs <n>` sets **bounded parallelism**. `1` (the default) runs sequentially with live, inherited output. `> 1` runs up to N worktrees at once, capturing each one's output and printing it as a per-worktree block (in worktree order) once the fan-out finishes — so concurrent runs don't interleave. Precedence: `--jobs` > a profile's [`jobs`](/configuration/gwm-toml#exec-and-clean) > `[exec] jobs` > `1`. The aggregate exit code is unchanged.

This runs the user's own command against their own worktrees, so **no bootstrap trust gate** ([`gwm trust`](#gwm-trust-listrevokeshow-issue-95)) applies. It is **not** journaled into [`gwm history`](#gwm-history---limit-n---all).

## `gwm clean [<slug>...] [--profile <name>] [--yes]` (issues #313, #324)

Report — and optionally reclaim — heavy build artifacts across worktrees. **Report-only by default.**

```bash
gwm clean                                # report reclaimable artifacts in every non-main worktree
gwm clean feat-1                         # scope to a fuzzy-matched worktree
gwm clean --yes                          # actually delete the listed artifacts
gwm clean --profile deep                 # use the [clean.profiles.deep] directory set
```

`gwm clean` scans each target worktree for `target/`, `node_modules/`, `dist/`, and `build/` and prints the reclaimable size per worktree. Positional slugs scope the set (fuzzy); with none, it targets every non-main worktree. Without `--yes` it only reports and prints `re-run with --yes to delete the listed artifacts`; an empty result prints `nothing to reclaim`.

| Flag               | Action                                                                |
|:-------------------|:---------------------------------------------------------------------|
| `--profile <name>` | Reclaim the [`[clean.profiles.<name>]`](/configuration/gwm-toml#exec-and-clean) directory set — a **complete** set that replaces the built-ins (unknown name exits `1`) |
| `--yes`            | Delete the listed artifacts instead of only reporting them            |

Without `--profile`, `gwm clean` uses `[clean.profiles.default]` when that profile is defined, else the built-in `target`/`node_modules`/`dist`/`build`. A profile's `dirs` **replaces** the built-ins (never adds to them); the safety gate below still applies to every directory.

**Safety:** `--yes` deletes a directory **only** when git treats it as ignored *and* it holds no tracked files. A `dist/` or `build/` that is tracked or hand-authored (hence non-regenerable) is reported as `skipped … not git-ignored, or holds tracked files`, never removed. Because the artifacts are regenerable, `gwm clean` is **deliberately not journaled** into [`gwm history`](#gwm-history---limit-n---all) — there is no `gwm undo` for it.

## exit codes

| Code | Meaning                                                                |
|:-----|:-----------------------------------------------------------------------|
| `0`  | success — also "all green" for `gwm doctor`                            |
| `1`  | recoverable failure — fuzzy miss, ambiguous match, doctor Warning      |
| `2`  | hard failure — bootstrap `✗`, doctor Failure, unrecoverable git error  |