gwm

Kylian Bardini·kbrdn1.gwm

git worktree manager - a TUI and CLI over git worktrees

gwm is a terminal UI and CLI over git worktrees, with native libgit2 and a per-repo .gwm.toml bootstrap (file copies, regex guards, lifecycle hooks). The installed command is gwm.

winget install --id kbrdn1.gwm --exact --source winget

Latest 1.10.0·September 1, 2026

Release Notes

[1.10.0] - 2026-09-01

Added

Ctrl+n opens the create form on an issue that already exists (#625). gwm create --issue (#617) derives the whole <type> <issue> <desc> triple from an issue on the forge, and it was CLI-only. The TUI is where a worktree usually gets created, so the one place already showing a worktree list and its linked issues was the one place that still asked for the title to be retyped as a slug and the branch type to be read off the labels by hand.

Ctrl+n (and create-from-issue in the command palette) opens the form on a single field, the issue number. Enter looks the issue up rather than creating anything; when the answer lands the form becomes the ordinary structured form with the type, the number and the derived slug in it, and a second Enter creates the worktree. Prefilling rather than creating is the point:the slug is a guess about a title, and this is the surface that can show the guess before committing to it. The derivation runs through the very functions the CLI uses, so the two cannot produce different slugs for the same title.

Where the CLI has to refuse, the form asks. A non-interactive command has nowhere to ask when the labels name no branch type or name two, which is what --type is for; the form lands the cursor on the type selector with everything else filled. A closed issue prefills with a warning rather than refusing, since nothing is written until you confirm. A number that already has a worktree closes the form and names it, matching the CLI's exit-0 behaviour, and reads the same link gwm list shows, so a worktree attached by hand with gwm link counts too.

The lookup runs on the async task spine, never the render path, and a result is applied only when the form asked for that exact number:the form is a second consumer of a message that also fires for the sidebar prefetch and for an explicit refresh. An issue already in the cache prefills straight away rather than waiting for a message that would never arrive, since the fetch coalesces on a cache hit.

Ctrl+t is inert in this mode and the hint row does not offer it. The toggle swaps between the structured triple and the free-form name, which are two ways of typing the same worktree; this one is a two-step mode, left by answering it or by cancelling, so a third stop on the cycle would only make the key unpredictable.

gwm create --issue <N> opens a worktree for an issue that already exists (#617). gwm new covered the issue that does not exist yet:it renders the issue from [issue_template.by_type.<type>], creates it, and opens its worktree. Nothing covered the other half, the issue a teammate, a bot, or you last week already filed, where the way through was to open the issue in a browser, read its title, decide which branch type its labels implied, and type a kebab-case slug that restated the title. Three of those four steps were transcription, and the transcription drifted.

--issue <N> fetches the issue and derives the triple. <desc> comes from the title with the type's title_prefix taken back off, normalised through the same kebab-case path a hand-typed <desc> goes through and truncated on a word boundary rather than mid-word. The prefix is resolved through the same combinator gwm new uses when it writes the title, falling back to the issue form's own title:, so the two halves of the flow produce the same slug for the same title. <type> comes from the labels: [issue_template.by_type.*].labels is the type-to-labels map gwm new writes with, read backwards. A type declaring no labels is never a candidate, since an empty list says nothing about which issues belong to it.

Nothing is guessed. Labels matching no type, labels matching two, and a repo that never configured the map are three distinct refusals, each naming what it saw and pointing at --type <TYPE>. A closed issue is refused too, because a worktree for one is usually a wrong number, and --force proceeds. A worktree that already carries the number is printed and the command exits 0, so the command is safe to re-run; that check runs before the closed-issue refusal, since an issue closes while its worktree is still alive. It reads the same link gwm list shows, so a worktree attached by hand with gwm link --issue counts too.

--issue is exclusive with the positional triple and with --name, the way --name already is:the mode is chosen explicitly, never inferred from how many arguments were supplied. Everything after the derivation is the existing gwm create path unchanged, #{issue} in branch_pattern included. An issue title is arbitrary text from the forge, so it reaches the slug through the same normaliser as a hand-typed <desc> rather than around it, and the echoed title, URL and labels are sanitised for the terminal.

W opens the Working Tree listing at full size (#592). The sidebar's Working Tree pane is one block among five in a column that is a fraction of the screen, so a worktree with more than a handful of changed files could only be read two rows at a time through J / K. W now opens the same file-explorer tree as a full-size overlay:same icons, same per-category colours, the same change counts on the bottom rule, scrolled with j / k, g / G, closed with Esc / q (or W again, whatever W gets rebound to, see #613 below), and rebindable under [tui.keys.modal.working_tree].

The listing is read when the overlay opens rather than taken from the sidebar's cache, so it does not go blank in the two states where that cache is never built:sidebar hidden, or the Details panel showing stashes. The read runs on a worker and the overlay opens on a loader, so a repository whose untracked walk is slow does not freeze the event loop on the keypress.

The right of each row says how many lines the file gained and lost (+120 -34), from one git diff against HEAD in the same read, so staged and unstaged changes are counted together. A directory, an untracked file and a binary file carry no counts:the first has no diff of its own, and for the other two git counts no lines. The column rides its own rect on the right and is dropped whole on a terminal too narrow to keep it and a readable file name, so the name is never what goes. D / U page the listing by half a screen, and the key is advertised in both pane footers, matching the commit listing (#593). • o on the agents overlay resumes the session in the multiplexer (#591). The overlay told you which agent was working where and then left you to get there by hand. a did not help:it is a pin, it changes gwm's bookkeeping, not where the session runs. o opens a pane running the selected session.

In the worktree the overlay is about, not in the session's recorded directory. A pinned session is pinned precisely because that directory names the wrong tree, and for a pinned Claude session it can be the slug directory under ~/.claude/projects rather than a worktree at all.

Multiplexer only, deliberately. With none active the key says so and does nothing, because the point is to put the session next to gwm and the PTY overlay would cover gwm instead. It opens at the level mux_open_in names, exactly as t does. One target stays refused:a zellij tab takes no trailing command in any form.

herdr works too, in two steps. None of its levels accepts a trailing command, so gwm opens the container, waits for its new shell to reach a prompt, then types the line in through the pane id herdr's response carries. All three of pane split, tab create and workspace create name a pane to run in. The wait is load-bearing rather than defensive:herdr pane run types into the interactive shell instead of exec'ing, so a line sent while the shell is still running its rc files lands in the middle of that output and is dropped, measured on a worktree with direnv and a nix flake where it took about a minute to settle. The whole sequence therefore runs off the event loop, the status bar reads opening agent pane… meanwhile, and it gives up after two minutes rather than leave a worker running.

What the pane runs is [tui.agent_resume], defaulting to claude -r {session}, codex resume {session}, opencode -s {session} and vibe --resume {session}, measured against the installed binaries. They are configuration rather than a hardcoded table because they are four third-party CLIs on their own release cadence. The session id is read out of each tool's own artefacts, so it reaches the shell quoted through a single-pass expander, the same rule the hook placeholders learned in GHSA-fffq-vg6f-gxqm.

A session that has ended resumes without comment; a live one is flagged on the status bar, since resuming it in a second pane while it runs elsewhere may fork or refuse depending on the tool.

c opens the commit listing full size, with load-more (#593). The sidebar's Commits pane is a fraction of a sidebar shared with four other blocks, and it stops at 300 commits:seeing further meant leaving gwm for lazygit. c now paints the same graph on the whole canvas, and m re-reads one page deeper, up to 1500 commits, so history is paged rather than capped. The title carries the row count and a trailing + while a deeper page exists; the load more hint disappears once the revwalk runs out of history or the cap is reached, so the key is never advertised where it would do nothing. The walk runs on

Installer type: zip

x64DBBB349E2E3F787C7C5EE3CFB4BD42175BD52A89ECA89C7CFB213B743EDE5174

Details

Homepage
https://github.com/kbrdn1/gwm-cli
License
MIT
Publisher
Kylian Bardini
Support
https://github.com/kbrdn1/gwm-cli/issues
Copyright
Copyright 2026 Kylian Bardini
Moniker
gwm

Tags

cligitgit-worktreelibgit2rustterminaltuiworktree

Older versions (2)

1.9.0
x646C5B5300F2DFADD6C4A1F8D779D36632101EACFD81571A0142CFDAADADC860C9
1.1.1
x647FE372AEE6E891563FD95A4C8026FCEFF904DA0E8AD9693A5E5D188110C6DF31