Releases: gfazioli/octoscope
Release list
v0.30.1
A display patch over 0.30.0. Nothing about what octoscope reports has changed.
The What's new tab pushed everything else off the screen
On 0.30.0, opening the What's new tab carried the banner, the profile card and
the tab bar off the top of the terminal — and there was no way to scroll them
back. Reported with a screenshot, which is the only way this kind of defect gets
found.
Two things were wrong, and they are different in kind.
The entry was too long. 0.30.0's What's new text was written as paragraphs:
1981 characters against 1528 for 0.29.0 and 647 for 0.26.0, the longest ever
shipped. The instruction to keep each item to three to five lines sits directly
above the list it belongs to, and it was not followed. It is now the shortest of
the recent four.
The tab could not scroll, and never could. It was the only long static
surface in octoscope without a viewport — Overview, Activity, every drill-in and
the scan panel all have one. Its renderer took a width and no height, so by
signature it could neither clip its content nor scroll it; the whole body was
written out and the terminal carried the rest away. It scrolls now, wired the
same way the Overview tab is, and the footer offers ↑/↓ only when there is
something to scroll to.
The shorter text alone would not have fixed this. Even 0.26.0's much smaller
entry overflows a thirty-row terminal, so the fault was old and general — the
long entry made it certain rather than merely likely.
Notable polish
- Every What's new entry is now measured against a line budget by a test. The
rule existed; nothing enforced it, which is why it could ship. - 0.30.1 shows 0.30.0's highlights rather than its own. The tab renders only the
running version's entry, so anyone upgrading straight from 0.29.0 lands here
and would otherwise never see what the release was about.
Upgrade
brew upgrade gfazioli/tap/octoscope
v0.30.0
Three of the four things in this release were written by someone else, in a comment
under the 0.27.0 launch. He did not ask for features. He found three places where the
report said more than it had verified, and all three turned out to be true when they
were measured.
The fourth came from a GitHub outage that looked exactly like an octoscope bug.
They compose into one sentence: when octoscope cannot tell you something, it says
so.
When it is GitHub, octoscope says so
octoscope does nothing but talk to GitHub, so when GitHub degrades every symptom — a
timeout, an empty tab, a 502, a scan that will not finish — is indistinguishable from
octoscope being broken. It now asks GitHub's own status page and tells you which it
is, at the moment you are deciding who to blame.
It is silent while GitHub is healthy, and silent again if the status page itself
cannot be reached: octoscope never renders a green "all systems operational", because
that would be a clean bill of health it has not verified. Only the services it
actually uses count — API Requests, Issues, Pull Requests, Actions, Webhooks — so an
incident confined to Codespaces or Copilot changes nothing and warns you about
nothing.
One unauthenticated request, to a different host from the API, so it costs nothing
against your rate limit. It does not poll: launch, r, and after a failed fetch —
plus your own refresh cadence while a warning is already up, which is what lets the
warning disappear when GitHub recovers.
Two measurements shaped it and both contradicted the plan. Atlassian's own API
reference states the component vocabulary verbatim and omits one of the values,
which is why an unrecognised state falls toward the warning rather than toward
"fine". And 15 of 50 real GitHub incidents carry no component information at all, so
the incident is context and the component state is the signal — never the other way
round.
The scan says how wide its window was
The scan runs when you ask it to, not on a schedule, so its store records when you
were paying attention, not what happened over time. "Unchanged since the last scan,
four minutes ago" and "unchanged since the last scan, twenty-nine days ago" are very
different sentences, and they used to render identically: the span was printed only
when the baseline was already stale.
The report now states it once, under the verdict — "compared against a baseline
recorded 29 days ago" — and a repository where nothing changed says it too, which
is the case that matters most and the one a per-finding note could never reach.
Always phrased as a gap, never as "unchanged for 29 days", which would claim a
continuous watch the tool does not keep. And the span is rounded rather than
truncated: truncation can only ever understate, and a narrower stated window claims a
tighter bracket around when something changed than the measurement supports.
A chain it cannot follow is said out loud
A uses: into another repository — or into your own that octoscope did not read — is
now always disclosed, not only where an outsider could reach the caller. Gating
the score on reachability is right. Gating the disclosure on it was the one
collapse this axis exists to prevent: a cross-repository call rendered exactly like a
chain that terminated safely, and silence is the reading it must never support.
It stays a note and never a score. Reachability still decides what counts against the
verdict; it just no longer decides whether you are told.
The noise objection was measured rather than argued. Across six real repositories,
ungating this added nothing at all to four of them — including one with 138 findings —
and seven to a repository that delegates its entire CI to another one and whose report
had said nothing about it.
A file that changes back is recognised
octoscope now keeps every distinct version it has ever seen at a path, so A → B → A
reports the return with the date that content was first observed there. Before, the
second change printed a blob hash identical to one the first scan had stored, and
nothing knew it — because retention was bounded at one, decided by the scoring
window rather than on its own terms.
There is no expiry, and that is the point: a fixed lookback is a published waiting
time, and waiting is the whole point of the attack this notices. Anything willing to
revert itself quietly is willing to wait out thirty days.
A note, not a score — reverting is usually somebody undoing a mistake. And it says
observed here on a date, never was here, because an on-demand scan cannot know
what stood there while nobody was looking.
Growth was measured rather than hoped at: the history grows by distinct contents,
not by scans, so a file that oscillates between two versions forever stores two.
Five workflows carrying twenty versions each is 10 KB.
Notable polish
- The service-status warning is suppressed under
--public-only, so a recorded demo
cannot pick up a live incident, and it disappears the moment you pressp. - The scan's boundary disclosure names the repositories the chains go to rather
than every filename: one real repository hands fifteen workflows to a single shared
repo, which as a list ran to about 1200 characters. check_service_status = falseopts out entirely. This is the only feature in
octoscope that contacts a host other thanapi.github.com, which is what makes it
the only one that needs the knob.
Tests
Every guard in this release was validated by mutation — broken deliberately, the test
required to fail, then restored — across nine separate batteries, each of which
ended with every mutant caught. Several did not start that way: the survivors were
real coverage gaps, including a rounding change that had quietly moved a boundary and
encoded the move in a test rather than declaring it.
Two defects were found by running the code rather than by testing it: a warning that
would have stayed on screen after GitHub recovered, and a correct diagnosis being
muddied by an irrelevant one when a rejected token was blamed on an unrelated outage.
And one was found by re-reading arithmetic that had been in the tree for releases:
negating the most negative int64 is a no-op, and time.Time.Sub saturates there
rather than wrapping — so a corrupted baseline timestamp scored at full weight while
claiming the tightest window the report can express. The exact failure this work is
about, at its maximum.
Upgrade
brew upgrade gfazioli/tap/octoscope
v0.29.0
octoscope has always been a dashboard you keep open. But four things still sent you
to a browser, and they were the same four every day: a gist you needed the code out
of, what you actually did this week, who funds the work, and the notification that
mentioned you.
0.29.0 is those four.
Gists, with the code in them
Your gists, newest first — visibility, file count, stars, last change. enter drills
in and shows the file contents, syntax-highlighted and scrollable, and c there
copies the code rather than the link. A gist is a snippet; reading and pasting it is
the point. A one-file gist opens straight into it, because that was the only thing
there was to see.
An untitled gist is listed by its first filename rather than its hash — and the filter
matches that, so you can find it by the name you can actually see. Under
--public-only the secret ones are never requested rather than fetched and hidden,
which keeps the count honest too: a "10 of 16" would itself have told anyone watching
that six secret gists exist.
Activity has a second half
← / → switches the contribution heatmap for a feed: pushes, pull requests
opened and merged, reviews, issues, releases, branches created and deleted — newest
first, across every repository at once. The heatmap answers how much; the feed
answers what.
Review and comment traffic on one subject folds into a single row with a ×N count,
so a heavily-reviewed pull request no longer buries the rest of the week. Only
adjacent events on the same subject fold, nothing is reordered, and anything that
changed state — an approval included — always keeps its own line. Folding the one
review that mattered into a "×12" is precisely the failure that rule exists to
prevent.
It is recent, not a history, and says so. GitHub keeps a limited window of events and
octoscope reads one page of 100 from it; on a busy account that was two days. The
line under the table always names the span it actually got.
Who funds your work, and who you fund
Sponsors received and given, by name, with GitHub's own monthly estimate for your own
account. The counts join the Social row, so a new sponsor arriving pulses like a new
star does.
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 cannot be read, --public-only drops the section entirely rather
than guessing which names are safe to draw.
Your real GitHub inbox
Mentions, review requests, assignments, subscriptions and CI activity — unread,
newest first. enter opens the thread on GitHub, c copies its link, / filters.
s cuts through the noise, and that key exists because of a measurement rather than 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 — 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, the search, or
--public-only is hiding, and which of the three is doing it.
Read-only, like everything else here. Marking a thread read is a PATCH, so
enter takes you to the thread and GitHub marks it read. There is deliberately no
--allow-mutations flag: that would make this a different product with a different
trust model, which is not a decision to take inside a release cycle.
It needs a classic personal access token with notifications or repo — GitHub
documents that endpoint as supporting "only … a personal access token (classic)", so
a fine-grained token cannot read it whatever permissions you grant. That is GitHub's
constraint rather than octoscope's, and the tab says which kind of token is needed
instead of reporting a permission that does not exist.
Notable polish
- GitHub does not return the inbox in time order. Measured: 12m, 12m, 13m, 14m,
14m, 13m, 19m, 23m, 15m, 28m old, interleaved. A column headed "When" that walks
backwards reads as a bug, so octoscope sorts it. - Pull-request rows in the feed carry a title when one can be found. GitHub's
events payload truncates the pull-request object and sends no title at all; where
the same page holds a comment on that pull request, the title comes from there. Where
it does not, the row shows the bare number rather than inventing a label. - The eighth tab shifted the keymap:
1-8now, with Gists at6, Inbox at7and
What's new at8. spaceopens the action menu on the Inbox, like every other list tab.- The Activity heatmap and the feed each scroll on their own terms, and the footer only
offers the scroll hint on the half that scrolls. - Every landing and guide still was regenerated at 0.29.0 — two new tabs change the tab
row, which is in every capture. Gists and Inbox now have a screenshot in the guide;
they were the only content tabs without one. go1.25.13, closing five stdlib advisories (GO-2026-6218, -6090, -6088, -5972,
-5026).
Tests
Every guard added this cycle was mutation-tested — broken deliberately, required
to fail, restored. That turned up ten places where a guard could be removed with the
suite still green: eight assertions that could not fail, and two guards that were
provably redundant with code beside them and were deleted rather than dressed up.
One of the eight pinned a lie rather than a gap. Screenshot mode asserted an account
had no sponsors while hiding the one it had — an omission turned into a false
statement — and the test requiring that sentence was green. A reviewer found it, not
the mutation pass, which is worth remembering when mutation testing starts to feel
sufficient: it proves an assertion bites, not that it asserts the right thing.
Two measurements worth keeping out of it: sort.Slice never reorders an all-equal or
already-sorted input at any size up to 200, so a stability test built either of those
ways cannot fail whichever sort it uses; and X-Accepted-GitHub-Permissions is absent
from endpoints that do accept fine-grained tokens, so it does not discriminate.
Upgrade
brew upgrade gfazioli/tap/octoscope
v0.28.0
A workflow file tells you what it does. 0.27.0's capability axis read those files
and reported what it found there, which is the right thing for a parser to do and
not enough for the question the axis actually asks: what can somebody who
shouldn't be here reach?
One file is often not where that answer lives. It lives in the file next to it,
or in a repository setting the file never mentions.
0.28.0 is the axis learning not to take a workflow at its word.
Chains are followed
Two files, each innocent on its own:
# caller — anyone can trigger it, and it holds nothing
on: pull_request_target
jobs:
call:
uses: ./.github/workflows/reusable.yml
secrets: inherit# callee — reads a secret, but on its own `workflow_call` is not untrusted input
on: workflow_call
jobs:
build:
steps:
- run: deploy --token ${{ secrets.DEPLOY_TOKEN }}Read one at a time, neither scores: the caller has a fork trigger and nothing
worth stealing, the callee has a secret and nothing that can reach it. Read
together they are a stranger's pull request opening a path to your deploy token.
The scan now reads them together, and the finding names the workflow an outsider
arrives through — so you are never told a workflow_call file is fork-triggered
without being shown how.
The direction matters, and getting it backwards is how a feature like this turns
into noise. Reachability accumulates: whatever can start the caller reaches
everything it calls, however deep. Power does not. GitHub only ever lets a
called workflow reduce what it was handed, so a callee is credited with what
its caller granted and never with what it merely declares. A workflow asking for
contents: write whose caller passes nothing holds nothing, and saying otherwise
would flag a shape that is entirely safe.
Three things the report used to claim and could not support are gone with it:
- A workflow nobody in the repository calls now says so, instead of reading
its own silence about permissions as "runs with the repository default" — a
decision its caller actually makes. - A callee is "reachable only through the workflows that call it", not
"reachable only from its own triggers", which a file with no triggers of its
own cannot be. - A call the scan cannot resolve — another repository, or this one written as
a full name and a ref — is reported as an unfollowed chain. Cross-repository
resolution is out of scope; staying quiet about it would have made the obscure
spelling the one that slips past.
The permission never written down
A workflow that declares no permissions: block does not run with none. It runs
with your repository's default, which an owner or organisation can widen to
read/write — so the file can hold write access it never names, and no amount of
reading it will tell you.
octoscope now asks GitHub for that setting and joins it to the file, treating an
inherited write exactly like a declared one: the power is the same, only the
spelling differs. Note that declaring anything overrides the default, even
permissions: {}, which grants nothing at all — a distinction the axis now makes
because it is the difference between a workflow that holds everything and one
that holds nothing.
Where the setting cannot be read — it needs a scope a minimal token will not have
— the report stops claiming the workflow holds nothing, because that
unreadable setting is precisely what would decide it. And it says so only where
some workflow actually depends on it: naming an unreadable setting on a
repository whose workflows all declare their own permissions is noise about a
fact that changes nothing.
Secrets by structure, and an event that was missing
Secret detection used to match text. "secrets": inherit, 'secrets': inherit
and secrets: inherit are the same thing to GitHub Actions and three
different strings to a substring check, so the axis could report "holds no
secrets or write scopes" about a workflow handing over every secret it has —
a false statement rather than silence. It now reads the parsed document, which
is spelling-agnostic by construction, and which also means a commented-out
reference no longer counts: a disabled line reaches nothing.
issues joins the outsider-triggerable events. Anyone can open one, its title
and body are theirs, and the run gets your token and your secrets.
issue_comment was already there on exactly that reasoning, and opening an issue
cannot be less untrusted than commenting on one.
The evidence is ranked
The scored findings in a scan report used to come out in the order the engine
evaluated them, which put a +1 above a +4. What drove the verdict is now the
first thing you read, rather than something to work out by comparing every
number.
Notable polish
- The report line for a workflow with several untrusted triggers explains each
one. It used to list them all and explain only the first, which meant the
more dangerous of two could be named with no reason attached. - A push burst can no longer be corroborated by the capability axis alone.
Capability describes configuration, not something that happened; the axis
carries a ceiling for that reason, and letting a timing signal add to it
handed that ceiling straight back. The gate was documented in three places
and implemented in none of them. - The design document under
docs/design/now matches the code on every point
the two had drifted on.
Tests
Roughly forty new cases, table-driven over the pure scoring engine, plus the
chain composition tested on real workflow sources rather than hand-built facts —
so the parser and the composition are exercised against each other and cannot
drift apart.
Every new guard was verified by breaking it. Composition disabled, the axis
ceiling disabled, reachability propagation off, the passed-secrets check off,
each fails the test that names it. That discipline found four assertions that
did not test what they claimed, one of them written the same hour and one in a
change small enough that skipping the check would have felt reasonable — a
stability test that passed identically under an unstable sort, because Go's sort
is deterministic at small sizes and the two only diverge at thirteen findings.
The ceiling matters more now than it did, since one attack path can produce a
finding per file in a chain. Measured with the clamp removed, a three-file chain
reaches likely compromised on capability alone — against suspicious for the
single-workflow shape. The clamp bounds the arithmetic and not the disclosure:
every file in the chain still appears in the report.
Verified against a repository doing this in production, not only against
fixtures: one that declares permissions: {} at the top level, grants
statuses: write and pull-requests: write on the calling job, and passes a
secret by name. The scan attributes exactly those two grants to the callee and
none of the callee's own — and the verdict stays clean, because the caller
triggers on pull_request, which is deliberately not treated as untrusted input.
brew upgrade gfazioli/tap/octoscope
v0.27.0
The integrity scan stops describing, and starts noticing
Until now the supply-chain scan could only tell you about a repository in the
present tense: what auto-executes right now, whether a commit tip looks forged.
That catches an implant you happen to scan while it is sitting there, and it is
useful — but it cannot answer the two questions you actually have when a worm is
circulating. Did something change? And what could whoever got in reach?
0.27.0 answers both, and wires in a third signal that has been sitting unused
since v0.20.0.
What changed since the last scan
Every scan now records a fingerprint of the repository's auto-execution surface
— the content hash of each scoring ignition path, per branch, plus whether each
branch tip carried a genuine author signature. The next scan diffs against it
and reports what moved:
- a file that auto-executes appeared on a branch that already existed
- an existing one's contents changed
- a branch tip that used to be signed no longer is
This is the most durable signal the scan has, because it cares about neither
the filename nor the contents. A variant that renames its dropper and
obfuscates it differently defeats every content check — but it still has to
appear, and appearing is what gets caught.
Three deliberate choices around it:
- The first scan of a repository says so. No baseline means no comparison,
and silence there would be indistinguishable from "nothing changed" — the one
reading this must never support. - A baseline older than 30 days still lists what changed, but stops affecting
the verdict. Months of legitimate drift scored as suspicion would train you
to ignore the whole axis. - Files that change constantly are not tracked at all —
package.json,
editor task files. Diffing them would bury real signal under your own
commits.
This is the one file octoscope writes on its own: scan-baselines.json, beside
your config. Deleting it is harmless; the next scan starts a fresh baseline.
What a compromise could reach
The fourth axis from the design, and the one with the most room to get wrong:
your capability footprint. Workflow permissions and triggers, self-hosted
runners, write-access deploy keys, webhooks delivering off GitHub.
Holding power is not a finding. octoscope's own release workflow asks for
contents: write and reads two secrets, and it is entirely correct — it fires
on a tag push, so only someone who can already push tags can reach it. Scoring
power by itself would flag a large share of GitHub and teach everyone to ignore
the axis.
What scores is power reachable from untrusted input: a pull_request_target
workflow holding the repository's secrets, or a self-hosted runner that an
outsider's pull request can actually target. Both halves of that sentence
matter — a fork-triggered job running on ubuntu-latest beside a runner used
only by your release workflow is two unrelated facts, not an escalation.
The checks that need admin scope fail open: a token without them still gets
a complete scan of everything else. But failing open is not the same as staying
quiet, so the report names what it could not check — "deploy keys (the token
lacks the scope this needs)" — and qualifies a clean verdict accordingly.
A cross-repo push burst finally counts as evidence
The timing signal that spots a worm fanning out — many repositories pushed
within seconds of each other, the way the reference attack hit five in
49 seconds — has been built and tested since v0.20.0 and connected to nothing.
It shipped once as an always-on banner and was pulled, because timing alone
cannot tell a fan-out from a maintainer scripting an update across their repos,
and because without a recency gate an old batch re-alarmed forever.
Folding it into the on-demand scan answers both objections. It is gated to the
last hour, and it only ever adds weight to a repository that already scored
on another axis. A push burst by itself is a Tuesday; a push burst alongside a
fresh ignition point is something to look at.
One axis is never enough
Worth stating because it constrains everything above: no single axis can reach a
high verdict tier alone. The capability axis carries an explicit ceiling below
the suspicious threshold, so its worst possible shape lands on watch and
needs a second axis to agree before the verdict escalates. Findings past that
ceiling are still reported in full — the arithmetic is bounded, not the
disclosure.
Notable polish
- The report gained a Context section for evidence that is recorded but does
not score. Without it, every weight-zero finding was invisible — including the
first-scan notice this release depends on you seeing. - The guide's version badge now reads the published release rather than a
hardcoded string, so it can no longer go stale between tags. - The README links the guide, which it had never done since the guide shipped.
Tests
Around forty new cases. The delta matrix and the capability scoring are
table-driven over the pure scoring engine; the three elevated-scope probes are
exercised hermetically against a local server, including every refusal path and
a cancelled context, and run under the race detector — the first tests to
schedule those goroutines against each other at all.
Two of them exist because they failed first. The capability weighting reached
suspicious on a single axis before the ceiling was added, and a review caught
it; deduplicating workflows by path let a safe copy on the default branch mask a
dangerous variant at the same path on a side branch, which is precisely the
divergence this scan exists to catch.
Upgrade
brew upgrade gfazioli/tap/octoscope
v0.26.0
Improvements & polish — a batch of long-standing backlog items
0.26.0 is a housekeeping release: eleven backlog items shipped across nine
PRs, spanning the drill-ins, themes, the settings panel, and the honesty of
octoscope's error and empty states. No new tab, no new fetch path — just the
existing surfaces made more complete and more truthful.
See the whole Checks list
In a repository or PR drill-in, the Checks section used to stop at the first
eight rows. Press c now to expand it in place — every check on the
default branch's tip — and c again to collapse it back. On a busy repo that
runs dozens of checks in a matrix, you can finally see all of them without
leaving octoscope.
Preview themes without guessing
octoscope --theme list prints all seven built-in palettes, each with a
colour swatch, and exits. Pick a theme in the terminal you'll actually run in
instead of relaunching to try each one. Under NO_COLOR / --no-color it
prints the names alone, so the preview itself honours the no-colour contract.
Accent colour & sponsor splash in Settings
The in-app settings panel (,) now edits accent_color and the
sponsor-splash toggle — the last two config keys that were file-only. Every
documented setting is now editable live, no reaching for config.toml.
Honest coverage & clearer empty states
- A partial integrity scan now says so in the report, instead of a limited
run reading as a clean all-clear. - The footer advertises only the keys that actually work in the current
context — no more tab-switch / public / settings / help hints showing through
a modal overlay or while you're typing into a filter. - A filter that hides every PR or issue now tells you how to clear it
(esc), instead of leaving you staring at an empty list.
Notable polish
- Render hygiene: constant inline styles hoisted into
styles.go, and the
Repos section-divider rule is built once per paint rather than once per row. - Settings text-field input is sanitized at the boundary, matching the same
discipline the list filters already use. - Message types extracted out of
model.gofor a leaner root model.
Tests
- Hermetic coverage for
FetchStatsand the per-item detail fetches,
driven through a fake transport — no network in the unit suite. - Core TUI interactions asserted at the model level, so keystroke wiring is
covered without a live terminal.
Upgrade
brew upgrade gfazioli/tap/octoscope
v0.25.0
A red CI dot now tells you what broke.
The Repos tab has shown a CI rollup dot per repository since v0.13.0, and it
stopped there: red meant something was wrong, never what. Open a repo now and
the drill-in answers the follow-up question.
Checks in the repo drill-in
A Checks section sits under the repo detail, built from the status-check rollup
on the default branch's tip: the aggregate state next to the heading, then one
row per check run or status context.
Checks failing
✗ govulncheck
✗ build (ubuntu-latest)
✓ lint
✓ test
⏳ deploy
+3 more
Failures float to the top. That is not a cosmetic choice — the visible list is
capped, so a red job in a forty-job matrix would otherwise land in the "+N more"
tail, hiding the one row you drilled in to find. Within a group the API's own
order survives, so a workflow's jobs stay together.
Each check name is an OSC 8 terminal hyperlink straight to its run on GitHub —
click it where your terminal supports that, and it stays ordinary text
everywhere else. The PR drill-in's existing checks section shares the renderer,
so it gained the same links.
Only GitHub targets become clickable
Wrapping a GitHub-sourced URL in a terminal escape needed a gate, so this
release adds one. A URL is linked only when it is an absolute https URL on a
github.com host. Everything else renders as plain text:
- a third-party CI provider reporting through the Checks API points at its own
dashboard, and a GitHub-looking row should not be a one-click trip
off-platform - a URL carrying
;, ESC or BEL is refused outright — those bytes can terminate
the escape sequence early and let the rest of a line be read as terminal
commands
Notable polish
- The rollup's own context count travels with the payload, so the "+N more"
line reports what is actually hidden rather than what happened to be fetched.
On a repository running 41 checks the old arithmetic would have claimed 12. - Union members from the rollup are identified by
__typename, the discriminator
the rest of the fetch layer already used. A check whose name comes back empty
is now kept and labelled instead of silently vanishing, and a type GitHub adds
later is skipped deliberately. - Check outcomes are told apart by glyph (
✓ ✗ · ⏳) rather than by colour, so
the section reads correctly under the monochrome, phosphor and amber themes. - A malformed reporting URL can no longer fail the whole drill-in fetch: URLs
arrive as plain strings and are sanitized at the boundary, so a bad one costs
a single row instead of the entire view.
Tests
Twenty-two new cases: the URL gate (scheme, host, lookalike domains, escape-
breaking bytes), the failure-first ordering, the overflow arithmetic in both
directions, glyph distinctness under a monochromatic theme, the fetch payload
including a nameless check and an unknown union member, and the section wiring
on both the repo and PR paths. Verified against the live API before shipping:
41 of 41 checks on charmbracelet/bubbletea, where the pre-release fetch cap was
losing 21 of them.
Upgrade
brew upgrade gfazioli/tap/octoscopev0.24.2
octoscope v0.24.2 — a context-aware footer
A small polish release: the bottom footer bar now tells the truth about
which keys work where.
Context-aware footer hotkeys
The footer used to advertise the same list-level hotkeys —
1-6/tab switch, p public, , settings, ? help — at every depth.
But once you drill into a repo, PR or issue detail (or the integrity
scan), that view captures every keystroke and returns before those
global hotkeys ever fire, so four of the advertised keys quietly did
nothing.
Now, while a drill-in is open, the footer collapses to exactly what
works at that depth:
esc back · r refresh · q quit
The drill-in's own actions (o open in GitHub, v cycle the star
history) already live in its title bar, so nothing is lost — the
footer just stops pointing at keys that don't respond. Back out with
esc and the full list-level set returns. Same "never advertise a key
that does nothing" principle the drill-in titles already followed.
Tests
A new table-driven TestFooterBarHotkeys locks the contract in both
directions: the list context still advertises the full hotkey set, and
the drill-in context drops tab-switch / public / settings / help while
keeping the keys that actually fire.
Upgrade
brew upgrade gfazioli/tap/octoscope
v0.24.1
octoscope v0.24.1 — a reliability & security patch
A small, safe patch: the repo drill-in no longer breaks when GitHub's stargazers endpoint misbehaves, and the binary picks up a standard-library security fix. No config changes, no new flags — just upgrade.
Repo drill-in survives a flaky star history
The 12-month star-history sparkline reads GitHub's stargazers connection — the same endpoint GitHub has been tightening (it now needs a scoped token to page) and one prone to its own transient 5xx. Until now a failure there aborted the entire repo detail view.
It's now best-effort: a failed or restricted star-history fetch simply drops the sparkline, and the rest of the drill-in — description, license, latest release, recent commits, open issues/PRs preview and topics — still loads. The star count was never affected; it's a public scalar, not the restricted list.
Standard-library security fix
The binary is now built against Go 1.25.12, picking up the crypto/tls fix for GO-2026-5856.
Notable polish
- Removed the broken "Star History" chart badge from the README — star-history.com now requires a scoped token, so the image no longer rendered.
- Landing: the "At a glance" section is now a single hover-pausing marquee instead of an ever-growing grid.
Tests
- Two new hermetic tests pin the drill-in contract: a failing star-history walk is non-fatal, while a failing detail query still aborts.
Upgrade
brew upgrade gfazioli/tap/octoscope
v0.24.0
octoscope outside the TUI — pipe it into your scripts.
v0.24.0 opens octoscope to a new audience: scripts, cron jobs and shell
status-lines. Run it once, get your dashboard as text or JSON, and exit —
no terminal UI, no alt-screen.
Non-interactive output — --plain and --json
Two new flags fetch the dashboard a single time and print it:
octoscope --plain— a human-readable summary: profile counters
plus the key lists (repositories, open PRs, issues, review requests,
watched repos), colourless and friendly to a shell status-line or a
quick glance.octoscope --json— the same data as JSON, ready to pipe into
jq, a cron job or a status-line generator.
Both honour --public-only and the usual auth cascade
($GITHUB_TOKEN → gh auth token), and are mutually exclusive.
octoscope --json | jq '.social.total_stars'
octoscope --json --public-only > snapshot.json
octoscope torvalds --plain
A stable JSON contract
The --json output is a versioned wire contract, not an incidental
dump: it's defined in one place, decoupled from octoscope's internals, so
your scripts keep working as the tool evolves.
- A top-level
schema_version(currently1) is bumped only on a
breaking change — new fields are additive. - Every list is always an array, never
null, so you can iterate
unconditionally. - The full schema is documented in the README's new "Scripting" section.
Notable
- Security: bumped
goldmarkto v1.7.17, closing GO-2026-5320 (a
fresh advisory reachable through the markdown renderer). The
govulncheckCI gate is green again.
Tests
New hermetic coverage for the internal/report package: the
Stats-to-DTO mapping, language-percent computation, the public-only
metadata flag, JSON well-formedness and stable top-level keys, empty
lists marshalling as [] (never null), and the --plain list cap.
Upgrade
brew upgrade gfazioli/tap/octoscope