What you see
Four figures across the top: how many secrets there are, how many of those sit on a repository rather than the organization, how many are past the threshold, and the age of the oldest. Alongside them, a sentence naming the furthest-reaching stale secret — not simply the oldest, because those are rarely the same secret.
Secrets are ordered oldest-first. Read the reach column alongside that
ordering — age and reach compound.
The inventory is remembered, not fetched
Pipefort records what GitHub reported and reads that back, rather than calling the Actions secrets API on every page view. A daily refresh (GET /api/cron/refresh-secrets) replaces the whole set for each organization.
Rotation ages come from GitHub’s own updated_at and do not drift between
refreshes. What does drift is the set: a secret created this morning is
missing until the next refresh, and one deleted last week would still be listed
if the refresh merged instead of replacing. The page therefore states when the
inventory was taken.
This exists because an empty table has four different meanings, and they used
to be indistinguishable:
A refusal does not erase what was already known. Losing the permission dates
the inventory rather than blanking it — a blank page would read as “this
organization has no secrets”, which is the opposite of what happened.
Which workflows read each secret
Under every row, the page lists the workflow files that read that secret — repository, file, line and job. The references come from the same parse the rules run over, on every scan, so this cannot disagree with the findings about what a file says. It answers the practical question first. Rotating a credential means knowing what breaks, and “no scanned workflow reads this” is the sentence that makes a stale secret safe to delete rather than merely old.Reads that name nothing
Three forms hand over every secret in scope without naming one:secrets: inheriton a reusable-workflow call${{ toJSON(secrets) }}secrets[expr]— an index by a computed name
DEPLOY_KEY about a workflow that is handed all of them — the most dangerous
sentence this feature could produce. The line says where the secrets went.
When a read is flagged
Reading a secret is what secrets are for, so most rows carry no colour. A row is flagged in red only when the file also has an open finding through which someone outside your organization could obtain the value:The bar is deliberately high. A hardcoded credential elsewhere in the same file
is not a route to a different secret, and an obfuscated expression is
evidence rather than a mechanism — neither is flagged. A red mark that fires on
“suspicious” trains people to stop reading it.
When a repository keeps its own copy
GitHub resolves${{ secrets.NPM_TOKEN }} against the repository first and
the organization second. So a repository holding its own NPM_TOKEN never
reads the organization’s — and the two are separate rows here, with separate
rotation ages and separate usage lists.
The page attributes each workflow reference to the secret it actually resolves
to, and says so on the organization’s row:
Overridden in acme-corp/payments-service by a repository secret of the same
name — that repository reads its own, not this one.
Without that line, an organization secret would look less used than it is
dangerous. Its usage list would be short for a reason the reader could not see,
and “barely anything reads this” is how a credential gets deleted from under a
workflow — or, worse, left in place as harmless while the copy that is actually
exposed sits on a repository.
Which secrets could be deleted rather than rotated
Rotation makes the same problem younger. Adopting OIDC removes it. GitHub mints a short-lived token for a job that asks for one (permissions: id-token: write), and every major cloud login action accepts
that token in place of a stored key. So for each secret a cloud login reads,
the page can say the thing that decides what to do about it:
This secret is replaceable:That last sentence is the size of the change — one step, or the workflow’s permissions as well. A Short-lived credentials panel summarises the organisation, counted in logins rather than repositories: one workflow can hold several, and a repository is not done while any of them still reads a key. Recognition is deliberately conservative. AWS, GCP, Azure and Vault are recognised, plus a job requesting the token directly. An action earns a place only when its own inputs distinguish federated from static — one that logs in through inputs Pipefort cannot tell apart would produce rows claiming a credential can be retired without knowing whether it can. A login whose credential comes from somewhere unnameable — a variable, a hardcoded value — is still counted as static even though it attaches to no row above. Dropping it would understate how much is left to do.acme-corp/payments-service.github/workflows/deploy.yml:26could take a short-lived token from an AWS IAM role instead. That job needsid-token: writeadding first.
What the page still cannot tell you
Environment secrets — those attached to a named deployment environment rather than to the repository itself — are listed nowhere here.Requires the “Secrets: read” permission
To enable the inventory, add Secrets → Read-only to the Pipefort GitHub App and approve the updated permission on your installation. Pipefort only ever reads secret metadata; it cannot read secret values, and GitHub does not expose them.On a single repository
The same table appears on each repository’s detail page, under its secrets tab, split into two blocks that answer different questions:- Stored on this repository — secrets that live on the repository itself. These are the ones its owners can rotate directly.
- Organisation secrets this repository reads — org-level secrets that a workflow in this repository references. A stale one here is not this repository’s alone to rotate, but it is this repository’s blast radius, which a repository-only view would otherwise hide.