Skip to main content
The Secrets hygiene page (Secrets in the sidebar) inventories your GitHub Actions secrets — both the organization’s and each repository’s own — and shows how long since each was last rotated, flagging any that have gone stale. GitHub never exposes secret values through its API — this reads metadata only (name, visibility, and last-updated timestamp). Pipefort computes the rotation age from the last-updated time and marks a secret stale when it hasn’t been rotated in over 180 days.

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: inherit on a reusable-workflow call
  • ${{ toJSON(secrets) }}
  • secrets[expr] — an index by a computed name
These are attached to every secret in the inventory, not to a row of their own. A page that listed only named references would tell you nothing reads 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.
A repository nobody has scanned reads nothing, as far as this page knows. If any repository has never been scanned, the page says how many, because “nothing reads this secret” drawn from an unscanned repository’s silence is the same mistake as scoring a never-scanned repository at zero risk.

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: acme-corp/payments-service .github/workflows/deploy.yml:26 could take a short-lived token from an AWS IAM role instead. That job needs id-token: write adding first.
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.
The panel also names workflows configured for OIDC in a job that cannot mint a token. Those do not fall back to a stored credential — they fail at run time, and they fail looking like a misconfigured role rather than a missing permission. It is the row most worth acting on, and neither a success nor a credential to retire.
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.

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

Reading Actions secrets needs the GitHub App’s Secrets: read permission, which is not part of the default install. Until it is granted — and each installation re-consents to the added permission — this page shows a prompt to grant it instead of an inventory.
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.
There is no separate endpoint behind it. Every item in the org-wide inventory already carries the repository it lives on and, per usage, the repository whose workflow reads it, so the tab is that response filtered rather than a second source — and the org page and the tab can never disagree. When the repository has never been scanned, the second block says its usage is unknown rather than empty: nothing has read its workflows yet, so “no organisation secret is read here” would be a claim rather than a result.

Scope

This page covers Actions secrets at both levels — the organization’s and each repository’s — and, since the reference index, which workflows read them. Repositories are polled with a cap, so a very large organization may have more repositories than one refresh asks about. The page reports how many were asked rather than implying all of them were. Environment secrets remain out of scope. Everything else on this page comes from one read of each workflow: what it reads, what it could stop reading, and what an open finding in the same file would let someone else take.