Guide

Scripting

octoscope isn't only a TUI. Two flags fetch once, print, and exit — for quick checks, shell status-lines, cron jobs and pipelines.

--plain — a text summary

A static, human-readable digest of the same data the dashboard shows, printed to stdout. No alt-screen, no colour codes to fight with.

$ octoscope --plain
$ octoscope torvalds --plain     # anyone's public profile

--json — a stable contract

The same data as JSON, versioned (schema_version) and documented — so a script can depend on its shape. Fields are added additively, every list is always an array, and a breaking change bumps the version.

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

Both honour --public-only and the usual auth cascade, and the two are mutually exclusive.

Since v0.38.0 a server error from GitHub — a 500, 502, 503 or 504, typically the 502 its gateway answers on a bad moment — or a dropped HTTP/2 stream is tried up to three times in all, with a short backoff between attempts, as the dashboard does, so a scheduled run rides out the blips the TUI never shows you. A rejected token, a rate limit, a user that does not exist or a network timeout fails at once. Either way a failed run exits with status 1, the reason on standard error and nothing on standard output.

--activity — the feed, on request

The recent-activity feed — the same events the TUI's Activity tab shows — is opt-in in both output modes. It costs one extra API request, and most scripted runs only want the counters, so it is off unless asked for.

$ octoscope --json --activity | jq '.recent_activity[0]'
$ octoscope --plain --activity

Two details worth knowing. recent_activity is absent rather than empty when there is nothing to report — the only list besides inbox below that behaves this way: no key means the flag was not passed, [] means it was and the account has no recent events. And the rows are newest first because octoscope sorts them — GitHub's own feed interleaves two id spaces and arrives roughly a quarter out of order.

On its own, --activity is a usage error rather than a no-op: the TUI shows that tab regardless, so there is nothing for the flag to do without --plain or --json.

--inbox — your notifications, on request

Since v0.37.0 the Inbox tab has the same switch: --inbox adds your unread notification threads to either output mode, newest first, one page of up to 50 — the page the tab loads. Three rules it shares with --activity: it is opt-in, inbox is absent rather than empty when not asked for (and [] when your inbox is empty), and on its own it is a usage error. One it does not: it refuses a username, because the inbox is always the token's own.

$ octoscope --json --inbox | jq '[.inbox[] | select(.reason == "review_requested")] | length'
$ octoscope --plain --inbox

It is a flag of its own rather than part of --activity because it fails differently. GitHub's notifications endpoint does not accept a fine-grained token whatever its permissions (see Authentication), so a script running on one would fail on --inbox and nothing else — and a status line that wants your inbox rarely wants a hundred events beside it. Under --public-only threads from private repositories are left out.

--scan — every repository at once

Since v0.38.0. The security scan answers “is this repository compromised?” one repository at a time; --scan answers “is anything of mine?” It runs the same scan on the default branch of every repository you own, then of every entry in your watch_repos list, so an organisation's repositories are included the way they are in the dashboard — prints a report and exits. About fifteen seconds for seventy repositories.

$ octoscope --scan
$ octoscope --scan --json | jq '.summary'

Three things it deliberately does not do. It reads the default branch only — run the dashboard's scan, which walks up to 20 branches, on anything it flags. It does not compare with earlier scans and records nothing: a default-branch fingerprint would make the next full scan report every other branch as gone. And a repository it cannot read is never counted clean: it is listed as not scanned, with why, and the JSON summary's counts add up to the number of repositories for that reason. A file it could not read is listed with its repository whatever the verdict — lockfiles excepted, since everything read in one is a comparison with an earlier scan.

The JSON is a contract of its own (schema_version: 1), separate from the dashboard report's. It needs a token, honours --public-only (it names a repository only when GitHub said it is public, and counts a watched entry it could not place), retries a transient GitHub 5xx, and writes its progress line to standard error only when that is a terminal, so a cron job stays quiet.

↗

The full field-by-field JSON schema lives in the README's Scripting section — this page is the tour, the README is the contract.

Two things about the shape that a field list will not warn you about. Counts are separate from lists, because the lists are capped — sponsors holds up to 20 entries while sponsors_total is what GitHub reports, so comparing len(sponsors) against “how many sponsors do I have” is wrong on a busy account. Same for repositories, PRs and issues, which have caps of their own. And monthly_sponsors_income_cents is only ever present for the account the token belongs to — GitHub answers that field with 0 for anybody else, so it is omitted rather than emitted as a zero somebody could read as a measurement.

In a container

Since v0.31.0 the same two flags are available as an image, which is the shape a CI step wants — no install, fetch once, print, exit:

$ docker run --rm -e GITHUB_TOKEN ghcr.io/gfazioli/octoscope:latest --json | jq .social

-e GITHUB_TOKEN with no value passes the variable through from the surrounding environment rather than putting the token on the command line, where it would show up in ps and in shell history.

Two behaviours worth knowing before you depend on it. With no token it exits 1 and writes nothing to standard output — but a shell pipeline only notices that with set -o pipefail, since otherwise the exit status is jq's and jq is perfectly happy with no input. GitHub Actions does not enable it for you unless the step says shell: bash. And with no flag at all the container refuses rather than hanging, because the dashboard needs a terminal it does not have.