Skip to content
gwmgwmgwmv1.10.0

gwm doctor

gwm doctor runs a battery of cheap checks against the current repo, prints a per-check report, and exits with a meaningful code so it can be wired into CI or a pre-commit hook without parsing stdout.

Coloured gwm doctor output with ✓ sigils

Code Meaning Triggered by
0 all green every check returns ✓
1 warning at least one !, no ✗
2 failure at least one ✗

The Warning / Failure split matches the bootstrap pipeline’s sigil convention. See Bootstrap pipeline.

Terminal window
$ gwm doctor
✓ .gwm.toml parses
/path/to/repo/.gwm.toml parses cleanly
✓ guard references resolve
2 guard reference(s) resolve
✓ `when` predicates supported
1 predicate(s) recognised
✓ external binaries on PATH
3/3 binaries found
✓ no prunable worktrees
5 worktree(s) tracked, none prunable
✓ no orphan gwm branches
7 merged gwm-style branch(es) preserved per CONTRIBUTING, no unmerged orphans
✓ base directory writable
/home/you/cc-worktree/myrepo is writable
✓ [tui.keys] keymap resolves
14 action(s) bound

A run with issues:

! no prunable worktrees
1 prunable entry: feat-12-old
→ run `gwm prune` to clear them
! no orphan gwm branches
1 unmerged orphan branch(es): feat/#99-wip-experiment
→ git branch -d feat/#99-wip-experiment

Each Warning / Failure carries a one-line remediation hint, copy-pasteable.

Reads .gwm.toml from the repo root.

  • ✓: file parses cleanly, OR file is absent (defaults assumed)
  • ✗: TOML is malformed, or contains an unknown [tui.open].mode value

The “absent” case is intentionally a ✓: gwm works without .gwm.toml and the user-facing message says so.

Walks every [[bootstrap.copy]].guards = [...] and verifies that each name resolves to an existing [[bootstrap.guard]].

  • ✓: every reference points at a real guard
  • ✗: at least one reference is dangling

Catches typos like guards = ["no-aws-dbs"] when the guard is no-aws-rds.

Parses every when predicate, on [[bootstrap.command]] and on all six [hooks.*] phases alike, and reports unknown keywords. A failure names where the atom came from: command \install`orhook post_create `install``.

  • ✓: every atom uses a recognised keyword
  • ✗: at least one keyword is unrecognised. The offending step still runs, since an unknown atom evaluates to true, but the condition its author meant to impose is gone, and this check is the only thing that catches the typo. Failure rather than Warning, so the exit code is 2

Recognised keywords: file_exists:, cmd_exists:, env_set:, env_eq:, glob_exists:. Boolean composition (!, &&, ||) is also validated. See when: predicates.

Probes $PATH for every binary gwm or .gwm.toml plans to spawn:

  • lazygit: only if [git_tui] doesn’t override the command
  • the first token of [git_tui].command: when overridden
  • the first token of [review].command / preset
  • direnv: only if .envrc exists in the worktree (the seed-from-example action may try direnv allow)
  • the first executable token of every [[bootstrap.command]].run
  • the first executable token of every [hooks.*].run, across all six lifecycle phases: a stack preset puts its install command there rather than in [[bootstrap.command]], so leaving hooks out meant the doctor cleared a config whose very first hook was about to fail

A step whose when predicate is false is skipped, since it is not going to run. This is what keeps the node preset green: it ships bun install under cmd_exists:bun and npm ci under !cmd_exists:bun, so probing both would always warn about one of the two. The predicate is evaluated against the main checkout rather than the future worktree, the same approximation as the .envrc probe above. An unrecognised keyword evaluates to true, so the step stays probed, which matches it still running at bootstrap time.

