Skip to content

Repository files navigation

octoscope — a terminal dashboard for GitHub, shown against a dark bokeh background

OCTOSCOPE is a terminal dashboard for your GitHub account, or anyone else's public profile — profile, activity, repo health and network at a glance, auto-refreshed in the background.

Latest release Guide Go License Platform

Read the guide — install, the tabs, drill-ins, themes, configuration, scripting, and a full keyboard reference.

octoscope — a terminal dashboard for GitHub, shown against a dark bokeh background

Contents

What it does

octoscope is a single-binary TUI built with BubbleTea. It pulls a focused set of numbers from the GitHub GraphQL API in one round-trip and keeps them current on screen so you can check the pulse of your GitHub life without switching to a browser.

The dashboard is split into tabs (Overview, Repos, PRs, Issues, Activity, Gists, Inbox, What's new) — jump with number keys or cycle with tab / shift+tab.

The tabs

  • Overview — the five stat sections below, the traditional dashboard landing.

  • Repos — every owned, non-fork repository in one sortable, searchable list. Columns: CI status (a coloured dot — green / red / yellow / dim — sourced from the default-branch status-check rollup; drill in with enter to see which check failed and jump to its run, v0.25.0+), name, primary language, stars, forks, open issues, open PRs, last push, latest release (tag + age, v0.14.0+). Press s to cycle sort (CI is in the cycle and surfaces failures first; "release" sort lists the most recently published first), / to filter by substring, P on a row to pin a repo to a sticky section at the top — the viewport scrolls so even a 100-repo account stays navigable. Set commit_counts = true to add a commits column — how many commits you authored in each repo over the last year, with its own sort (v0.32.0+). Off by default: it is the one column that costs a query of its own on every refresh. Add watch_repos = ["..."] to the config to monitor repositories you don't own — they appear in a third "Watched" section under your own list. Press w (v0.18.0+) to cycle the work filtersPRs open, CI broken, stale 90d — quick presets for "where is my attention needed"; they compose with the / search and apply to pinned / owned / watched sections alike. esc clears both filters at once (while the search box is open, esc first cancels that input — press it again to clear the work preset too).

  • PRs — every open pull request you've authored, across every repo. Number, title, repo, state (draft / ready / conflicts) and last-update time. Same sort & search idioms as Repos. PRs awaiting your review (v0.15.0+) surface in a sticky section at the top of the tab when someone has requested you as a reviewer — separated from your authored list by a muted rule, ordered most-recently-updated first.

  • Issues — every open issue you've authored, wherever it lives. Same shape as PRs minus the state column.

  • Activity — two halves, switched with / . Heatmap is the 52-week contribution calendar, plus total / current streak / longest streak / busiest day computed from the same cells. Feed (v0.29.0+) is the timeline underneath it: what you actually did, most recent first.

  • Gists (v0.29.0+) — your gists, newest first, with visibility, file count and stars. enter drills in and shows the file contents, syntax-highlighted, and c there copies the code rather than the link — a gist is a snippet, so reading and pasting it is the point. A one-file gist opens straight into it. An untitled gist is listed by its first filename rather than by its hash, and --public-only doesn't fetch the secret ones at all, so not even the count reveals how many there are.

  • Inbox (v0.29.0+) — your actual GitHub notification inbox: mentions, review requests, assignments, subscriptions, CI activity, newest first. Unread only — which is what an inbox is, and worth saying because it has a visible consequence: open a thread and it is gone from the tab on the next reload, because GitHub marked it read when you got there. enter opens the thread on GitHub, c copies its link, / filters, and s cycles a three-way filter — all, involving you (the reasons that name you specifically: mention, team mention, review requested, assigned, author, security alert) and ci.

    That filter is there because of a measurement, not a preference: of 104 notifications on a real account, 77 were CI activity — three quarters of an inbox reporting that a workflow succeeded. The default is still all, because an inbox that hides most of itself is worse than a noisy one, and the line under the table always says how many rows the current filter is hiding.

    Three things are worth knowing:

    • It needs a classic token. GitHub documents /notifications as supporting "only … a personal access token (classic)", with the notifications or repo scope. A fine-grained token — which the Token scopes section otherwise recommends — cannot read it at all, and the tab says so instead of looking broken.
    • It is read-only, like the rest of octoscope. Marking a thread read is a PATCH, and octoscope does not mutate GitHub state — so enter takes you to the thread, where GitHub marks it read. There is deliberately no --allow-mutations flag: that would make this a different product with a different trust model.
    • It loads when you open the tab, not on every refresh. GitHub asks callers to poll /notifications no more than once a minute, and --refresh goes down to 5s.
    • GitHub does not return the inbox in time order — measured — so octoscope sorts it. Notifications from private repositories are dropped under --public-only.
  • What's new (v0.16.0+) — the highlights of the version you're running, bundled into the binary so it works offline, plus a sponsor section (o opens the Sponsors page, c copies the link). Jump here any time with 8.

The Overview tab is organised in six sections:

  • Profile — name, login, pronouns, bio, company, location, website, and how many years you've been on GitHub

  • Social — Followers · Following · Stars received (across your non-fork repositories) · plus a 4th Stars + Forks card when you own forks that carry stargazers, so the dashboard reconciles with counters that don't filter forks out · plus Sponsors and Sponsoring cards (v0.29.0+) when there is any sponsorship in that direction

  • Activity — lifetime PRs authored and merged, lifetime issues authored, and commits in the last 12 months, with a takeaway line ("X% of N PRs merged · M still open") summarising the funnel; underneath, a Languages bar (byte counts across your owned repos, colour-matched to GitHub's own hex palette) and a Top repositories column ranking your five most-starred owned non-fork repos

  • Operational — repositories, forks received, open issues (own) and open PRs (own) across your owned repositories — the (own) qualifier disambiguates from your lifetime authored counts above

  • Sponsors (v0.29.0+) — who funds this account and who it funds, by name, with GitHub's own monthly income estimate. The whole section is absent unless there is something to say: an account with no sponsorship and no listing never grows an empty row. An open listing with no sponsors yet says so, because "nobody has sponsored me" and "I can't be sponsored" are different statements.

    Two limits are stated rather than papered over. There is no tier, start date or amount, and no way to tell a public sponsor from a private one: every one of those fields needs the read:user scope, and octoscope is not going to ask you to widen a token so a dashboard can print a tier name. And because the public/private distinction is exactly what can't be read, --public-only drops the section entirely — names, counts and income — rather than guessing which sponsors are safe to draw. The income figure is your own or it is not shown at all; GitHub answers that field with 0 for anybody else, so it is never read where a zero would be indistinguishable from "not allowed to know".

  • Network — the organisations you're a member of plus your verified social accounts (X, LinkedIn, Bluesky, Mastodon…)

The top header also shows whether the current session is authenticated and how fresh the data is. Auto-refresh runs every 60 seconds; press r at any time for an on-demand refresh.

The banner, profile card, tab bar and footer stay pinned. The body of the Overview and Activity tabs scrolls vertically when the rendered content overflows a short terminal — / for line, pgup / pgdn for page, u / d for half a page. A ↑/↓ scroll hint appears in the footer only when the active tab actually overflows.

Drill-in details

On any list tab (Repos, PRs, Issues, Gists), pressing enter on the selected row opens a rich drill-in view of that item; pressing space opens the action menu instead:

┌─ Actions for octoscope ─────────────────┐
│  ▸ o  Open in GitHub                    │
│    d  View details                      │
│    c  Copy URL                          │
│                                         │
│   ↑/↓ select · enter confirm · esc back │
└─────────────────────────────────────────┘

enter confirms the highlighted action; the underlined letter is a direct shortcut you can press from inside the menu to skip selection.

  • Open in GitHub (o) — opens the repo / PR / issue in your default browser. Same gesture is bound to o directly on the list row, so the menu is just one of two paths to the same action.

  • View details (d) — replaces the tab body with a rich drill-in view of the selected item, also reachable directly via enter on the row (the canonical TUI convention, mirroring lazygit / k9s / ranger). Per tab:

    • Repos — description, license, languages bar, latest release, a Checks section (v0.25.0+) breaking the list's CI dot down into the individual checks on the default branch's tip — failures first, each name an OSC 8 hyperlink to its run on github.com, c to expand past the first 8 on busy repos — a 12-month star-history sparkline (press v, v0.18.0+, to switch between weekly density and a cumulative growth curve à la star-history.com), recent commits with total + your-commits-in-the-last-year counts, open issues / PRs preview, topics.
    • PRs — title + state chip (Open / Draft / Merged / Closed), glamour-rendered description, reviewers, checks summary, files changed, recent commits, labels, curated timeline. Press f to inspect the changed files: a full-screen list with cursor navigation, and Enter on a file opens its unified diff in a syntax-highlighted viewer (chroma diff lexer + monokai palette). Both sub-views support ↑↓ / pgup / pgdn, o to open on github.com, c to copy the path, and esc to go back one level.
    • Issues — title + state chip, glamour-rendered description, assignees, recent comments, labels, linked PRs (those that would close the issue when merged), curated timeline.

    Each detail is fetched in a single targeted query for that one item, so it never affects the dashboard's rate-limit budget. The PR detail additionally pulls the file changeset from GitHub's REST /pulls/{n}/files in parallel with the GraphQL drill-in — wall-clock latency stays close to the slower of the two. While in the detail view: r refreshes the data, o opens the item in your browser, c copies its URL, esc goes back to the list (cursor preserved).

  • Copy URL (c) — copies the row's URL to your system clipboard (uses pbcopy on macOS, clip on Windows, wl-copy / xclip / xsel on Linux). A URL copied toast confirms in the footer.

  • Security scan (s, Repos only, v0.20.0+) — runs an on-demand supply-chain integrity scan of the repo and opens a read-only report with a weighted, explainable verdict (clean / watch / suspicious / likely compromised) for the Shai-Hulud / Miasma class of attack — an implant pushed to your repos that auto-runs when you open them in an AI editor or install them. It flags auto-execution surfaces, oversized / obfuscated payloads and forged or unsigned commit tips — matching the invariant of the attack, not a single filename, so renamed variants still trip it — ranks the evidence behind the verdict heaviest first, lists every auto-executing file it found, and shows per-branch commit-tip provenance. When something looks wrong it offers a copy-paste remediation script (y) and the right OAuth-grant revoke links. octoscope never mutates the repo.

    Each scan also records a fingerprint of the repo's auto-execution surface (v0.27.0+), so the next one reports what changed since last time — a file that auto-executes appeared, an existing one changed, or a branch tip that used to be signed no longer is. That signal survives renames and re-obfuscation, because a variant still has to appear. The first scan of a repo says so explicitly rather than passing for a clean comparison, and a baseline older than 30 days reports without affecting the verdict. The report states how wide its comparison window was (v0.30.0+) — "compared against a baseline recorded 29 days ago" — once, under the verdict. The scan runs when you ask it to, not on a schedule, so a delta measured over four minutes and one measured over four weeks are different claims, and a repository where nothing changed says which it is too. It also keeps an unbounded history (v0.30.0+) of the content it has seen at each path, so a file that changes and later changes back says so — "this exact content was observed here before, on 2026-06-01" — however long the round trip took. A note, never a score: reverting is usually somebody undoing a mistake. It is unbounded because a fixed lookback is a published waiting time, and waiting is the whole point of the attack this is meant to notice. It grows by distinct contents, not by scans — a file that oscillates between two versions forever stores two — so a real repository stays a few kilobytes. This is the one file octoscope writes on its own — scan-baselines.json, beside your config; deleting it just starts a fresh baseline.

    It also compares the dependency install surface (v0.33.0+) — the subset of your npm dependencies that run code at install time, read from package-lock.json / npm-shrinkwrap.json on the default branch. A dependency that did not run code at install and now does is a finding; the same version shipping different content is a stronger one, because no upgrade explains it. An ordinary version bump is inventory and scores nothing. It records the subset rather than the file because the file churns and the subset does not — measured over 57 lockfile revisions of axios/axios, npm/cli and nodejs/undici, the subset moved twice and the same version was never republished. npm only, and the report says so: pnpm dropped its build declaration at lockfileVersion 9 and Yarn never had one, so a repository whose only lockfile is theirs gets an explicit line saying its dependency surface was not compared — as does one with no lockfile, one whose lockfile is too large to read, and one whose schema octoscope has not measured. octoscope never looks at the registry: the claim is that your dependencies' auto-execute surface changed, never that a dependency is malicious.

    It also reports your capability footprint (v0.27.0+) — what a compromise of the repo could reach. Workflow permissions and triggers are read from the files it already fetched, plus self-hosted runners, write deploy keys and off-platform webhooks. Holding power is not itself a finding: a release workflow with contents: write on a tag push is normal and stays inventory. What scores is power reachable from untrusted input — a pull_request_target workflow holding the repo's secrets, or a self-hosted runner an outsider's pull request can reach. The probes that need admin scope fail open on a minimal token, and the report says which ones it could not check.

    Since v0.28.0 it stops taking a workflow file at its word. Reusable-workflow chains are followed: a fork-triggered workflow holding nothing that calls one reading a secret is one path to that secret, not two harmless files, and the finding names the caller an outsider comes through. Power travels the other way — a called workflow holds what its caller granted, never more, so one whose caller hands over nothing is not credited with anything it declares. And a workflow that declares no permissions: runs with your repository's default, which an owner can widen to read/write; octoscope reads that setting and joins it to the file, and where it cannot, says so rather than deciding the workflow holds nothing. Where a chain leaves what the scan can see (v0.30.0+) — a call into another repository, or one into this repository the scan did not read — it says so, always, not only where an outsider could reach the caller. It does not follow the call, and an unfollowed chain that reads like one which ended safely is the failure this axis exists to prevent. One line for the scan, naming where those workflows live.

Rate-limit awareness

The footer surfaces your GitHub GraphQL budget live:

Updated 12s ago  ·  rate 4872/5000  ·  reset 23m  ·  auto 60s

The chip is muted at normal levels, warn-yellow under 20% remaining, and error-red under 5%. If GitHub ever tells us we're out of budget, the auto-refresh backs off until the reset time instead of hammering every 60s.

When a refresh fails, the footer says whyrate-limited · retry at 14:23, token rejected · check $GITHUB_TOKEN, offline · retrying, or github errored · retrying — so you know whether to wait, fix auth, or check the network.

Transient GitHub hiccups — a 502 from the GraphQL gateway, an HTTP/2 stream error mid-flight — are retried automatically a few times with a short backoff before the error ever reaches the screen (v0.17.0). The full-screen error view now reads as a clean, human sentence instead of a raw stack of 502 Bad Gateway HTML, so a passing blip no longer looks like a broken app.

Press % (v0.18.0+) for the rate-limit detail panel: a per-resource breakdown of every REST + GraphQL budget — used, remaining and reset time for graphql, core, search and the rest — straight from GitHub's /rate_limit endpoint, which is free and consumes no quota. The footer chip tells you how you're doing; the panel tells you why. r refreshes it, esc closes.

Public-only mode

Pass --public-only to hide private repositories, PRs and issues from the lists — perfect for demos, screenshots and screencasts where you don't want internal work leaking. Global counters (PRs Authored, PRs Merged, Issues Authored) stay complete; only titles and repo names get filtered.

Activity tab

The Activity tab has two halves at different zoom levels, switched with / (or h / l): the heatmap answers how much, the feed answers what.

Heatmap

The last ~52 weeks of your contribution calendar, shaded on an accent-pink gradient that adapts to your own distribution (the busiest day always hits the full neon pink, the quiet days sit on the surface grey). Underneath:

  • Total contributions for the window
  • Current streak (how many consecutive days you've pushed)
  • Longest streak in the window
  • Busiest day with its date, so you know when you shipped the most

Feed (v0.29.0+)

A timeline of your recent events — pushes, pull requests opened and merged, reviews, issues, releases, branches created and deleted — newest first, across every repository at once. enter opens the row's subject on GitHub, c copies its link, / filters on anything the row shows (a repo name, a verb like merged, part of a title), and r reloads.

Four things are worth knowing, because they are properties of GitHub's events API rather than choices:

  • It is genuinely recent, not a history. GitHub keeps a limited window and octoscope reads one page of 100 events from it. On a busy account that page can cover as little as two days. The line under the table always names the span it actually got, and says when it is at the cap.
  • Review and comment traffic on the same subject is folded into one row with a ×N count, so a heavily-reviewed pull request does not bury everything else. Only adjacent events on the same subject fold — nothing is reordered — and anything that changed state (opened, merged, closed, pushed, released, approved, changes requested) always keeps its own line.
  • Pull-request rows show a title when one can be found. GitHub sends a truncated pull-request object with no title in it; where the same page contains a comment on that pull request, the title comes from there. When it does not, the row shows the bare number rather than inventing a label.
  • It loads when you open it, not on every dashboard refresh — GitHub asks callers to poll this endpoint no more than once a minute, and --refresh goes as low as 5s.

Under --public-only the feed asks GitHub's public-events endpoint, so events in private repositories are never fetched at all; toggling the mode with p mid-session drops them from what is already on screen. The visibility column only appears when there is a mix to distinguish.

Live feedback

  • Change pulse — whenever a value changes between two refreshes (e.g. a new star arrives, someone follows or unfollows you, an issue gets closed), the affected card's border flashes accent-pink for 2 seconds.
  • Native notifications — Stars and Followers changes also trigger a system notification and a short audio beep, so you notice the "passive" events even when octoscope is in a background tab. Clicking the banner opens the relevant page (your profile for follower changes, the starred-repos tab for star changes).
    • macOS: notifications go through terminal-notifier, which the Homebrew formula installs automatically as a dependency. If you installed octoscope another way (go install, manual binary), run brew install terminal-notifier once. The click-through works; the custom icon does not — Apple deprecated NSUserNotification in macOS 11, and the system now ignores -appIcon overrides for notifications coming from CLI tools. The banner shows the terminal-notifier icon instead. Cosmetic only.
    • Linux & Windows: notifications carry the embedded octoscope icon. Click activation depends on your DE / shell — best-effort.
  • Update notice — on launch (and hourly) octoscope checks whether a newer release is out and, if so, shows a quiet line under the banner with the right upgrade command for how you installed it (brew, go install, gh extension, or a download link). It never self-updates — the package manager owns the binary. Turn the check off with check_for_updates = false; it's also suppressed under --public-only.
  • GitHub's own status (v0.30.0+) — when a fetch fails, octoscope asks githubstatus.com whether the problem is GitHub's, and says so instead of leaving you to guess. Silent while GitHub is healthy, silent if the status page can't be reached, and quiet about incidents that don't touch anything octoscope uses. Never polls. Opt out with check_service_status = false.
  • Actionable auth errors (v0.23.0+) — an expired or revoked token says so and names the fix for where the token came from: $GITHUB_TOKEN points at the regenerate URL, a gh CLI login points at gh auth refresh. A token that's valid but missing a scope names the scopes GitHub asked for.
  • Watched-entry notices (v0.23.0+) — a watch_repos entry that no longer resolves (renamed, deleted, gone private) surfaces as a muted "N watched entries skipped" line under the Repos tab naming the stale refs, instead of vanishing silently. Transient network blips still pass quietly.

What octoscope can't show

Some things you can see on your GitHub profile page are not exposed by the GitHub GraphQL or REST API, so octoscope doesn't show them:

  • Achievements (Pull Shark, Starstruck, YOLO, …)
  • Highlights like the PRO badge
  • The local time next to the location field

Supporting any of these would require scraping the profile HTML, which we don't do.

Install

Homebrew (macOS & Linux)

brew install gfazioli/tap/octoscope

brew upgrade gfazioli/tap/octoscope picks up newer versions as they ship.

From source

go install github.com/gfazioli/octoscope@latest

Requires Go 1.25.11 or later.

gh extension

gh extension install gfazioli/gh-octoscope
gh octoscope

The same binary from the same release, reachable without leaving gh. It lives in a second repository only because gh derives an extension's name from the repository name and refuses anything not prefixed gh- — there is nothing else in it, and issues belong here.

Docker

The image is for the scriptable half of octoscope, not the dashboard: a TUI in a container has nothing attached to read it, while --json and --plain fetch once, print and exit.

docker run --rm -e GITHUB_TOKEN ghcr.io/gfazioli/octoscope:latest --plain

Multi-arch (linux/amd64, linux/arm64), ~7 MB, and it runs as an unprivileged user. -e GITHUB_TOKEN with no value passes the variable through from your shell rather than baking it into the command line, where it would be visible in ps and in your shell history.

In a CI step, piping into jq:

- name: Publish account stats
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  run: |
    set -o pipefail
    docker run --rm -e GITHUB_TOKEN ghcr.io/gfazioli/octoscope:latest --json \
      | jq -r '"followers=\(.social.followers) stars=\(.social.total_stars)"'

With no token it exits 1 with an empty stdout — but a pipeline only notices with pipefail set, otherwise the exit status is jq's and jq is perfectly happy with no input. GitHub Actions does not set it for you unless the step says shell: bash; the default is bash -e {0}. Hence the line above, which travels to any CI.

Useful fields for a gate: .rate_limit (remaining, reset_at), .review_requests, .activity.

Run it with no flag at all and it refuses rather than hanging — the dashboard needs a terminal the container does not have:

octoscope: could not open a new TTY: open /dev/tty: no such device or address

Pre-built binary

Download the right platform archive from the latest GitHub Release, unpack it, and drop the octoscope binary anywhere on your $PATH.

Each release also carries the bare executables next to the archives, named octoscope_<version>_<os>-<arch>, for when unpacking is the awkward part:

VERSION=0.33.0   # or whatever the latest release says
curl -fsSL -o octoscope \
  "https://github.com/gfazioli/octoscope/releases/download/v${VERSION}/octoscope_${VERSION}_linux-amd64" \
  && chmod +x octoscope

The version appears in the asset's own name, so the releases/latest/ shortcut cannot be used here — it resolves the newest release but still asks for the file name you typed, which 404s the moment a newer one ships.

Usage

octoscope                       # your dashboard (requires a token)
octoscope <username>            # any public profile (token optional)
octoscope --refresh 30s         # auto-refresh every 30 seconds
octoscope --compact             # dense card layout for narrow terminals
octoscope --public-only         # hide private repos/PRs/issues (safe for demos)
octoscope --no-sponsor          # skip the sponsor splash for this run
octoscope --theme phosphor      # 80s green CRT theme — see Themes section
octoscope --theme list          # preview all built-in palettes and exit
octoscope --no-color            # force the monochrome theme (or set NO_COLOR)
octoscope --plain               # static text summary, no TUI
octoscope --json                # machine-readable JSON, no TUI

Examples:

octoscope                # you
octoscope torvalds       # Linus Torvalds
octoscope gvanrossum     # Guido van Rossum
octoscope gfazioli       # the author
octoscope --public-only  # you, but screenshot-safe

Scripting — --plain and --json

octoscope can run non-interactively: it fetches the dashboard once, prints it, and exits without ever entering the TUI. Two modes:

  • --plain — a human-readable text summary (profile counters plus the key lists), colourless and safe for shell status-lines and quick checks. Its exact layout is not a contract and may change between releases.
  • --json — the same data as JSON, for piping into jq, cron jobs, or a status-line generator.

Both honour --public-only and the usual auth cascade ($GITHUB_TOKENgh auth token). They are mutually exclusive.

octoscope --json | jq '.social.total_stars'
octoscope --json --public-only > snapshot.json
octoscope torvalds --plain

JSON schema — a stable contract

The --json output is a versioned contract: the top-level schema_version (currently 1) is bumped only on a breaking change (a renamed, removed, or retyped field). New fields may be added without a bump, so pin your consumers to schema_version and treat unknown keys leniently. Every list is always an array (never null), so you can iterate unconditionally.

{
  "schema_version": 1,
  "octoscope_version": "0.24.1",
  "generated_at": "2026-07-08T12:00:00Z",
  "authenticated": true,
  "is_viewer": true,
  "public_only": false,
  "profile":     { "login": "...", "name": "...", "bio": "...",
                   "company": "...", "location": "...", "created_at": "..." },
  "social":      { "followers": 0, "following": 0,
                   "total_stars": 0, "total_stars_with_forks": 0 },
  "activity":    { "prs_total": 0, "prs_merged": 0, "issues_authored": 0,
                   "open_prs_authored": 0, "commits_last_year": 0,
                   "contributed_repos_last_year": 0 },
  "operational": { "public_repos": 0, "forks_received": 0,
                   "open_issues": 0, "open_prs": 0 },
  "languages":    [ { "name": "Go", "bytes": 0, "percent": 0.0 } ],
  "repositories": [ { "name": "...", "url": "...", "language": "...",
                      "stars": 0, "forks": 0, "open_issues": 0, "open_prs": 0,
                      "pushed_at": "...", "private": false,
                      "ci_state": "SUCCESS",
                      "latest_release": { "tag": "...", "published_at": "..." } } ],
  "open_pull_requests": [ { "number": 0, "title": "...", "repo": "owner/name",
                            "url": "...", "draft": false, "mergeable": "MERGEABLE",
                            "updated_at": "...", "private": false } ],
  "open_issues_list":   [ { "number": 0, "title": "...", "repo": "owner/name",
                            "url": "...", "updated_at": "...", "private": false } ],
  "review_requests":    [ /* same shape as open_pull_requests, plus "author" */ ],
  "organizations":  [ { "login": "...", "name": "..." } ],
  "sponsors":       [ { "login": "...", "name": "...", "url": "...", "is_org": false } ],
  "sponsors_total":   0,
  "sponsoring":     [ /* same shape as sponsors */ ],
  "sponsoring_total": 0,
  "has_sponsors_listing": false,
  "monthly_sponsors_income_cents": 0,
  "watched_repos":  [ /* same shape as repositories */ ],
  "watched_skipped": [ "owner/renamed" ],
  "rate_limit": { "cost": 0, "limit": 5000, "remaining": 0, "reset_at": "..." }
}

ci_state, latest_release, rate_limit and monthly_sponsors_income_cents are omitted when empty / unavailable. Lists follow the same caps as the TUI (repositories up to 100, PRs / issues up to 50, sponsors up to 20 per direction).

The sponsor totals are separate from the lists on purpose: both lists are capped, so comparing len(sponsors) against "how many sponsors do I have" is wrong on a busy account — read sponsors_total. And monthly_sponsors_income_cents is only ever present for the account the token belongs to.

Themes

octoscope ships with seven built-in themes. Pick one with --theme NAME, the theme config key, or cycle through them live in the in-app settings panel (, → arrow down to Theme / ). The panel also edits the accent colour override and the sponsor splash toggle (v0.26.0) — the last two config keys that were previously file-only.

Theme Vibe
octoscope Default — pink + cyan
high-contrast Pure white accent, ANSI brights — maximum legibility
terminal Inherits from your emulator's palette (ANSI 8-15)
monochrome All-greys, zero chroma
stranger-things Crimson + Christmas-lights yellow on an "Upside Down" muted
phosphor 80s P1/P31 CRT pure green — vt100 vibe
amber 80s amber CRT (IBM 5151, WordStar)

Three of them, side by side:

stranger-things phosphor terminal
stranger-things theme phosphor theme terminal theme — uses the host emulator palette, here purple from the user's iTerm

The terminal theme is special: it picks colours from the ANSI brights of your emulator's palette, so it follows your iTerm / Ghostty / Alacritty colour scheme automatically. The screenshot above is what terminal looks like with the author's iTerm palette — yours will differ.

You can override just the accent colour while keeping the rest of a theme via the accent_color config key (or --theme plus an accent_color in the file). Any value lipgloss accepts works: hex like "#FF0080" or ANSI 256 numbers like "201".

No colour

octoscope honours the NO_COLOR convention (since v0.22.0): when the NO_COLOR environment variable is present and non-empty (its value doesn't matter), or you pass --no-color, octoscope forces the zero-chroma monochrome theme, overriding any --theme / theme config value and dropping the accent override. It's an environment directive for that run only — the theme / accent_color keys saved in your config file are left untouched, so your real theme comes back the moment NO_COLOR is unset.

Configuration

octoscope reads ~/.config/octoscope/config.toml on startup (honours $XDG_CONFIG_HOME when set). All keys are optional; missing keys fall back to defaults. CLI flags override the file. The file is not created automatically — write it yourself when you want to customise.

# ~/.config/octoscope/config.toml

# Auto-refresh interval. Go duration syntax: "30s", "1m", "5m", "1h".
# 0 / negative falls back to 1m; values below 5s are raised to 5s so a
# typo can't hammer the GitHub API.
refresh_interval = "1m"

# Hide private repositories, PRs and issues from the list tabs.
# Useful if you screenshot or screencast octoscope often. Global
# counters (PRs Authored, Issues Authored, ...) stay complete since
# they're aggregate numbers, not titles.
public_only = false

# Use the dense card layout in the Overview tab — smaller cards,
# abbreviated labels. Fits more onto narrow terminals.
compact = false

# Visual theme. Built-in: octoscope (default), high-contrast,
# terminal, monochrome, stranger-things, phosphor, amber.
theme = "octoscope"

# Initial view preferences (v0.23.0+). One sort key seeds every tab
# whose sort cycle has that column: pushed | stars | forks | name |
# ci | release | commits (needs commit_counts) apply to the Repos tab;
# updated | repo | number apply
# to the PRs and Issues tabs. Unset keys keep the built-in defaults
# (pushed / updated, no work filter, density sparkline).
default_sort = "pushed"

# Repos work filter preset (the `w` cycle): "prs-open" | "ci-broken"
# | "stale". Empty = no filter.
default_work_filter = ""

# Star-history sparkline mode in the repo drill-in (the `v` cycle):
# "density" | "cumulative".
default_star_history = "density"

# Show the sponsor splash at launch (v0.16.0+). Set to false to opt
# out, or pass --no-sponsor for a single run. Always suppressed under
# --public-only so screenshots stay clean.
show_sponsor = true

# Check for a newer octoscope release on launch + hourly, and show a
# small notice under the banner when one exists (v0.19.0+). octoscope
# never self-updates — it only suggests the upgrade command for how you
# installed it. Set to false to disable the check entirely.
check_for_updates = true

# Ask GitHub's own status page (www.githubstatus.com) whether GitHub is
# healthy, so an outage on their side doesn't look like an octoscope bug.
# Unauthenticated, costs nothing against your rate limit, and never
# polls. Silent unless something octoscope depends on is unhealthy — and
# silent too if the status page can't be reached, because octoscope never
# claims a clean state it hasn't verified. This is the only feature that
# contacts a host other than api.github.com; set false to opt out.
check_service_status = true

# Add a "commits by you, last year" column to the Repos tab (v0.32.0+).
# Off by default: it is the one column that costs a query of its own on
# every refresh — GitHub counts each repository's history on request —
# and it needs an authenticated viewer to count for. Sortable with s;
# also a toggle in the settings panel (,).
commit_counts = false

# Optional override for just the accent slot of the active theme.
# Hex ("#FF0080") or ANSI 256 ("201"). Leave unset to keep the
# theme's default accent.
# accent_color = "#FF0080"

# Repositories pinned to the top of the Repos tab. Each entry is
# "owner/name"; order is preserved exactly as listed here. Press
# P on any repo row to add / remove it — the file is rewritten
# atomically. Malformed entries are dropped silently at load.
# pinned_repos = [
#   "gfazioli/octoscope",
#   "gfazioli/Mantine-Hint",
# ]

# Issues pinned to the top of the Issues tab (v0.21.0+). Each entry
# is "owner/name#N"; order is preserved exactly as listed here. Press
# P on any issue row to add / remove it — the file is rewritten
# atomically. Malformed entries are dropped silently at load, and a
# pinned issue that gets closed simply stops showing (the entry is
# harmless).
# pinned_issues = [
#   "gfazioli/octoscope#42",
#   "charmbracelet/bubbletea#1234",
# ]

# External repositories to monitor in a Watched section under
# the Repos tab (v0.14.0+). Hand-edit only — there is no
# in-app toggle. Each entry resolves to its own GraphQL query
# at refresh time; failures (404, private, network blip) are
# dropped silently so a stale entry doesn't break refresh.
# watch_repos = [
#   "charmbracelet/bubbletea",
#   "cli/cli",
# ]

Pass --config PATH to read a different file (handy for trying out configs without touching the default one).

A malformed TOML file makes octoscope exit with an error so you notice the typo straight away — there's no silent fallback to defaults when the file is present but broken.

In-app settings panel

You don't have to drop to a shell to tweak settings: press , (comma) while octoscope is running and a settings panel opens. Use / (or Tab) to move between rows, space to flip a toggle, / to cycle the theme picker, type to edit the refresh field, Enter to save, Esc to cancel.

What you change applies live and instantly: a new refresh_interval reschedules the auto-refresh tick, compact re-renders, public_only filters the lists on the spot, and theme rebuilds every style on save so the dashboard repaints in the new palette without a restart. The panel persists changes back to your config file (the default path or whatever you passed to --config), so the next launch picks them up too.

For the most common toggle, you also get a single-key shortcut outside the panel: hit p from any tab and public-only flips state immediately, with the file updated alongside. A yellow ◐ public-only badge next to authenticated in the profile card makes the current mode unmissable at a glance.

Key bindings while running:

Key Action
1-8 Jump to tab (Overview, Repos, PRs, Issues, Activity, Gists, Inbox, What's new)
tab / shift+tab Cycle tabs forward / backward
/ , j / k Move cursor in list tabs and the Activity feed · scroll Overview / Activity heatmap when the body overflows the window
pgup / pgdn, u / d Page up / down on Overview & Activity (vertical scrolling)
space On Repos / PRs / Issues / Gists / Inbox: open the action menu for the selected row · on Overview / Activity: page down
enter (Activity feed) Open the event's subject on GitHub — the pull request, issue, comment, release or compare view
g / G Jump to top / bottom
s Cycle sort column · on Inbox: cycle the filter (all / involving you / ci)
/ Filter by substring
w On the Repos tab: cycle the work filter (PRs open → CI broken → stale 90d → off) — composes with /, spans pinned / owned / watched sections
v Inside a repo's detail view: toggle the star-history sparkline between weekly density and cumulative growth
% Open the rate-limit panel: per-resource REST + GraphQL budget breakdown (used / remaining / reset) from GitHub's free /rate_limit endpoint
enter On Repos / PRs / Issues: open the drill-in detail view for the selected row · in any menu: confirm the highlighted action · in the PR files-list sub-view: open the file's diff
o Open the selected repo / PR / issue in your browser (or, inside the PR diff viewer, the PR's Files-changed tab on github.com)
c Copy the selected row's URL (or, inside the PR diff viewer / files list, the current file's path) to your system clipboard
f Inside the PR drill-in: inspect the changed files (full-screen list → Enter opens each file's diff with syntax highlighting)
P On a Repos or Issues row: toggle pin/unpin (pinned rows stick to the top of that tab, ordering preserved across refreshes; writes back to config)
o / d / c / P Inside the action menu on Repos or Issues: Open in GitHub · View details · Copy URL · Pin/Unpin
esc Close the action menu / detail view, or clear the current filter
r Refresh now (or refetch the current detail view when open)
p Toggle public-only mode (saves to config)
, Open the in-app settings panel
? Open the keyboard-shortcut overlay (any key to dismiss)
/ , h / l On the Activity tab: switch between the heatmap and the feed
/ Cycle theme (when the Theme row is focused in the settings panel)
q Quit
ctrl+c Quit

(Pass --help for the full list of CLI flags and their defaults.)

Authentication

octoscope resolves a GitHub token from, in order:

  1. $GITHUB_TOKEN environment variable
  2. gh auth token — if the GitHub CLI is installed and logged in
  3. No token — falls back to the unauthenticated GitHub rate limit (60 req/h)

Rules of thumb:

  • Viewing your own account (octoscope with no arg) requires a token — there's no "viewer" to resolve without one.
  • Viewing any other user (octoscope <username>) works with or without a token, but without one the unauthenticated 60 req/h limit gets burned through fast at the default 60-second refresh interval.

A token is effectively required if you plan to keep the dashboard open for more than a few minutes regardless of whose profile you're viewing.

Token scopes

octoscope is read-only — it never mutates anything on your account. The minimal scopes it needs depend on which kind of token you mint:

Fine-grained personal access token (recommended). All permissions are read-only:

  • Repository permissions
    • Metadata — Read (mandatory)
    • Contents — Read
    • Issues — Read
    • Pull requests — Read
  • Account permissions
    • Profile — Read
    • Followers — Read
    • Email addresses — Read (only if you want the email field on the profile card)

Under Repository access pick All repositories (or just the ones you want to see).

One exception, and it is GitHub's rather than octoscope's. The Inbox tab reads /notifications, and GitHub documents that endpoint as supporting "only … a personal access token (classic)" — so a fine-grained token cannot load it, whatever permissions you grant. Every other tab works normally; the Inbox says what happened rather than failing silently. If you want it, mint a classic token with notifications or repo.

Classic personal access token:

  • read:user — profile, followers, social accounts
  • notifications — the Inbox tab. repo also covers it, so if you already have repo there is nothing to add
  • repo — required to see your private repos / PRs / issues. Drop it if you only care about public content; the dashboard still works and just hides private items.
  • read:org — only needed if you're a member of orgs with private membership and want them under Network. Public org memberships show up without it.

Via gh auth token: if the GitHub CLI is already logged in (gh auth login), octoscope picks up that token automatically — the default gh scopes already cover everything.

octoscope never needs write:*, delete_repo, admin:*, gist, or any workflow scope.

Contributing

Bug reports and ideas are welcome via issues. Pull requests, too — please open an issue first for anything non-trivial so we can agree on the shape before code lands.

Sponsor

Your support helps me:

  • Keep the project actively maintained with timely bug fixes and security updates
  • Add new features, improve performance, and refine the developer experience
  • Expand test coverage and documentation for smoother adoption
  • Ensure long-term sustainability without relying on ad hoc free time
  • Prioritize community requests and roadmap items that matter most

Open source thrives when those who benefit can give back—even a small monthly contribution makes a real difference. Sponsorships help cover maintenance time, infrastructure, and the countless invisible tasks that keep a project healthy.

Your help truly matters.

💚 Become a sponsor today and help me keep this project reliable, up-to-date, and growing for everyone.

Since v0.16.0, octoscope shows a small sponsor splash at launch (press o to open the page, c to copy the link, or any other key to dismiss). It's suppressed automatically under --public-only. To turn it off, set show_sponsor = false in your config, or pass --no-sponsor for a single run.

On terminals that support OSC 8 hyperlinks (iTerm2, WezTerm, Kitty, recent VS Code), the splash link is clickable directly — no need to copy/paste the URL (v0.17.0).

License

MIT — see LICENSE.

About

Terminal dashboard for GitHub — followers, stars, PRs and issues at a glance, auto-refreshed

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

63 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages