Troubleshooting
Start with the local doctor from the repository checkout:
gitmoot doctor --repo .
gitmoot status --repo owner/repo
gitmoot daemon status
Most Gitmoot failures come from one of four places: the installed binary, GitHub CLI auth, runtime/plugin discovery, or a local daemon/job/lock state.
Install Script Failed
Symptom: curl -fsSL https://gitmoot.io/install.sh | sh exits before
installing gitmoot.
Likely cause: network failure, unsupported platform, missing shell tools, or a release artifact that is not available for the current OS/architecture.
Check:
uname -s
uname -m
curl -fsSL https://gitmoot.io/install.sh -o /tmp/gitmoot-install.sh
sh -n /tmp/gitmoot-install.sh
Fix: retry the installer or use the direct binary fallback from the install page. Verify the artifact checksum before running it.
Binary Not On PATH
Symptom: gitmoot: command not found after install.
Likely cause: the install directory is not on PATH, or the shell has not been
restarted after pipx ensurepath or installer profile changes.
Check:
command -v gitmoot
echo "$PATH"
ls -l ~/.local/bin/gitmoot
Fix: add the install directory to PATH, restart the shell, or move the binary
to a directory already on PATH.
Checksum Mismatch
Symptom: the local SHA256 does not match the release checksum.
Likely cause: partial download, wrong artifact, stale checksum, or a tampered download.
Check:
sha256sum <artifact>
shasum -a 256 <artifact>
Fix: delete the file, download the artifact again from GitHub Releases, and compare against the checksum for that exact release and platform.
GitHub CLI Auth Fails
Symptom: PR comments, issue comments, review publication, status checks, or merge actions fail.
Likely cause: gh is not installed, is authenticated as the wrong account, or
does not have access to the repo.
Check:
gh auth status
gh repo view owner/repo --json nameWithOwner
gh pr list --repo owner/repo --state open
Fix: authenticate gh for the account that can read and write the repository,
then retry the Gitmoot operation.
Send A Gitmoot Bug Report
Symptom: a Gitmoot job failed, blocked, or was cancelled, and you want to send a useful report without exposing raw runtime output.
Likely cause: the failing job has local context that matters for debugging: repo, agent, runtime, action, task, selected error, result summary, and recent events.
Check:
gitmoot job show <job-id>
gitmoot report bug --job <job-id> --preview
Fix: preview first. The report is redacted, omits raw runtime output by default,
adds the gitmoot-dashboard-report and bug labels, and includes a
fingerprint marker so open duplicates can be reused.
Create the GitHub issue only when you intend to file it:
gitmoot report bug --job <job-id> --create --yes
The command prints either created issue: ... or existing issue: ...; use that
URL when sharing status. In the interactive dashboard, select a failed, blocked,
or cancelled job and press B report bug to open the same preview, then g to
create or reuse the issue. If creation fails, the preview stays open and shows
the error inline.
Plugin Doctor Fails
Symptom: Codex or Claude Code does not discover Gitmoot, or
gitmoot plugin doctor reports missing package or runtime state.
Likely cause: plugin package was not generated, runtime CLI is missing, runtime uses a different home directory, or the package cache is stale.
Check:
gitmoot plugin doctor
gitmoot plugin doctor codex
gitmoot plugin doctor claude
gitmoot plugin path codex
gitmoot plugin path claude
Fix:
gitmoot plugin install codex --force
gitmoot plugin install claude --force
gitmoot plugin doctor
The plugin is discovery and guidance. It does not replace gitmoot, GitHub
CLI auth, or runtime/model credentials.
Runtime Session Not Found
Symptom: gitmoot agent doctor <name> cannot validate a Codex, Claude, or Kimi
session, or a job resumes the wrong session.
Likely cause: a last reference changed, the runtime home changed, or the
session id is stale. For a Kimi agent, the runtime reference must be a Kimi
session id (session_<uuid>) or empty.
Check:
gitmoot agent list
gitmoot agent show <agent>
gitmoot agent doctor <agent>
codex exec resume --help
claude --help
kimi --help
Fix: prefer explicit session UUIDs or thread names over last, then
re-subscribe the agent with the correct session reference — or, for a
registered agent whose session is genuinely dead or stranded, rebind it in
place without re-registering:
gitmoot agent restart <agent>
agent restart abandons the old runtime session and starts a fresh one for the
same agent; it refuses while the session is live or the agent has in-flight
jobs (finish or cancel those first).
Note that some session failures self-heal without your intervention: a dead
Claude --resume target is retried on a fresh session and re-pinned (#443),
and a transient 401 ("socket connection closed unexpectedly") under sustained
concurrency is retried with backoff (#487/#509). A job whose events show one of
these errors followed by a success worked as designed.
Daemon Not Running
Symptom: queued jobs do not move, PR comments are not consumed, or dashboard shows the daemon as down.
Likely cause: daemon was never started, exited, or is running with the wrong repo/home.
Check:
gitmoot daemon status
gitmoot daemon logs
gitmoot status --repo owner/repo
If daemon status warns that the advertised log is missing or was last written
before the running daemon started, the path is not receiving current output.
gitmoot doctor reports the same confirmed condition as a non-fatal
daemon log check. If the daemon runs under systemd --user, follow the likely
live stream instead:
journalctl --user -u gitmoot-daemon -f
The warning does not assert how the daemon was launched. It is omitted when the daemon is stopped, the log is current, or the comparison cannot be made.
Fix:
gitmoot daemon start --poll 30s --workers 1
Use gitmoot daemon run only when you intentionally want a foreground process.
--repo owner/repo scopes the daemon to a single repo: it polls only that
repo's PRs and claims only that repo's queued jobs. Omit --repo to supervise
every enabled registered repo from one daemon (#581). If queued jobs for one
repo never move while another repo's jobs do, check whether the daemon was
started with --repo scoping it to a different repo, or with a --session <root-job-id> (alias --root) filter: a daemon started with --session runs
only jobs whose root_job_id matches that orchestration run plus the root
coordinator job itself. Restart it without --repo/--session to drain
unrelated jobs.
Also check the repo is enabled in gitmoot repo list.
Claude auth is independent of daemon restarts. Rotate the owner-only
runtime-auth.env with gitmoot auth set claude; the next delivery observes
it. Inspect masked sources with gitmoot auth status and validate them with
gitmoot auth probe claude or gitmoot doctor.
Daemon Already Running
Symptom: gitmoot daemon start/run refuses with daemon already running with pid ….
Likely cause: a daemon is already supervising this Gitmoot home. One daemon per home is enforced with a pidfile plus a flock backstop (#550/#556); starting a second one is refused by design (a stale pidfile whose owner is dead is liveness-checked and recovered automatically, so restarts after a crash work).
Fix: use the running daemon — it supervises all subscribed repos. To change its
settings, send kill -HUP <pid> for a live [daemon] config reload (#577), or
use gitmoot daemon restart. Scripts that start daemons should treat this
refusal as success, not an error.
Job Stuck Or Failed
Symptom: a job is queued, blocked, failed, or no longer changing state.
Likely cause: runtime delivery failed, worker is read-only, GitHub auth failed, or another lock is active.
Check — read the stuck reason first:
gitmoot job list --repo owner/repo # WHY: column on queued/blocked jobs
gitmoot job show <job-id> # why_stuck: / next_retry_at: lines
gitmoot job events <job-id>
gitmoot agent show <agent>
gitmoot lock list --repo owner/repo
gitmoot job list appends a WHY: column and gitmoot job show prints a
why_stuck: line for queued/blocked jobs (#552) — e.g. a runtime-session lock
wait (naming the holder), blocked: awaiting human, auth failing: …,
throttled: …, retrying: …, or a blocked-operational: <class> deferral with
the attempt schedule. A deferral that needs a human (dirty/wrong-head checkout)
also prints a suggested_action naming the fix.
Deferred jobs recover on their own (#532): a delivery failure classified as a
retryable operational blocker — runtime_auth, runtime_quota,
network_outage, or checkout_contention — is re-queued with a bounded retry
budget instead of failing terminally. job show --json carries the
blocker_class, attempt count, and suggested_action. A runtime_auth deferral
only re-dispatches once a live doctor-style credential probe passes (a failing
probe extends the hold without spending a retry), and over [events] the
deferral is a first-class job.deferred emitted instead of job.failed. A job
that "failed then reappeared as queued" is the deferral working; only act when
the retry budget is spent and the job stays failed.
A job stuck in running is recovered automatically once it shows no lease
progress past the staleness window (default 30m; tune with the
GITMOOT_STALE_RUNNING_AFTER environment variable; the smallest honored value
is 1m — below-1m, malformed, or non-positive values are rejected in favor of the
30m default rather than clamped, #560). The window is a same-boot crash backstop,
not a timeout: a job holding a runtime session lock whose lease has not elapsed is
left running regardless of the window. After a reboot there is no wait at
all — the kernel boot id changes, so on its next startup and every tick the daemon
immediately requeues every job claimed on the previous boot and reclaims its
stranded runtime session lock, regardless of any unexpired lease (#651).
Boot-aware recovery is Linux only; elsewhere recovery falls back to the lease/age
window above.
Fix: resolve the underlying runtime/auth/lock issue, then retry when safe:
gitmoot job retry <job-id>
Cancel (abandon) only when the job should not continue. Cancel now dismisses a
blocked job — one paused awaiting a human — as well as a queued/running one
(#631); a dismissed job is not lost, since gitmoot job retry accepts a cancelled
job and resurrects it:
gitmoot job cancel <job-id>
A backlog of blocked jobs never clears on its own. Clear a stale batch with the
bulk form, which is a dry-run by default (it prints id/agent/repo/age and
cancels nothing) until you pass --yes:
gitmoot job cancel --state blocked --older-than 7d # preview the selection
gitmoot job cancel --state blocked --older-than 7d --yes # cancel it
Only blocked is accepted for --state; narrow the selection with --older-than
(a Go duration like 168h, or a <N>d days suffix), --repo owner/repo, and
--agent name. gitmoot doctor warns when blocked jobs older than 30d have piled
up and prints the exact command to dismiss them.
To sweep the backlog automatically, set [orchestrate].blocked_ttl to a positive
Go duration (e.g. blocked_ttl = "168h"): the daemon then dismisses any blocked
job idle longer than the TTL through the same cancel path, recording a
blocked_ttl_expired job event. It is off by default (empty or 0s disables
it; a negative value is rejected), because a blocked job is a human-awaiting
decision that is never auto-discarded unless you opt in. This is the single-job
counterpart of [orchestrate].escalation_ttl, which auto-finalizes a whole paused
delegation tree and is on by default (24h).
Stale Lock
Symptom: implementation or merge work is blocked by a lock whose owner is gone.
Likely cause: a worker died or the daemon stopped before cleanup.
Check:
gitmoot lock list --repo owner/repo
gitmoot lock show owner/repo <branch>
Fix: let a running daemon reclaim stale resource locks automatically. Release a branch lock only after confirming the owner is no longer working:
gitmoot lock release owner/repo <branch> --owner <agent>
Merge Gate Deferred By An Active Branch Job
Symptom: a ready PR remains unmerged and the gate reason says an active job is in flight on its branch.
Likely cause: an ask, review, or implement job targeting that PR branch is
still queued or running. Gitmoot treats this as a transient deferral so it cannot
squash-merge, delete the source branch, and strand an in-progress fix. The task
stays ready_to_merge rather than entering a blocked state or emitting a
merge-gate error.
Check:
gitmoot job list --repo owner/repo
gitmoot job show <job-id>
gitmoot job events <job-id>
Fix: let the job settle or cancel it deliberately. The daemon re-evaluates the unchanged policy merge path on its next tick; do not release its branch lock while the job is active.
Dashboard Blank Or Noninteractive
Symptom: gitmoot dashboard does not open the TUI, prints plain output, or
looks blank under a script/agent.
Likely cause: stdin/stdout is not a TTY, TERM=dumb, or TUI was disabled.
Check:
gitmoot dashboard --plain
gitmoot dashboard --json
gitmoot dashboard --watch
echo "$TERM"
Fix: run from a real terminal for the interactive TUI, or use --plain /
--json in agents, CI, pipes, and redirected output.
SkillOpt Optimizer Missing
Symptom: gitmoot skillopt train continue reaches optimizer handoff and
reports blocked_config or missing gitmoot-skillopt.
Likely cause: the separate Python optimizer is not installed or is not on
PATH.
Check:
gitmoot-skillopt --version
gitmoot-skillopt optimize --help
gitmoot skillopt train status --session <session-id> --verbose
Fix:
python3 -m pip install --user pipx
python3 -m pipx ensurepath
pipx install https://github.com/jerryfane/gitmoot-skillopt/releases/download/v0.4.2/gitmoot_skillopt-0.4.2-py3-none-any.whl
gitmoot-skillopt --version
gitmoot-skillopt optimize --help
For a virtualenv install, pass --skillopt-bin /path/to/gitmoot-skillopt.
SkillOpt Dependency Or Credential Failure
Symptom: optimizer preflight starts but fails before producing a candidate.
Likely cause: missing Python dependency, backend/model credentials, evaluator configuration, or writable output directory.
Check:
gitmoot skillopt train status --session <session-id> --verbose
gitmoot-skillopt optimize --help
Fix: install the missing dependency, configure the required backend credential through user-owned environment/config, and restart any daemon or runtime that must inherit the environment. Do not commit secrets.
Train Session Recoverable
Symptom: verbose status reports status_phase: recovery_available.
Likely cause: optimizer wrote completed artifacts but the wrapper failed before Gitmoot imported the result.
Check:
gitmoot skillopt train status --session <session-id> --verbose
Fix:
gitmoot skillopt train recover --session <session-id> --out-root <optimizer-output-root>
Recovery validates artifacts and imports either a completed candidate or a completed no-candidate result through the normal gate.
Live Docs Or LLM Context Stale
Symptom: gitmoot.io/docs or /llms.txt does not show current source docs.
Likely cause: docs were changed but not rebuilt/deployed, or stale deployed files were not deleted.
Check:
cd website
npm run build
curl -fsS https://gitmoot.io/docs/reference/cli | rg 'gitmoot dashboard'
curl -fsS https://gitmoot.io/llms.txt | rg 'SkillOpt|Dashboard|Release Notes'
Fix: deploy the current static build with delete semantics:
cd /root/gitmoot/website
npm run build
rsync -a --delete build/ /var/www/gitmoot-docs/
Delegation worktrees consume too much disk
gitmoot doctor reports N stale worktrees / X GB under <home>/worktrees and
separates reclaimable final owners, pinned non-final owners, and unproven
directories. /api/health exposes the same metric in its top-level worktrees
field.
[workflow].delegation_worktree_ttl = "72h" is default-on: after that grace
period the daemon force-removes dirty terminal-owned delegation worktrees,
prunes Git worktree metadata, and records
delegation_worktree_reclaimed_ttl. Set it to "0" to disable this pass.
Blocked, queued, and running owners remain pinned and are never force-removed.
For immediate relief, list candidate directories and prove ownership before removing anything:
find "$HOME/.gitmoot/worktrees" -type d -path '*/delegations/*/*' -prune -print
sqlite3 "$HOME/.gitmoot/gitmoot.db" -header -column '
SELECT id, state, updated_at, json_extract(payload, "$.worktree_path") AS path
FROM jobs
WHERE state IN ("succeeded", "failed", "cancelled")
AND datetime(updated_at) <= datetime("now", "-72 hours")
AND json_extract(payload, "$.worktree_path") <> ""
AND (json_extract(payload, "$.delegation_id") <> ""
OR json_extract(payload, "$.read_only_worktree") = 1);
'
gitmoot job show <job-id> --json
git -C <registered-checkout> worktree remove --force <verified-worktree-path>
git -C <registered-checkout> worktree prune
The sqlite3 command is an optional operator aid, not a Gitmoot dependency.
Verify that job show still reports succeeded, failed, or cancelled and
the same payload.worktree_path. Never remove a worktree for a blocked, queued,
or running job; settle it first.
Isolated worktrees duplicate gigabytes of tool cache
An isolated-worktree job re-materializing its own uv/go/npm/pip cache
inside its worktree (rather than reusing a shared one) is the largest driver of
gitmoot doctor's worktree disk figure once a fleet has run for a while — one
worktree can carry several gigabytes of immutable, content-addressed packages
that duplicate what every other job already downloaded.
Gitmoot points UV_CACHE_DIR, PIP_CACHE_DIR, npm_config_cache, GOCACHE,
and GOMODCACHE at one shared, host-level directory (default
<home>/cache/tools) for isolated-worktree jobs, so package caches are reused
across jobs instead of duplicated per worktree. This is on by default; set
[cache] enabled = false in config.toml to opt out, or [cache] dir = "/absolute/path" to relocate the shared directory (must be absolute).
A read-only/auto codex job and a codex chat seat do not get the redirect —
codex never grants writable-path access to either, so pointing their tools at
the shared directory would break rather than help. Those jobs keep their
pre-existing (in-worktree) cache behavior.
If an existing worktree already carries a large in-worktree cache from before this took effect, it is cleaned up the same way as any other worktree content: by the delegation-worktree reclaim pass above, or manually once the owning job is terminal.