Only two predicate shapes get evaluated, because a .gwm.toml has not been through the trust gate: cmd_exists: on a bare binary name, which is a $PATH lookup on the same set this check reports on, and file_exists: on a single repo-root component that is not itself a symlink, which is a stat on something the config’s own author committed. Everything else is a way out of the repo for a file nobody vetted, and one declined atom leaves the whole expression unevaluated: glob_exists: chooses its own root and walks it, so glob_exists:/**/nope is a whole-disk walk on a check meant to be instant; a multi-component file_exists: escapes through a committed symlink (outside/etc/passwd with outside -> /), and so does a single component that is a symlink, since exists() follows it; env_set: / env_eq: read the process environment and report the answer through which binaries got probed; and a cmd_exists: argument carrying a path separator is file_exists: under another name.

A step whose binary cannot be resolved statically is skipped rather than probed. [hooks.*] steps expand {path} / {repo} in run before spawning, so a hook reading {path}/scripts/setup would be looked up as that literal string and always come back missing; and a step that sets its own PATH in env resolves against that one, not against the $PATH the doctor happens to have.

A run is a shell script handed whole to sh -c, not an argv, so its first token is a shell word as often as it is a program: cd sub && ./setup.sh, set -e; …, if [ -f composer.json ]; then …. Shell keywords and builtins are not probed. Names that do ship a real binary everywhere (echo, test, printf, true) still are, since probing them resolves and costs nothing. source is probed too, deliberately: it is a bashism, and where /bin/sh is dash the step dies with source: not found, so the warning is the useful one and its fix is the portable . form. exec composer install is probed as composer, like env and command, since the wrapper stands in front of the real binary.

Reporting:

  • ✓: all probed binaries resolved
  • !: [review] binary missing (review is opt-in; a CI pre-commit hook keeps passing)
  • ✗: [git_tui] binary missing or a bootstrap command’s binary missing

The v0.6 update to this check (probing [git_tui] and [review]) landed with #75. Pre-v0.6, only lazygit and direnv were checked.

Looks at .git/worktrees/ and flags entries whose working directory was removed manually (e.g. someone rm -rf’d the worktree directory outside gwm).

  • ✓: every tracked entry points at a real directory
  • !: at least one stale entry, with gwm prune as the remediation

Walks local branches matching <type>/#<N>-<slug> and flags those with no associated worktree.

  • ✓: every gwm-style branch is either active (has a worktree) or merged into a trunk
  • !: at least one unmerged orphan, with git branch -d <name> as the remediation

The “merged into a trunk” exemption respects CONTRIBUTING.md’s “never delete the source branch after merge” rule: merged branches are preserved, not flagged. The trunk list is configurable via [doctor].trunks (default ["dev", "main"]); empty list disables the exemption.

User-managed branches (main, release-*, dependabot/..., anything not matching the gwm pattern) are silently ignored.

Checks that the configured [worktree].base exists and is writable, or (if it doesn’t exist yet) that its parent is writable. gwm creates the base lazily on the first gwm create, so a non-existent base is fine as long as it can be created.

  • ✓: base (or its parent) is writable
  • ✗: neither is writable (gwm cannot create worktrees here)

Re-runs the same [tui.keys] resolution path the TUI itself uses at startup, so any keymap mistake surfaces in gwm doctor before the TUI fails to dispatch. Added by #87 / #165 alongside the configurable keymap.

  • ✓: the keymap resolves and quit has at least one user-visible binding; the detail reports how many actions are bound (N action(s) bound)
  • !: the keymap resolves, but quit has been unbound entirely. Ctrl+C still exits the TUI as a hard-coded fallback in run_app, but no discoverable quit key remains, so the hint suggests adding e.g. quit = ["q", "Esc"] to [tui.keys]
  • ✗: the keymap fails to resolve (parse error, unknown action slug, chord conflict, or prefix collision); the detail repeats the underlying [tui.keys] error verbatim, and the hint points at gwm tui keys for the full action-slug list

Only actions with at least one chord count toward the N action(s) bound figure: an action set to [] in [tui.keys] is unbound and excluded. See .gwm.toml schema → [tui.keys], TUI → Keymap & command palette, and TUI → Keybindings for the action slugs and chord grammar.

9. worktree.branch_pattern round-trips through the parser

Section titled “9. worktree.branch_pattern round-trips through the parser”

