Skip to main content

git-roost

ci pypi npm winget discussions license

top for git — every repo and worktree on the box, in one table, most actionable first.

roost answers what are my Claude sessions doing. git-roost answers the other half: what is actually in the trees they are working in. Sessions report intent; git reports what happened, and only one of those two can be wrong.

One file, no dependencies, Python 3.9+, macOS/Linux/Windows.

git-roost watch mode: cost-of-ignoring groups, a > cursor, a detail view, the keymap, and the commit feed

The short ambient loop below is the same program, watching quietly. A row that changes group, WORK or DRIFT is marked *:

git-roost's watch mode, idling until a tree goes dirty, is marked *, and climbs out of QUIET

Recorded against a synthetic fixture, not a real machine — see demo/ for how. Text fallback, same shape:

  REPO                    TREE                      BRANCH                       WORK   DRIFT  STASH  LAST  SUBJECT
MID-OPERATION
  copilot-money-mcp       fork-merge-and-repack     fork/merge-and-repack        17     ^1            1h    ** merge in progress, 3 conflict(s) **
DIVERGED
  counting-chicken-wings  docs-roadmap-refresh      docs/roadmap-refresh         clean  ^1v1   1      1h    Re-verify the roadmap against the tree
  llm-security-rules      pipeline-e2e              pipeline-e2e                 clean  ^4v8          3h    Add launch post draft
UNCOMMITTED
  llm-security-rules      fix-action-injection      fix/action-injection         2+1?   =             1h    fix: script injection via unquoted input
UNPUSHED
  copilot-money-mcp       fork-merge-and-repack     fork/merge-and-repack        clean  ^1            1h    test: registry-derived tool-count check
