# gwm config — copy this to your repo root as `.gwm.toml` # # Global config (issue #190): the same schema may live at # `~/.config/gwm/config.toml` ($XDG_CONFIG_HOME/gwm/config.toml). It is # merged UNDERNEATH this repo file — set a preference once (e.g. # `[theme] preset = "…"`) and every repo inherits it. The repo `.gwm.toml` # always wins on conflict: scalars override, tables merge key-by-key, # arrays ([[labels]], [[bootstrap.copy]], …) are replaced wholesale. # Set GWM_NO_GLOBAL_CONFIG=1 to ignore the global file (repo-only). # # placeholders supported in paths/patterns: # {repo} repo name {home} your home directory # {repo_path} repo's absolute dir {repo_parent} the dir *containing* the repo # {type} {issue} {desc} # `{repo_parent}` lets the base sit next to the repo — e.g. # base = "{repo_parent}/worktrees" → a sibling `worktrees/` dir, # matching an editor's `../worktrees` convention (Zed git.worktree_directory). # # === TOFU trust gate (issue #95) =========================================== # The first `gwm create` / `gwm bootstrap` against this file opens a # one-shot trust prompt that summarises the bootstrap surface (copies, # guards, no-symlinks, commands) and records the (origin URL, sha256) # pair in `~/.config/gwm/trust.toml` on approval. Any byte change to this # file re-prompts (whitespace matters — `rm -rf /tmp/` and # `rm -rf /tmp /` differ by one byte and behave catastrophically # differently). CI runners bypass the prompt with `--allow-bootstrap` or # `GWM_ALLOW_BOOTSTRAP=1`. Forensic mode: `--deny-bootstrap` refuses # even when trusted. Manage entries with `gwm trust list / revoke / show`. # Full threat model: https://github.com/kbrdn1/gwm-cli/blob/main/src/trust.rs # === forge (issue #419) ==================================================== # Which code-hosting platform backs issue / PR lookups: "github" (shells out # to `gh`) or "gitlab" (shells out to `glab`, and says "MR" where GitHub says # "PR"). Everything else — branch links, worktrees, bootstrap — is identical. # # Omit the key and gwm infers the forge from the `origin` host, which covers # github.com, ghe.com and gitlab.com. A SELF-HOSTED instance lives on an # arbitrary domain and cannot be detected from the URL alone, so name it: # # forge = "gitlab" # # The key always wins over inference, so it also forces GitHub on a host that # would otherwise be read as GitLab. # # NOTE: on a host gwm does not recognise, this key names the BACKEND but does # not authorise the HOST — it ships with the repo, so on its own it would let # a clone point `gh` / `glab` at any server it likes, carrying whatever token # is in your environment. Either approve this repo once with `gwm trust add`, # or authorise the host in YOUR OWN ~/.config/gwm/config.toml (that file is # never shipped with a repo, which is what makes it an answer): # # [forge_hosts] # "gitlab.acme.com" = "gitlab" # "ghe.acme.com" = "github" # # `forge_hosts` is read from the user-level config ONLY — setting it here in a # repo's .gwm.toml has no effect, by design. [worktree] base = "{home}/cc-worktree/{repo}" # Repo-relative alternative (sibling `worktrees/`, à la Zed `../worktrees`): # base = "{repo_parent}/worktrees" path_pattern = "{type}-{issue}-{desc}" branch_pattern = "{type}/#{issue}-{desc}" # --- file copies from main repo into the new worktree ----------------------- [[bootstrap.copy]] from = ".env.testing" to = ".env.testing" required = true fallback = "inline" [[bootstrap.copy]] from = ".env" to = ".env" required = false guards = ["no-aws-rds"] # --- regex guards on copied files ------------------------------------------- [[bootstrap.guard]] name = "no-aws-rds" deny_patterns = ["amazonaws\\.com", "\\.rds\\."] on_match = "seed-from-example" example_file = ".env.example" # --- inline fallback when a required copy source is missing ----------------- [bootstrap.fallback.env_testing] target = ".env.testing" content = """ # Auto-generated by gwm. Safe sqlite :memory: defaults. APP_ENV=testing APP_DEBUG=true APP_URL=http://localhost APP_KEY= DB_CONNECTION=sqlite DB_DATABASE=:memory: CACHE_STORE=array QUEUE_CONNECTION=sync MAIL_MAILER=array SESSION_DRIVER=array BCRYPT_ROUNDS=4 """ # --- refuse to inherit symlinks (e.g. vendor/ from main repo) --------------- [[bootstrap.no_symlink]] path = "vendor" [[bootstrap.no_symlink]] path = "node_modules" # --- lifecycle hooks -------------------------------------------------------- # Phases: pre_create, post_create, pre_bootstrap, post_bootstrap, pre_remove, # post_remove. Placeholders: {branch} {path} {type} {issue} {desc} {user} # {owner} {repo}. Failure handling: on_fail = "abort" (default), "warn", # or "ignore". Emergency bypass: --skip-hooks ; `gwm remove # --force` implies --skip-hooks pre_remove,post_remove. [[hooks.post_create]] name = "composer install" run = "composer install --no-interaction --prefer-dist" when = "file_exists:composer.json" on_fail = "warn" env = { COMPOSER_IGNORE_PLATFORM_REQ = "ext-imagick" } [[hooks.post_create]] name = "direnv allow" run = "direnv allow ." when = "file_exists:.envrc" on_fail = "ignore" [[hooks.post_create]] name = "npm install" run = "npm install" when = "file_exists:package.json" # --- legacy post-create commands ------------------------------------------- # `[[bootstrap.command]]` is still accepted for compatibility. New configs # should prefer `[[hooks.post_create]]`; if both legacy commands and any # `[hooks.*]` entries are present, gwm warns and runs legacy commands as # additional post_create hooks. # --- composable when predicates -------------------------------------------- # Atoms: file_exists / cmd_exists / env_set / env_eq / glob_exists # Operators: ! (NOT), && (AND), || (OR) — precedence: ! > && > || [[hooks.post_create]] name = "install (bun if available)" run = "bun install" when = "file_exists:package.json && cmd_exists:bun" [[hooks.post_create]] name = "install (npm fallback)" run = "npm ci" when = "file_exists:package.json && !cmd_exists:bun" [[hooks.post_create]] name = "full local build" run = "./scripts/full-build.sh" when = "glob_exists:src/**/*.rs && !env_set:CI" # --- branch types (issue #80) ----------------------------------------------- # Per-repo override of the allowed branch types. Absent (default) ⇒ the # built-in list is used: feat, fix, hotfix, docs, test, refactor, chore, # perf, ci, build. Present ⇒ only the listed types are accepted by # `gwm create` / TUI create / `BranchSpec::validate()`. `gwm types` # prints the resolved set with a footer noting the source # (`(source: built-in defaults)` vs `(source: .gwm.toml)`). # # An empty array (`branch_types = []`) is treated as absent — the # built-in list is restored so an accidental wipe doesn't lock you out # of `gwm create`. # # [[branch_types]] # name = "feat" # description = "New feature implementation" # # [[branch_types]] # name = "fix" # description = "Bug fix" # # [[branch_types]] # name = "migration" # description = "Database migration" # --- gitmoji mapping (issue #85) -------------------------------------------- # Per-repo override of the built-in `branch_type → :shortcode:` table used # by `gwm commit-prefix`, `gwm types --gitmoji`, and the bundled # `commit-msg` hook (`gwm hooks install commit-msg`). Absent (default) ⇒ # the built-in table is used: # # feat → :sparkles: ✨ # fix → :bug: 🐛 # hotfix → :ambulance: 🚑 # docs → :memo: 📝 # test → :white_check_mark: ✅ # refactor → :recycle: ♻ # chore → :wrench: 🔧 # perf → :zap: ⚡ # ci → :construction_worker: 👷 # build → :package: 📦 # # `[gitmoji]` is **additive** — overriding one entry doesn't wipe the # other nine. You can also map custom branch types (e.g. `migration`) # without redeclaring the built-ins. # # Surface: # gwm commit-prefix → ":sparkles: feat(#41):" (HEAD) # gwm commit-prefix --unicode → "✨ feat(#41):" # gwm commit-prefix --branch feat/#7-x → ":sparkles: feat(#7):" (named) # gwm types --gitmoji → branch types + emoji columns # gwm hooks install commit-msg → opt-in auto-prepend on `git commit` # # `--unicode` normalisation of overrides: # With `feat = ":rocket:"`: # gwm commit-prefix --branch feat/#1-x → ":rocket: feat(#1):" # gwm commit-prefix --branch feat/#1-x --unicode → "🚀 feat(#1):" # With `feat = ":foo:"` (shortcode unknown to gwm's built-in table): # gwm commit-prefix --branch feat/#1-x --unicode → ":foo: feat(#1):" # Unknown shortcodes fall through verbatim — no panic, no # `:question:` substitution. The known-shortcode set covers the ten # built-in mappings plus the most commonly-swapped Gitmoji entries # (`:rocket:`, `:fire:`, `:lock:`, `:art:`, `:lipstick:`, `:hammer:`, # `:bookmark:`, …). # # [gitmoji] # feat = ":rocket:" # team uses 🚀 for new features instead of ✨ # migration = ":truck:" # custom branch type — emoji wins on `gwm types --gitmoji` # --- doctor knobs ----------------------------------------------------------- # Trunk branches `gwm doctor`'s orphan-branch check treats as merge # destinations. A gwm-style branch fully reachable from one of these is # preserved per CONTRIBUTING ("never delete the source branch after # merge") and is NOT flagged as orphan. Default: ["dev", "main"]. Repos # with non-standard trunk conventions (e.g. release trains) opt in here. # An empty list (`trunks = []`) disables the filter — every unclaimed # gwm-style branch becomes an orphan warning. # # [doctor] # trunks = ["master", "release-3.x", "release-4.x"] # --- TUI knobs -------------------------------------------------------------- # Safety countdown (in seconds) applied to the delete-confirm overlay when # `delete branch on remove` is armed (`p` in the TUI). The high-risk path # becomes a two-step `y → countdown → auto-fire`; `y` during the countdown # cancels. Range: 0..=5. `0` disables the countdown (classic single- # keystroke confirm even when `p` is armed); values above `5` are clamped # to `5` on read. Default: 3. # # [tui] # confirm_countdown_secs = 3 # # Auto-refresh the TUI worktree list/status every N seconds so Issue/PR # markers and branch metadata do not stay stale while the session is open. # `0` disables periodic refresh. Default: 60. # # auto_refresh_secs = 60 # --- TUI sidebar layout (issues #188 / #365) --------------------------------- # `sidebar_position` — which side the worktree-details preview sidebar sits on # in the side-by-side layout: "right" (default, pre-#188 behaviour) or "left". # Toggle it live in the TUI with `v`. The stacked layout ignores this: there the # sidebar is always at the bottom. # # `sidebar_orientation` — how the sidebar is arranged relative to the table: # "stacked" table on top, sidebar below (default since #217) # "side-by-side" always beside the table, even on a narrow terminal # "auto" side-by-side at >= 120 columns, stacked below that # Cycle it live with `z`. Before #365 this reset on every launch; set it here to # make the choice stick. An unknown value is a hard error at load time. # # [tui] # sidebar_position = "left" # sidebar_orientation = "side-by-side" # --- TUI layout (issue #545) ------------------------------------------------- # How panes and sidebar sections are framed: # "compact" (DEFAULT) no box rules; each section is delimited by a filled # one-line header. Buys back two rows and two columns per # section: the title keeps its bracketed keybinding and goes # uppercase (`[1] WORKTREES`, `ISSUE / PR [F]`), the counter sits # at the right of that same line instead of in a bottom rule, a # muted rule marks the boundary between the two panes, and the # worktrees pane sizes itself to its row count rather than # reserving its full share of the stacked split. # "bordered" the lazygit-style boxes — gwm's layout up to 1.7. Left # deliberately untouched by the compact refinements (no dimming, # no separator rule) so it stays a faithful restore. # # Focus reads on the header in compact mode, where the border colour used to # say it: the active pane's header wears the `focus` role and the # `selection_bg` fill, the others `muted` over `section_bg`. # # `dim_unfocused` additionally dims the BODY of whichever pane does not hold # focus, in either layout. Off by default: it trades contrast for a stronger # "where am I" cue, and the inactive pane's content is still readable # information. Uses the terminal's DIM attribute, so semantic colours survive # (a dirty branch stays yellow); a terminal that ignores DIM renders as if the # option were off. # # [tui] # dim_unfocused = true # # The header fill is the `section_bg` theme role (`gwm theme show` lists it # with the rest), an indexed colour rather than a translucent white so it # survives a terminal without truecolor. # # Overlays and modals keep their border under either value. # # `status_one_line` folds the sidebar's Status block onto a single row — # branch, head, state badges, diff and age joined by ` · ` — instead of one # labelled row per value. ON by default (#547): four rows for four short # values was the largest waste left in the sidebar. It is independent of # `layout`, so `bordered` folds too; set it to false for the labelled block. # The `Path` row never folds in. Under width pressure the row is clipped on # the right, so the age goes first and the branch survives. # # [tui] # status_one_line = false # # [tui] # layout = "bordered" # --- TUI note editor: vim normal mode (issue #557) --------------------------- # `note_vim` gives the in-TUI note editor (`N`) a vim normal mode. ON by # default, and the cost is `Esc`: it leaves insert instead of writing and # closing, so saving takes two presses. Turn it off for the modeless editor, # where one `Esc` writes and closes. # # On, `N` opens in normal mode, the modal title carries a NORMAL / INSERT chip # and the statusbar under it lists that mode's keys. `i` / `I` / `a` / `A` / # `o` / `O` enter insert; `hjkl`, `w` / `b` / `e` (and `W` / `B` / `E`), # `0` / `^` / `$`, `gg` / `G`, `x` and `dd` are the motions. No counts, no # registers, no undo: `Ctrl+e` hands the file to the real vim for anything # past that. # # It changes no binding. `[tui.keys.modal.note]` holds the same four verbs # either way (`close`, `open_editor`, `toggle_bullet`, `toggle_checkbox`), and # an unmodified printable bound to one of them is still refused at load time. # # [tui] # note_vim = false # --- What a mux spawn opens (issues #589 / #608) ----------------------------- # `mux_open_in` — what the TUI's `t` key opens in the multiplexer: # "pane" split the current pane (DEFAULT) # "tab" a whole screen: a tmux window, a zellij or herdr tab # "workspace" herdr's level above a tab. HERDR ONLY. # # "workspace" is REFUSED on tmux and zellij, not downgraded: neither has a # level there, so `t` says so on the status bar and opens nothing. A silent # tab would leave this setting describing something that did not happen. # (Both have sessions, the structural analogue, but gwm runs inside one: # tmux would need two commands to create and switch to a sibling, and zellij # refuses to nest sessions.) # # `mux_pane_direction` — which half a pane takes, under "pane" only: # "right" side by side (DEFAULT since #589) # "down" stacked below # "left" side by side, other side (tmux `-h -b`, zellij; REFUSED by herdr) # "up" stacked above (tmux `-v -b`, zellij; REFUSED by herdr) # # `right` is a behaviour change for tmux and zellij users, and a deliberate # one. Before #589 a split carried no direction at all, so each backend # answered for itself: tmux fell back to `-v` and stacked, zellij took "the # biggest available space", herdr went right because gwm hardcoded it. `right` # is what the `--split` help has promised since it shipped, and the half that # is free on a wide screen. Set "down" for the old tmux behaviour. # # `-b` ("before") flips the side on the axis `-h` / `-v` picked, which is where # left and up come from on tmux. herdr declares `[possible values: right, down]` # and REFUSES the other two rather than substituting one it does have. # # `gwm tmux|zellij|herdr --direction ` overrides the # direction for one invocation. The CLI spells its own target instead (bare is # a tab, `-p` is a pane), so `mux_open_in` does not reach it. # # A `[tui.macro*]` with `open_in = "mux_pane"` reads both, with one caveat: # under "tab" a zellij macro and under "workspace" every macro falls back to # the PTY overlay, because those verbs take no trailing command to run. # # [tui] # mux_open_in = "workspace" # mux_pane_direction = "down" # --- Terminal browser (issue #590) ------------------------------------------- # `terminal_browser` is a command that renders a URL INSIDE the terminal, so a # link opens next to gwm instead of pulling you out of the workspace gwm is # sitting in. It covers every link the TUI opens: the browse-links menu (`B`), # the open-menu Issue and PR picks, a row in the rich PR/issue view, a CI # check's details URL, and `.` for the docs. # # The `{url}` placeholder is optional: a bare "w3m" gets the URL appended as # its last argument, which w3m / lynx / carbonyl / browsh all take anyway. # # ONLY consulted when a multiplexer is detected ($TMUX / $ZELLIJ / $HERDR_ENV). # A terminal browser with nowhere to put it is worse than the system browser, # so unset (the DEFAULT) is gwm's behaviour up to 1.9 on every platform: every # link goes to `open` / `xdg-open` / `explorer`. The level a pane opens at is # `mux_open_in` / `mux_pane_direction` above, the pair `t` and `o` read. # # Where the container takes no command (herdr, a zellij tab, any "workspace") # the browser runs in the PTY overlay instead, and the status bar names the # backend that refused. A browser missing from $PATH falls back to the system # browser, with the binary named. # # The URL is always ONE argument: the template is tokenised BEFORE the # placeholder is substituted, so `w3m {url}` and `w3m "{url}"` are the same # command and a URL's `?`, `&` and `#` cannot become shell syntax. Only # absolute http/https URLs are passed on. An empty string reads as unset. # # [tui] # terminal_browser = "w3m {url}" # --- TUI clipboard (issue #367) ---------------------------------------------- # How yanked text (path / branch / worktree name / command logs) reaches the # clipboard: # "auto" OSC52 when an SSH session is detected, host tools otherwise (default) # "osc52" always emit the OSC52 escape sequence # "tools" always use pbcopy / wl-copy / xclip / xsel / clip.exe # # Why "auto" matters over SSH: the host tools write to the clipboard of the # machine gwm runs on. On a remote host `pbcopy` is found, succeeds, and reports # success — while the text lands in the *remote* clipboard, out of reach. OSC52 # hands the text to your terminal emulator instead. # # Caveats, because OSC52 is never acknowledged by the terminal: # - Under tmux, gwm wraps the sequence in DCS passthrough, but tmux needs # `set -g allow-passthrough on` (off by default since tmux 3.3). # - Inside GNU screen ($STY), gwm falls back to the host tools rather than # emit a sequence screen would silently swallow. # - Terminal support varies (kitty / WezTerm / Alacritty / iTerm2 yes, # Terminal.app no). Use "tools" if your terminal drops it. # # [tui] # clipboard = "auto" # --- TUI keymap: `[tui.keys]` (issues #87 / #219 / #294) --------------------- # Remap any TUI key. An override REPLACES the default for that verb; an empty # array unbinds it. Run `gwm tui keys` for the full list of actions, contexts, # and verbs, and `gwm doctor` to validate overrides. You can also edit every # binding live from inside the TUI: open the Settings panel (`4`), tab to # `Keys`, select a binding and press the activate key to capture a new key — # it writes the same TOML shown below to the layer the panel targets (#294). # # Global list-view verbs are arrays keyed by action slug (multi-key chords # like "g g" allowed here): # [tui.keys] # quit = ["q", "Esc"] # down = ["j", "Down"] # top = ["g g"] # # Modal / overlay verbs are nested under the dedicated `[tui.keys.modal]` # namespace, keyed by context (issue #219). The separate namespace keeps a # modal context from colliding with a same-named global action (create / help / # command_logs / link are both): a global `create` array and a modal # `[tui.keys.modal.create]` table can coexist. These are SINGLE keystrokes # only — modals have no chord timeout. The same physical key can mean different # verbs in different contexts: # [tui.keys.modal.create] # next_field = ["Tab"] # prev_field = ["BackTab"] # submit = ["Enter"] # cancel = ["Esc"] # # [tui.keys.modal.confirm] # confirm = ["y"] # cancel = ["n", "Esc"] # focus_confirm = ["Left", "h"] # focus_cancel = ["Right", "l"] # activate = ["Enter"] # # The link prompt and the settings editor are two-stage, addressed with a # dotted context path: # [tui.keys.modal.link.choose_target] # issue = ["i"] # pr = ["p"] # next = ["j", "Down"] # prev = ["k", "Up"] # accept = ["Enter"] # cancel = ["Esc"] # # [tui.keys.modal.link.input_number] # submit = ["Enter"] # cancel = ["Esc"] # # [tui.keys.modal.config.edit] # submit = ["Enter"] # cancel = ["Esc"] # --- TUI launcher: `l` (git_tui) keybinding (issue #75) ---------------------- # The `l` key suspends the gwm TUI and launches a git TUI on the selected # worktree. Placeholders: {path}. Default (no section) → `lazygit -p {path}` # fullscreen=true, identical to the pre-issue-#75 hardcoded behaviour. # # Switch to gitui: # [git_tui] # command = "gitui -d {path}" # fullscreen = true # # Or to a non-TUI tool (gwm stays visible, stderr first-line lands in the # status bar): # [git_tui] # command = "code {path}" # fullscreen = false # --- TUI launcher: `R` (review) keybinding (issue #75) ----------------------- # The `R` key runs a code-review tool against the resolved base ref. # Placeholders in `command`: # {base} — resolved base (upstream → branch..gwm-base → default_base # → "dev" → "main") # {head} — selected worktree's branch name # {path} — absolute path of the selected worktree # {diff} — path to a temp file holding `git diff {base}..{head}` # (lazily materialised; only created if the template uses it) # # Built-in presets — pick one with `tool = ""` instead of `command`: # lumen → "lumen diff {base}..{head}" · fullscreen=true # claude → "claude --print 'review the diff {base}..{head}'" · fullscreen=false # codex → "codex review {base}..{head}" · fullscreen=false # aider → "aider --message 'review {base}..{head}'" · fullscreen=true # gh → "gh pr view --web" · fullscreen=false # # When both `command` and `tool` are set, `command` wins (the TUI flags # the shadow in the status bar). Setting neither leaves `R` inert with # a status-bar hint. # # [review] # tool = "lumen" # skip_when_no_changes = true # default true — skip when `git rev-list --count {base}..{head} == 0` # # default_base = "dev" # optional pin overriding the auto-discovery chain # # Explicit form (overrides any preset): # [review] # command = "lumen diff {base}..{head}" # fullscreen = true # skip_when_no_changes = true # --- `gwm exec` profiles (issue #324) ---------------------------------------- # Saved commands for `gwm exec --profile `. Each profile carries an # argv ARRAY — run with NO shell, exactly like the inline `gwm exec -- `. # # ⚠️ Divergence to note: `[exec.profiles.*].command` is an argv ARRAY # (`["cargo", "test"]`), whereas the `command` of `[git_tui]` / `[review]` # above is a single SHELL line (`"lazygit -p {path}"`). exec deliberately # avoids a shell — no word-splitting, no globbing, no `{path}` placeholders; # the program is run verbatim in each worktree. This is frozen for 1.0. # # `--profile` and an inline `-- ` are mutually exclusive (exit 1), and an # unknown profile name exits 1. # # `jobs` is bounded parallelism: `[exec] jobs` is the global default, a # profile's `jobs` overrides it, and the `--jobs ` flag wins over both # (precedence: --jobs > profile.jobs > [exec] jobs > 1). `1`/absent runs # sequentially with live output; `> 1` runs up to N worktrees at once and # prints each one's captured output as a block at the end. # # [exec] # jobs = 1 # # [exec.profiles.test] # command = ["cargo", "test"] # # [exec.profiles.fmt] # command = ["cargo", "fmt", "--all"] # jobs = 4 # # A profile can run its command in a container instead of on the host (issue # #421). `image` is required; `runtime` is auto-detected (docker first, then # podman) and accepts any Docker-compatible CLI; `extra_args` is forwarded to # `run` after gwm's own flags (so a repeated flag overrides them) and before # the image. The block rides a PROFILE only: an inline `gwm exec -- ` # always runs on the host, whatever this file says. # # gwm mirrors host paths and mounts the main checkout's gitdir alongside: # run --rm -v : -v
/.git:
/.git -w # That second mount is the point. A linked worktree's `.git` is a FILE holding # an absolute host path, so mounting the worktree alone yields a container in # which no git command answers. Every mounted path is declared `safe.directory` # via GIT_CONFIG_* env, so a rootful Docker on Linux (uid 0 vs a tree owned by # the host user) does not refuse the repo as `dubious ownership`. # # There is no `interactive` knob: `gwm exec` is a fan-out over N worktrees, # where a TTY per container means nothing. The TUI exec overlay owns a real # pty, so IT runs the container with `-i -t`, names the container and removes # it when the overlay closes (killing a `docker run` client does not stop the # container). Not supported on Windows: host paths cannot be mirrored into a # Linux container. A worktree path containing a `:` is refused too, since that # is the field separator of `-v source:destination`. # # On an SELinux-enforcing host (Fedora, RHEL), add `selinux_relabel = true` to # suffix gwm's own mounts with `:z`. Off by default because relabelling # writes a shared label to the host tree, recursively. # # [exec.profiles.ci] # command = ["cargo", "test", "--all-features"] # # [exec.profiles.ci.container] # image = "rust:1.90" # runtime = "podman" # extra_args = ["-e", "CI=1", "-v", "gwm-cargo:/usr/local/cargo/registry"] # --- `gwm clean` profiles (issue #324) --------------------------------------- # Saved directory sets for `gwm clean --profile `. A profile's `dirs` is # a COMPLETE set that REPLACES the built-in target/node_modules/dist/build — # it never adds to them. The safety gate (git-ignored + no tracked files + # skip symlinks) still applies to every listed directory. # # The `default` profile is special: `gwm clean` WITHOUT `--profile` uses it # when present, else falls back to the built-in four. An unknown `--profile` # name exits 1. # # [clean.profiles.default] # dirs = ["target", "node_modules", "dist", "build", "coverage", ".turbo"] # # [clean.profiles.deep] # dirs = ["target", "node_modules", "dist", "build", ".cache", ".venv"] # --- agent resume commands (issue #591) ------------------------------------- # What `o` runs in the new multiplexer pane when you resume a session from the # agents overlay (`a`). `{session}` is the detected session id; it is read out # of the tool's own artefacts, so gwm refuses to resume unless it is a plain id # (letters, digits, `-`, `_`, `.`) and quotes it on top. Quoting alone would not # hold if YOUR template puts the placeholder inside double quotes. # # The LEVEL the pane opens at is not set here: `o` reads `mux_open_in` and # `mux_pane_direction` above, the same pair `t` reads. # # The values below ARE the defaults, so this block only earns its keep when # one of the four CLIs changes its flags before gwm catches up. An empty # string reads as unset and restores the default. # # [tui.agent_resume] # claude = "claude -r {session}" # codex = "codex resume {session}" # opencode = "opencode -s {session}" # vibe = "vibe --resume {session}" # --- `o: open` dispatch (issue #73) ----------------------------------------- # How the `o` key behaves on the highlighted worktree. Three modes: # "shell" — suspend the TUI and spawn $SHELL with cwd = worktree. # Lazygit-style. Default. Exit the shell ⇒ TUI restores. # "editor" — suspend the TUI and run $EDITOR . # "finder" — pre-#73 behaviour: hand off to the OS file manager # (`open` / `xdg-open` / `explorer`). # `shell_cmd` / `editor_cmd` override the env var when set (empty string # = unset). An unknown `mode` is a hard config error at load time. # # [tui.open] # mode = "shell" # shell_cmd = "" # leave empty to read $SHELL # editor_cmd = "hx" # example: Helix # --- declarative GitHub labels (issue #81) ---------------------------------- # Each `[[labels]]` entry declares a GitHub label that `gwm labels push` will # create or update on the `origin` remote via `gh label create --force`. # # Fields: # name — required. The GitHub label name. Whitespace is preserved # verbatim (e.g. "good first issue"); use `name = "…"`, not a # bare key. # description — optional. Empty / absent means "leave the description # untouched on the remote". # color — optional. 6-hex lowercase (no leading `#` — `#d73a4a` is # accepted and normalised). When omitted, gwm picks a # deterministic pastel from a hash of the name so the same # label gets the same colour across repos. `--random-colors` # overrides this to a random pastel per push. # # Workflow: # gwm labels list — print the resolved set + the diff against remote # (`+ create`, `~ update`, `= match`, `- extra`). # gwm labels push — apply the diff (create + update). # gwm labels push --dry-run — same as list, but with an explicit summary # line ("would create 2, update 1, …"). # gwm labels push --prune — also delete labels on the remote that are # NOT in `[[labels]]`. Destructive, off by # default. # # Requires `gh` on $PATH (the same soft dependency as `gwm status`). # # [[labels]] # name = "bug" # description = "Something isn't working" # color = "d73a4a" # # [[labels]] # name = "enhancement" # description = "New feature or request" # # [[labels]] # name = "good first issue" # description = "Good for newcomers" # color = "7057ff" # --- declarative GitHub milestones (issue #82) ------------------------------ # Each `[[milestones]]` entry declares a GitHub milestone that `gwm # milestones push` will create or update on the `origin` remote via the # GitHub REST API (no native `gh milestone` subcommand exists). # # Fields: # title — required. The GitHub milestone title (unique per repo). # description — optional. Empty / absent means "leave the description # untouched on the remote". # due_on — optional. Accepts `YYYY-MM-DD` (materialised as # 23:59:59 UTC of that day — "due Friday" closes at end # of day, not midnight) or full RFC3339 # (`2026-07-15T17:00:00Z`). Absent means no due date. # state — optional. `"open"` (default) or `"closed"`. Use # `"closed"` to archive a milestone declaratively. # # Workflow: # gwm milestones list — print the resolved set + the diff against # remote (`+ create`, `~ update`, `= match`, # `- extra`). # gwm milestones push — apply the diff (create + update). # gwm milestones push --dry-run — same as list, but with an explicit # summary line ("would create 2, # update 1, …"). # gwm milestones push --prune — also delete milestones on the remote # that are NOT in `[[milestones]]`. # Destructive, off by default. # # Requires `gh` on $PATH (same soft dependency as `gwm labels` / `gwm # status`). # # [[milestones]] # title = "v0.7.0" # description = "Configurability sprint" # due_on = "2026-07-15" # state = "open" # # [[milestones]] # title = "v0.8.0" # due_on = "2026-10-01T17:00:00Z" # # [[milestones]] # title = "v0.6.0" # state = "closed" # --- issue templates for `gwm new` (issue #83) ------------------------------ # `gwm new ` renders one of `.github/ISSUE_TEMPLATE/*.yml`, # creates the issue with `gh issue create`, captures the issue number, then # delegates to `gwm create `. # # [issue_template] # default = "feature_request.yml" # # [issue_template.by_type] # feat = { template = "feature_request.yml", surface = "cli", title_prefix = "[Feature]: ", labels = ["enhancement"] } # fix = { template = "bug_report.yml", surface = "cli", title_prefix = "[Bug]: " } # docs = { template = "task.yml", title_prefix = "[Docs]: " } # hotfix = { template = "bug_report.yml", surface = "cli", title_prefix = "[Hotfix]: ", labels = ["priority: high"] } # --- PR templates for `gwm pr` (issue #84) ---------------------------------- # `gwm pr [--draft] [--base ] [--render]` renders a per-branch-type PR # body and shells out to `gh pr create`. Bodies can be a workdir-relative # Markdown file (`path = …`) or an inline string (`body = "…"`). Placeholders # `{type}`, `{issue}`, `{desc}`, `{base}`, `{head}`, `{repo}`, `{commits}` # (`- subject` lines from `git log base..head`), and `{files_changed}` # (`git diff --stat base..head`, capped at 30 lines) are substituted before # the body lands on the temp file `gh pr create --body-file` reads. # # `gwm pr --render` skips `gh` entirely and prints the rendered body to # stdout so you can pipe it elsewhere: `gwm pr --render | gh pr create # --body-file -`. # # [pr_template] # default = ".github/pull_request_template.md" # # [pr_template.by_type] # feat = { path = ".github/pr-templates/feat.md" } # fix = { path = ".github/pr-templates/fix.md" } # docs = { path = ".github/pr-templates/docs.md" } # # [pr_template.by_type.chore] # body = """ # ## Summary # {desc} # # Closes #{issue} # # ## Test plan # - [ ] cargo test # """ # --- CLI aliases (issue #86) ------------------------------------------------ # Mirror of `git config`'s `[alias]` block. Each entry maps a short # name to an argv-substituted expansion run BEFORE clap parses, so # `gwm wip` becomes `gwm create feat 0 wip` for the dispatcher. # # Resolution order (highest precedence first): # 1. Built-in subcommands (`gwm list`, `gwm switch`, …) — never # shadowable. # 2. `visible_alias` set on built-in subcommands (`s → switch`, # `cd → path`) — also never shadowable. # 3. This `[aliases]` block (repo-level, follows the repo). # 4. `~/.config/gwm/aliases.toml` (user-level, lowest precedence). # # Rules: # - Aliases are argv substitution only — `gwm ` becomes # `gwm `. NO shell pipelines: values containing # `&&`, `||`, `|`, `;`, or backticks are refused at load time. Use a # shell alias if you need shell semantics (`alias gwmwip='gwm wip && # lazygit'`). # - Names that shadow a built-in subcommand or visible alias are a # hard config error surfaced at `Config::load_for_repo` time. # - Single-pass expansion — `wip = "ll"` followed by `ll = "list # --format names"` expands ONCE and then dispatches. # # Surface the resolved chain (built-in + repo + user, with shadowing # flagged inline) via: # # gwm aliases list # # Example: # # [aliases] # wip = "create feat 0 wip" # ll = "list --format names" # sync = "bootstrap"