branch_pattern drives how gwm writes a branch name, but the parser that reads one back is a hardcoded regex for the default {type}/#{issue}-{desc}. When the two disagree, every feature keyed on the parsed segments reads the wrong thing, silently. Added by #415.

The check is a real round-trip probe, not a comparison against the default string: a custom pattern is not automatically a broken one. {type}/#{issue}-prefix-{desc} still yields feat/#42-…, so type and issue survive and only desc comes back wrong.

  • ✓: parse_branch reads back the segments branch_name writes, for every branch the repo can produce

  • !: the pattern does not round-trip. Three shapes:

    • nothing parses → issue auto-linking from the branch name, gitmoji / gwm commit-prefix, gwm pr template selection and body placeholders, hook placeholders on the remove / bootstrap paths, the TUI rename and the branch-convention check (#6 above) are all inactive. PR/MR detection is not affected: Forge::find_pr_for_branch queries the forge with the whole branch name and never parses it
    • only some values parse → the detail names a failing example and scopes the loss to the branches that come out like it. {desc}/#{issue}-{type} is unreadable for a desc carrying a - and perfectly readable for one without; calling the whole pattern dead would be false for half the branches it produces
    • everything parses but a segment comes back different → the detail names which one and every feature reading it: type → gitmoji selection, issue → issue auto-linking, and both → remove/bootstrap hook placeholders and the TUI rename, which consume all three segments

    The hint stays neutral: restore the default, or keep the pattern and accept exactly the loss the detail names. Which workaround applies depends on which segment broke, so recommending one unconditionally would be wrong.

Round-trip is value-dependent, so probing a couple of arbitrary values proves nothing either way. The probe enumerates the value space gwm create actually admits, by construction rather than by sampling:

Segment Probed with Why that is the whole space
type every configured branch type finite, so this is exhaustive, and a type your [[branch_types]] forbids must not raise a warning about branches that cannot exist
issue one single-digit, one multi-digit \d+ is the only distinction BRANCH_RE can split on
desc one with a -, one without, one all-digits the dash is the only character that collides with a literal separator; digits-only is the only desc BRANCH_RE’s \d+ issue group can swallow ({type}/#{desc}-{issue} parses for 123 and for nothing else)
{repo} the real repo name the verdict depends on it: {repo}/#{issue}-{desc} is fine in a repo called gwm and unparseable in one called gwm-cli, whose dash the [a-z]+ type charset rejects

A pattern that survives all of it is the strongest claim this check can make short of deriving the parser from the pattern. And the verdict never quantifies beyond what the probes saw: when only part of the probed shapes lose something, the detail counts them (on 27 of the 30 branch shapes probed, …) rather than claiming every branch is affected.

This check states the limitation rather than fixing it. Deriving the parser from the pattern is tracked by #417. gwm config validate prints the same message on stderr and still exits 0 (a custom pattern is valid configuration, not an error) and reads the effective pattern, so one set only in the global ~/.config/gwm/config.toml is caught too.

.github/workflows/ci.yml
- name: gwm health
env:
GWM_ALLOW_BOOTSTRAP: '1' # if the job also runs `gwm create` / `gwm bootstrap`
run: gwm doctor # exits 1 on Warning, 2 on Failure

gwm doctor itself doesn’t go through the TOFU trust gate (the doctor never invokes bootstrap::run; it only reads config), so GWM_ALLOW_BOOTSTRAP=1 is harmless here. Set it for jobs that also create worktrees in the same workflow run.

What doctor does NOT audit (yet): the trust ledger contents themselves. A job that wants to assert “this CI host has trusted exactly the configs the workflow expects” would have to parse ~/.config/gwm/trust.toml (or $GWM_TRUST_LEDGER) manually. Auditing the ledger from gwm doctor is on the post-#95 follow-up list.

Or as a pre-commit hook:

Terminal window
# .git/hooks/pre-commit (or via pre-commit framework)
gwm doctor || { echo "gwm doctor reported issues, see above"; exit 1; }

Because [review] missing only triggers a Warning (exit 1), a pre-commit hook can choose to allow Warnings (gwm doctor; [ $? -le 1 ]) and only block on Failures.