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.

Exit codes
Section titled “Exit codes”| 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.
Sample output
Section titled “Sample output”$ 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) boundA 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-experimentEach Warning / Failure carries a one-line remediation hint, copy-pasteable.
Checks performed
Section titled “Checks performed”1. .gwm.toml parses
Section titled “1. .gwm.toml parses”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].modevalue
The “absent” case is intentionally a ✓: gwm works without .gwm.toml and the user-facing message says so.
2. guard references resolve
Section titled “2. guard references resolve”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.
3. when predicates supported
Section titled “3. when predicates supported”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 totrue, 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 is2
Recognised keywords: file_exists:, cmd_exists:, env_set:, env_eq:, glob_exists:. Boolean composition (!, &&, ||) is also validated. See when: predicates.
4. external binaries on PATH
Section titled “4. external binaries on PATH”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.envrcexists in the worktree (theseed-from-exampleaction may trydirenv 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, onlylazygitanddirenvwere checked.
5. no prunable worktrees
Section titled “5. no prunable worktrees”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, withgwm pruneas the remediation
6. no orphan gwm branches
Section titled “6. no orphan gwm branches”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, withgit 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.
7. base directory writable
Section titled “7. base directory writable”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)
8. [tui.keys] keymap resolves
Section titled “8. [tui.keys] keymap resolves”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 andquithas at least one user-visible binding; the detail reports how many actions are bound (N action(s) bound)!: the keymap resolves, butquithas been unbound entirely.Ctrl+Cstill exits the TUI as a hard-coded fallback inrun_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 atgwm tui keysfor 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_branchreads back the segmentsbranch_namewrites, 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 prtemplate 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_branchqueries 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.
- nothing parses → issue auto-linking from the branch name, gitmoji /
How the probe space is chosen
Section titled “How the probe space is chosen”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.
CI integration
Section titled “CI integration”- 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 Failuregwm 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 fromgwm doctoris on the post-#95 follow-up list.
Or as a pre-commit hook:
# .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.
Related
Section titled “Related”- Bootstrap pipeline: same
✓ / ! / ✗convention used by stage reports - TUI → Configurable launchers: why the review launcher’s missing-binary is a Warning, not a Failure
.gwm.tomlschema →[doctor]: thetrunksknob