BEHIND
  roost                   (primary)                 main                         clean  v2            1h    Bump version to 0.3 (#3)
ACTIVE
  shunt-ai-power          (primary)                 main                         clean  =             3m    docs+fix: wall-power wording

QUIET (8)  blog/chicken-fest . copilot-money-mcp/(primary) . repo-security-ci/(primary) . ...

27 tree(s) across 11 repo(s)  |  2 with uncommitted work  |  1 mid-operation

Install

Questions, ideas, or your own fleet's screenshot go in Discussions — bugs go in Issues.

The distribution, the command, the module and the repo are all the bare git-roost — pick whichever channel is already on the box.

pipx install git-roost          # or: pip install --user git-roost
npm install -g git-roost        # if Node is what you have
brew install gmhoward9289-ops/tap/git-roost
winget install gmhoward9289-ops.git-roost   # Windows; still needs Python on PATH

Or just take the file. It is one script with no dependencies, so curl and chmod +x is a complete install:

curl -fsSLO https://raw.githubusercontent.com/gmhoward9289-ops/git-roost/main/git_roost.py
chmod +x git_roost.py && ./git_roost.py

Installed under any of those names, git roost works too: git dispatches an unknown subcommand to a git-<name> on PATH.

Then run it:

git-roost

That is the whole first run. A bare git-roost scans the usual checkout folders under your home directory that actually exist — ~/dev, ~/src, ~/GitHub, ~/Documents/GitHub (GitHub Desktop), ~/code, ~/repos, ~/work, ~/git, ~/projects, and Visual Studio's ~/source/repos. If none of those folders exist, it scans the current directory, three levels down. It will not walk $HOME itself (too wide, too much junk).

If your trees live somewhere else:

git-roost --root ~/wherever
git-roost --root                   # this directory (omit the path)
git-roost --root ~/dev --root ~/src

Keep that as the daily default with GIT_ROOST_ROOT (same separator as PATH: : on Unix, ; on Windows):

export GIT_ROOST_ROOT=~/wherever           # bash
$env:GIT_ROOST_ROOT = "$HOME\wherever"     # PowerShell

--root still wins for one call. Deeper trees need --depth.

The man page installs to <prefix>/share/man/man1. A system or Homebrew install puts that on the default MANPATH; a venv or pipx install does not, so man git-roost there needs MANPATH help.

Why

lazygit, gitui and tig are all excellent and all single-repo. They answer "what is happening in this repo". They do not answer "which of my thirty trees is diverged, which has uncommitted work nobody has looked at, and which one is stuck half way through a rebase" — which is the question you have the moment more than one thing is working in parallel.

Usage

git-roost                # one table; usual checkout folders, then cwd
git-roost -1             # render once and exit (the default; --once)
git-roost -w             # redraw every 3s (the top view)
git-roost --log          # commit feed across every repo, newest first
git-roost --all          # expand the QUIET group
git-roost --json         # records, for piping somewhere else
git-roost --root ~/src   # extra roots (repeatable); overrides the usual folders
git-roost --repo wings --sort work --filter dirty   # scope, sort and filter together
git-roost --check                      # no table -- exit 1 if any tree needs a human first
git-roost --fail-on stuck              # like --check, but keeps the normal table
git-roost --github                     # add a PR/CI column, via `gh` (opt-in: network calls)

Watch mode takes keys:

Key Action
? the keymap
r refresh now
s sort: recent / repo / work
f filter: all / uncommitted / mid-operation
a expand or collapse QUIET
l toggle the fleet table and the commit feed, without restarting
j / k move the row cursor; the viewport follows
enter open a detail view for the highlighted tree
q quit

-w is a screen, not a dump. It uses the terminal height, keeps the status line and ? on screen, and j/k scroll through the rest. A one-shot git-roost still prints every tree (pipe that to less if you want the list). Watch mode also uses the alternate screen, so it does not leave 85 rows in the scrollback.

Sort cycles within a group and never across one. The group order is the whole argument this tool makes — cost of ignoring, not recency or size — so a sort that let an ACTIVE tree float above a MID-OPERATION one would be quietly answering a different question. Changing sort, filter or the quiet toggle resets the row cursor rather than leaving it pointing at whatever row happens to land underneath it.

l used to be a restart: --log decided table-or-feed once, at launch. Now it just seeds the initial view — git-roost --log -w opens on the feed, and git-roost -w opens on the table — and l flips between the two live, without losing the scan already in flight.

j/k move a highlighted row (> in the left margin) through whatever the table is currently showing — same sort, same filter, same QUIET collapse everyone else sees. The viewport follows the cursor so a fleet of eighty trees stays inside the terminal. enter opens a detail screen for that tree: its whole stash list rather than just a count, a diffstat of the most recent stash, what it's stuck doing if anything, and its last five commits. Any other key returns to the table, the same way dismissing the ? overlay does — one dismissal convention, not two.

Two consecutive frames of an idle fleet used to be indistinguishable from each other, which is a strange thing for a tool named after top. Now a row whose group, WORK or DRIFT changed since the last redraw is marked with a leading *, so watching quietly still tells you when something moved.

--repo NAME (repeatable, case-insensitive substring), --sort and --filter put the f/s keys' view on the command line, so --json and one-shot renders can be scoped without a terminal — git-roost --repo wings --filter dirty --json is one repo's uncommitted trees, nothing else. Both also seed -w's starting view, so -w --sort work --filter dirty opens watch mode already positioned there and the keys still cycle from it.

Keys need a terminal. Piped, redirected, or on a box with neither termios nor msvcrt, watch mode degrades to the plain timer redraw rather than failing, and the default one-shot render touches no terminal settings at all — which is what keeps git-roost | less and git-roost --json | jq safe.

Two exit-code flags, for two different hook shapes:

  • --check is a different shape entirely from the table: no --filter, just pass/fail. 0 means every tree is at worst UNPUSHED or BEHIND; 1 means at least one is MID-OPERATION, DIVERGED, or has UNCOMMITTED work — and those offending trees print (or --json them), so the caller knows which, without reading a full table. --root/--repo still scope the scan.

    git-roost --repo counting-chicken-wings --check || echo "not clean, look first"
    
  • --fail-on {stuck,diverged,dirty} is --check with the threshold made a choice, and without replacing the table: it prints the normal render (or the normal --json) and only changes the exit code, checked against the whole fleet (never the --filter view — a hook asking "is anything stuck" should not get a false 0 just because a human also filtered the table they're looking at). stuck is mid-operation only; diverged adds ahead-and-behind; dirty (equivalent to --check's fixed threshold) adds uncommitted work on top of that.

    git-roost -w --fail-on stuck   # a human's normal view, that also exits 1
    

--github

Opt-in, not on by default. The local scan is disk-only and fast; PR and CI state is a network call to GitHub through the gh CLI, with a meaningfully different latency and trust profile, so it never runs unless asked.

git-roost --github                     # one-shot table with a PR column
git-roost -w --github                  # watch mode; PR/CI refreshed every 30s
git-roost -w --github --github-interval 10   # refresh PR/CI more often
git-roost --json --github              # pr_number/pr_state/pr_draft/pr_review/pr_ci per tree

Each tree's current branch is looked up with a single gh pr view call, run from inside that tree so gh resolves "the PR for this branch" itself. It degrades the same way git() does: no gh on PATH, no auth, no GitHub remote, no open PR, a rate limit, or a slow network are all just "we don't know" for that one tree — a blank PR column and, in --json, null values rather than a missing key or a crashed scan. The calls go through gh_call(), a wrapper as careful as git()'s: an allowlist keyed on gh's subcommand (pr view / pr list / pr status only — nothing that could merge, close, edit, or comment), checked before the subprocess ever runs.

In watch mode, PR/CI data is cached and refreshed on its own interval (--github-interval, default 30s) rather than on every redraw (default 3s): the local git scan is cheap enough to run every frame, but gh is a rate-limited network call and a PR's review state rarely changes inside a 3s window.

--json only gains the five pr_* keys when --github was passed — the default JSON shape is unchanged for any consumer that never asks for GitHub data.

Reading the table

Groups are ordered by what it costs to ignore them, not by how interesting they look.

Group Meaning
MID-OPERATION Stuck part-way through a rebase, merge, cherry-pick, revert or bisect. This is the one that most needs a human, and it looks identical to an idle tree in every other column.
DIVERGED Ahead and behind. Someone is going to resolve a conflict later.
UNCOMMITTED Tracked changes sitting unsaved.
UNPUSHED Commits that exist only on this machine.
BEHIND Someone else moved on without you.
ACTIVE Committed within the hour, otherwise clean.
QUIET Collapsed to one line. --all expands it.

Columns:

  • WORKclean, 3 tracked changes, +2? untracked, 3+2? both. Untracked files get a marker but never a group of their own: they are mostly scratch output, and one tree here carries a dozen permanently.
  • DRIFT= in sync, ^2 ahead, v3 behind, ^2v3 diverged, - unknown. - means unknown, never in sync.
  • STASH — stash entries, blank when there are none.
  • LAST — age of the last commit.
  • PR — only with --github. #123 open, #123+ CI green, #123x CI red, #123~ CI still running, #123 draft, blank when there is no open PR.

Finding the baseline

Drift needs something to measure against. The chain is:

  1. the branch's own upstream, when it has one;
  2. origin/HEAD;
  3. if the repo has exactly one remote, that remote's HEAD, then its main or master;
  4. otherwise -.

Steps 1 and 2 are the ones that matter most and are also the easiest to get wrong in opposite directions. Branches that are never pushed have no upstream, so an upstream-only check calls every one of them clean. And origin is a convention, not a guarantee — one repo here has a single remote named deploy, and an origin-only lookup filed a tree that was nine commits behind under QUIET.

Step 3 stops at a single remote on purpose. With two remotes there is no way to know which is authoritative, and a confident wrong baseline is worse than -.

Configuration

Variable Default What it does
GIT_ROOST_ROOT usual checkout folders under $HOME that exist, else cwd where to look for repos, os.pathsep-separated for more than one (same convention as PATH) — --root overrides it for one call
GIT_ROOST_TIMEOUT 5 seconds any single git call may take before it is abandoned
GIT_ROOST_WORKERS 12 how many trees are scanned in parallel
GIT_ROOST_GH_TIMEOUT 8 seconds any single gh call may take before it is abandoned (only with --github)
GIT_ROOST_GH_WORKERS 4 how many gh calls run in parallel (only with --github; its own, smaller pool so a slow gh never starves the git scan)

No dotfile or config file, deliberately — every setting here is an env var or a flag, matching roost, leghorn and legbar. The timeout and worker count are both about how hard to push a slow disk rather than about git. The GH pair is separate on purpose: gh is a network call with its own, longer timeout and its own, smaller worker cap, and both are irrelevant unless --github is passed.

The timeout is a ceiling on one call, not on the scan. A tree behind a stalled network mount is dropped rather than allowed to hold the whole table hostage — in a watch loop, one unreachable repo would otherwise stop every other repo from redrawing.

Read-only by construction

Every git invocation goes through one function that refuses anything outside an allowlist of read-only plumbing. It cannot mutate a tree, an index or a ref — not because it happens not to, but because the call raises.

The allowlist is keyed on (subcommand, first argument), not the subcommand alone, because the subcommand alone does not settle it: stash list reports but stash pop mutates and stash clear destroys; config --get reads but config <key> <value> writes; symbolic-ref --short REF reads but symbolic-ref HEAD REF rewrites HEAD. A subcommand-level allowlist admits all of those writes, and an earlier version of this file did.

The test suite asserts the policy directly, asserts each of those dangerous forms is refused, and asserts that reading a repo leaves its status and HEAD byte-identical.

This matters because the tool is meant to sit in a watch loop over every repo on the machine, unattended.

Performance

A full scan is parallel across trees, and repo-level facts (stashes, remote refs) are computed once per repo rather than once per worktree.

Measured on 27 trees across 11 repos: ~0.68s per redraw, comfortably inside the default 3s watch interval. A Windows box with ~85 trees is closer to ~8s for a one-shot: finding the trees is cheap, each one is several git.exe spawns. A bare run prints scanning N tree(s)... on stderr and ticks k/N plus a spinner until the table is ready. Watch mode fills a fixed slot per tree (discover order) so the screen does not flash or regroup mid-scan; later refreshes keep the last grouped table and only spin the status line. Tunable with GIT_ROOST_WORKERS and GIT_ROOST_TIMEOUT.

The family

Four tools, one shape: single file, no dependencies, stdlib unittest, and a table ordered by what it costs to ignore.

  • roosttop for Claude Code: per-session context burn, models, and the subagents a session spawned.
  • git-roost — this one. The other half of the same question: not what the sessions say they are doing, but what their trees actually contain.
  • leghorn — sessions joined to worktrees and real git state, CI, and a commit feed.
  • legbar — both lanes on one screen, over one discovery layer.

git-roost is the one that needs no Claude Code, no sessions and no ~/.claude at all. It reads git and nothing else, which is why it is the one worth running on a box that has never seen an agent.

Tests

python -m unittest discover -s tests

Stdlib unittest, so it runs with nothing installed; pytest collects it unchanged.

License

Apache-2.0

Release files for git-roost 0.4.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for git-roost 0.4.2
File Size Uploaded
git_roost-0.4.2.tar.gz 74.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for git-roost 0.4.2
File Interpreter ABI Platform
git_roost-0.4.2-py3-none-any.whl Python 3 none any Details

Total release size: 116.4 kB

Release files / git_roost-0.4.2.tar.gz

Download URL git_roost-0.4.2.tar.gz
Size 74.5 kB
Tags Source
SHA-256 checksum
How to use checksums
54640c2f2d5b6880b8c38fb1484e8d69d2def7eca57c9b6e58921f3d4e63e58d
BLAKE2b-256 checksum
How to use checksums
cba9a998ef34bacf0b387a0c4ae800be0d462f75e8cfb134ad52de0b573f289e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 21, 2026.

Transparency log

Release files / git_roost-0.4.2-py3-none-any.whl

Download URL git_roost-0.4.2-py3-none-any.whl
Size 42.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
08d21841fb318fe564cd45661af5bd766b2fc50ee88ca44e38d0ec85a2785245
BLAKE2b-256 checksum
How to use checksums
5b9269ded4e072c329bedf90ebe4dc6bcf1cc8f6dea2f43f2c8229fa32b5c04d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 21, 2026.

Transparency log

Release history Release notifications | RSS feed

0.9.1

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.1

2 release files

0.5.0

2 release files

This release

0.4.2 This release

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page