Find every copy of a key, and rotate them all
The Secrets page in the desktop app lists each key your tracked repositories and platforms use once, with every place a copy lives. This page shows how to fill it, rotate a key in every place, revoke the old value, and leave a key to rotate on its own. The values stay in an encrypted vault on your machine and never reach repoops.ai.
Watch it
Transcript
The walkthrough is silent. These are its captions, in order. Every value it shows is illustrative.
- 0:00One row per key, with every place a copy lives
- 0:06Scan now reads .env files, Vercel, GitHub and Railway
- 0:12Rotate writes the new value everywhere, then checks each place
- 0:18The old key is revoked only after every place reads back the new one
- 0:24Values stay in a vault on this machine. The page shows names and fingerprints.
The key names, places and fingerprints in this video are illustrative.
How to
The RepoOps desktop app with at least one tracked repository. The vault that holds the values is locked by a key in this machine's keychain (DPAPI on Windows, the Keychain on macOS, Secret Service on Linux). When the keychain is missing or locked the vault stays closed and nothing is stored in plain text instead.
To read Vercel, GitHub Actions or Railway, an admin key for that platform. Without one the scan reads your .env files and skips the platform, with the reason.
- Open Secrets.
Open AI Security, pick the Secrets tab and press Open the inventory on the Key inventory card. The page lists each key once, under the heading Every key in the fleet, and where it lives.
- Connect the admin keys.
On the Keys the button uses tab, press Connect beside a platform and paste its admin key. RepoOps tests the key and keeps it in this machine's keychain only; it never goes to Vercel, Railway or repoops.ai. Vercel, GitHub and Railway keys let the scan read those platforms. The Vercel and Railway keys, and the OpenRouter, OpenAI, Resend and Azure keys listed on the same tab, let a rotation make new keys at those providers. Check tests a connected key again, and Remove deletes it from the keychain.
- Run the scan.
Press Scan now. It reads the .env files in every tracked repository and in the RepoOps data folder, the agent environment rows of the non-human identity inventory, Vercel project variables, GitHub Actions secret names and Railway service and shared variables. Each value gets a keyed fingerprint and is sealed in the vault. The page answers with a line such as Scanned: 14 keys in 31 places, 3 new. GitHub never hands back a secret's value, so a GitHub copy is matched by its name. A Railway variable that points at another one, such as
${{ Postgres.PGPASSWORD }}, holds no copy of its own: the key's page lists its service under Services that read it by reference, and a rotation redeploys that service once, beside the ones it writes. A service that is not deployed is listed and never started.The scan also reads the .env files in every git worktree of a tracked repository, and in a sibling folder named after it only when that folder is one of its worktrees. A worktree copy holding an older value than the main checkout's copy of the same file shows on the current key as a copy that differs. A URL that works as a password counts as a key: an ntfy topic with a name nobody would guess, a Slack, Discord or Teams incoming webhook, a Sentry DSN with its secret part, and any address whose query carries a token, key, secret, signature or password. A plain web address stays a setting.
Scan history and logs, beside it, looks for every stored key in each repository's git history, shipped client bundles, GitHub Actions logs and agent transcripts, then runs the risk pass. It works for a minute and each source keeps its place, so press it again to carry on; the daily run does the same. A key found there reads Exposed, and its Exposures tab names where: a commit, file and line, a bundle file, a CI run and line, or a session. A risk with no leak, such as one value used in production and in development, shows under the key's status in the list and in a Risks card on its page.
- Read the inventory.
The tiles count the places keys live, the exposed keys, the keys overdue or due under their own policy, and the copies that differ. Where copies live has one card per platform. The Keys table opens on Needs action; All, Automatic and You paste narrow it by how a key rotates, and Repo narrows it to one repository. No row shows a value, only a name and a short fingerprint.
- Open one key.
Press a key to open its page. Places lists each copy with its platform, where it is, the variable name and a match: Matches, Differs, missed a rotation, Cannot be read back, matched by name, or No longer found here. A copy found only by its name reads Suggested by name, not confirmed; press Confirm before a rotation writes there. Verify makes one call to the provider with the value the vault holds and reads that place back. Add a place records a copy the scan cannot see; pick Another tool (by hand) for a tool such as Clay, Zapier or n8n. Versions lists each value the key has held by fingerprint, Exposures lists where it showed up, and History lists every rotation, write, check, revoke and exposure, newest first.
A copy in another worktree reads Worktree copy. A copy on a Railway service that has not deployed in three months, or never has, or on a Vercel project with no production deployment in that time, reads Idle host with its last deploy date, and the key carries the same risk; REPOOPS_SECRETS_IDLE_MONTHS changes the three months. Remove deletes one copy: the Railway or Vercel variable, or its line in a .env file. Type the variable's name to confirm. The last copy of a key that lives nowhere else needs I am retiring this key ticked. The next scan confirms the copy is gone. On repoops.ai the same button reads Request removal: type the name, and once an owner or admin approves, the desktop that holds the key removes a .env copy, a copy that differs or a copy on an idle host on its next sync. Any other copy, and a key's last copy, is removed on that desktop.
- Rotate the key.
Press Rotate now on the key, or Rotate what needs it on the Secrets page for every key that needs action. The run page opens with five steps for each key: Map, Mint, Write, Verify and Revoke. RepoOps makes a generated value itself, and asks a provider with a key API for a new key with the old key's scope. A key you paste waits under Needs you: open the provider's key page from there, paste the new value and press Check and continue. A copy RepoOps cannot write, in another tool or hard-coded in a file, is a tick under Needs you. The rest runs on its own.
- Revoke the old value.
Once every place holds the new value, press Revoke the old key. When something needs reading first, such as a redeploy that did not finish or a match by name nobody confirmed, the run lists it under Before you revoke the old key and waits for Revoke anyway. A service that reads the key by reference and whose redeploy failed is different: Revoke refuses until RepoOps reads a newer healthy deployment of it, so redeploy it on Railway and press Revoke again. A provider RepoOps cannot revoke at sends you to its key page, and you tick I revoked the old key at the provider. Until then, Pause run stops the run before its next step (the same button then reads Resume run), and Undo: write the old values back puts every old value back in place. Undo works only while no old value has been revoked.
- Set when it rotates.
On the Policy tab, pick When it is due (Every 30, 60 or 90 days, Before it expires, or Manual only), how many days ahead to warn (0 to 365, 7 by default) and Where the alert goes: Today only, or Today and a Slack or Teams channel set up on this machine. Rotate on its own when due, Auto-rotate if exposed and Rotation time on this machine are the unattended switches. Press Save policy.
Use cases
A platform reports a breach
- Situation
- Railway says customer environment variables may have been read. You hold a dozen keys, and some of them sit on Railway as well as in .env files and on Vercel.
- You do
- On the Secrets page, press Mark exposed on the Railway card under Where copies live. Say why, type railway to confirm, and press the button that marks them exposed and rotates them.
- You see
- Every key with a copy on Railway reads Exposed and goes into one rotation run. Keys RepoOps can make start at once and are written to every place, Railway, Vercel and the .env files alike; their old values are revoked once each place reads back the new one. The keys you paste wait under Needs you with a link to each provider's key page. The breach report on the Incidents tab lists every one of those keys and which are still open.
A key turns up in a CI log
- Situation
- A build printed an API key into a GitHub Actions log. Nobody noticed, and the key also lives in two repositories and a Vercel project.
- You do
- Leave the daily exposure scan to find it, or open the key when its case lands on Today. Read the Exposures tab, then press Rotate now.
- You see
- The scan matched the key by its fingerprint, marked it Exposed and opened a case, and the Exposures tab says where it was found without quoting the value. The run writes the new key to the two repositories and the Vercel project, checks each, and revokes the leaked one. With Auto-rotate if exposed on, the run starts by itself and you read about it after.
Expected outcome
Next step: Press Scan now. To read Vercel, GitHub and Railway as well, connect their admin keys on Keys the button uses first.
Next step: That platform has no admin key, or the key could not read it. Connect or Check the key on Keys the button uses and scan again.
Next step: Reload. If the reason names the keychain, unlock or repair this machine's keychain: RepoOps never makes a new vault key over an old one, because every stored value would be lost.
Next step: That copy holds an older value. Press Rotate now to write the current value there, or Verify to read it again.
Next step: Press Confirm if it is this key. Until then, no rotation writes to it.
Next step: An unattended run revokes only when every place read back the new value and no step needs a person. Open the run, do what Needs you lists, then press Revoke the old key.
Next step: Its provider refused the key at the daily check. Open the key's page: if the key was revoked at the provider on purpose, press Rotate now for a new one; if the provider was down, press Check now tomorrow, or wait for the next daily check.
Next step: Hover it for the reason. A key you paste never rotates unattended; when it is due, RepoOps alerts you instead.
What else to consider
Where the values live, and what leaves
Values are sealed with AES-256-GCM under a data key, which is itself locked by a master key in the operating system's keychain. Fingerprints are a keyed hash, so a copy of the database alone cannot guess a short password from one. If the fingerprint key goes missing from the keychain or is replaced, the vault stays closed and the Secrets page shows a card with Rebuild fingerprints: type recover (and this machine's operator confirmation, when it sets one), and RepoOps makes a new fingerprint key and every stored fingerprint again from the values, so no value is lost. The admin keys stay in the keychain. Rotation writes a new value only to the places listed on the key, and the platforms it calls are the ones that hold a copy or make the key. Nothing in the secrets manager sends a value to repoops.ai.
Run a command with the vault's values
repoops secrets run -- npm run dev starts the command with this repository's .env variables set from the vault, so a plain-text .env file can go. The repository is the tracked one that holds the current folder, or --repo <id>. --env-file <path> takes the variables of one .env file only, and --keep-env keeps a value your shell already set. The command's exit code is passed through.
It never writes a value to disk, puts one on the command line or prints one; it names the variables it set. Only copies the last scan found holding the vault's value are used. Names that steer how a program starts, such as PATH and NODE_OPTIONS, are never set. When two .env files give one variable two values it starts nothing and asks for --env-file. With no values for the repository it says so and asks you to run a scan first.
How each key rotates
Each key reads Automatic or You paste. RepoOps generates the value for secrets it makes itself, such as AUTH_SECRET and CRON_SECRET. OpenRouter, OpenAI, Vercel, Resend, Railway and Azure keys are made by the provider's API, and fall back to a paste whenever the old key's scope cannot be read exactly. A Neon password is reset by API too, but only after you confirm it on the run's page: the reset ends the old password at once, so the card lists every database URL on the same role, one reset covers them all, and a place that fails is written again rather than put back. A password on any other Postgres, such as Railway's, is changed the same way, with one ALTER ROLE in the database after you confirm. Anthropic, GitHub, Stripe and the others are pasted, and so is any provider RepoOps does not know, whose key page you set with Edit.
What rotates on its own
The desktop app checks every 15 minutes. A key with Rotate on its own when due rotates at its rotation time, 03:00 unless you change it, and a key that fell due while the app was closed rotates at the next start. With Auto-rotate if exposed on, an exposure of the key's current value starts a run at once; an exposure of an older value asks you to revoke that version instead. Saving the policy with a switch on approves the places listed at that moment, and a place added later waits until you save again. Mark exposed takes at most 200 keys in one run.
Where exposures come from
Five checks match as they fire: an agent's file write, a repository's working tree, an agent session, a screenshot and an agent's code change. Once a day the background service also reads git history, shipped client code, the CI logs GitHub still keeps and agent transcripts, picking up where it stopped; REPOOPS_DAEMON_SECRETS_SCAN=0 turns that pass off. A match records where (a file and line, or a session), never the value. A match on a value that is already revoked is recorded as information and raises no alert.
Alerts
Every hour RepoOps works out which keys are due, overdue or close to expiry. It reads an expiry from the key itself for a JWT, and asks Vercel and GitHub for theirs. The same read finds when a key was issued (a JWT's own date, or the date Vercel or OpenRouter gives) and whether it never expires, so a key RepoOps scans for the first time shows its real age, not the day of the scan. Each copy on GitHub or Vercel shows when that platform says it last changed, even one that cannot be read back. Three risks read these dates: a key older than its policy allows (90 days with none), a key that never expires, and a key issued, or a copy last changed, before a breach of a platform that holds it. Mark exposed by platform takes the day the provider says the breach happened. Each change of state opens one case on Today and posts once to the Slack or Teams channel the key names; a key back in order closes its case.
Revoke without a run
Revoke on a key's page ends one version at the provider without writing a new one. It first lists the places that stop working the moment you revoke, then asks you to type the key's name. On the current version that leaves those places with a dead key, so use Rotate now when you want a working key in its place.
Does every key still work?
Once a day the desktop app asks each provider it has a check call for (built-in providers, and a custom provider with a check request on its page) whether the key still works: one read-only call with the current value. Under Status, each key shows Works, Refused or Could not tell, with the day it was checked. A key in several parts, or one with no check call, reads Could not tell, because RepoOps checks it by reading its copies back instead.
A key the provider refuses (HTTP 401 or 403) reads Stopped working, unless it is already Exposed, Revoked, Rotating or has copies that differ, and those statuses stay. It opens one case on Today and sends one alert to the key's alert channel; when the key works again, the case closes and the status goes back. A refused key is asked again the next day, not sooner. A version you revoked that the provider still accepts shows Revoked value still works, with its own alert, until it stops working.
Check keys now on the Secrets page, and Check now on a key's page, run the same check at once. When a provider answers that it is being asked too often, RepoOps waits as long as it says before asking it again.
The breach report
Mark exposed on a platform's card also records the breach on the Incidents tab, with the day it happened and the provider's notice if you paste its address. Record a breach on that tab does the same by hand, without marking anything or starting a run.
Each breach opens a report of every key that had a copy on that platform on the breach date. A key reads Rotated and revoked, Rotated, revoke pending, Not rotated, or Cannot rotate here with the reason. A key whose value was revoked and never replaced reads Revoked, not replaced. A value made on the breach day counts as exposed unless it was made after you recorded the breach, since the breach may have begun later that day. Under each key the report shows the version that was exposed and when it was issued, the new version, the run that rotated it, and the proof the old value is dead: the provider refusing it, a delete by API, or the person who revoked it and ticked it, with the date. It also lists each place with whether it was written and read back, each redeploy, and what is still open.
Export it as a printable page (print it to PDF), a CSV, or a JSON file signed with an Ed25519 key kept in this machine's keychain. The Incidents tab shows that key's id: an auditor checks the file against the key you give them, not the one inside the file. No export carries a value or a fingerprint. On repoops.ai a teammate reads the same report and exports the CSV and the printable page; the signed file comes from the desktop.
Who can reach your repositories on GitHub
The GitHub access card on AI Security's Secrets tab reviews each tracked repository and its owner once a day, or when you press Run now. Per repository it lists the collaborators and their permission, outside collaborators by name, pending invitations, deploy keys and how many can write, the hosts webhooks send to, self-hosted runners, the count of Actions secret names, and whether the default branch is protected. Per organization it lists the GitHub Apps installed and whether the organization audit log is available, which needs GitHub Enterprise Cloud. Every call only reads. It keeps names, permissions and dates: a webhook's host, never the rest of its address, and never a secret or variable value.
The first run records what it sees as the baseline. After that, each new collaborator or outside collaborator, raised permission, invitation, deploy key, webhook host, runner, app installation or wider app permission, added or updated Actions secret, and removed branch protection is one finding with a link to the GitHub settings page. Each finding opens a security case and the run posts one alert to your alert channel. Accept marks a change as expected and adds it to the baseline. A change that narrows access, such as a removed collaborator, updates the baseline on its own. A part the key cannot read is listed as a gap with the reason and is left as it was.
When a platform has a recorded breach, the card also lists the Actions secrets not updated since the breach date that share a name with a key placed on that platform.
On the desktop the review uses the GitHub admin key you connected. On repoops.ai it runs through the RepoOps GitHub App, once an owner grants the App's read permissions for administration, webhooks, secrets, variables, environments and self-hosted runners; until then the card names the permission each gap needs. While repoops.ai runs it for your team, a bound desktop shows that result instead of running its own, so a change raises one alert.
Your team's view
A desktop bound to a hosted team can publish a copy of the inventory: names, places, dates and status, never a value or a fingerprint. It is listed as Secrets inventory under What this machine publishes to your team in Settings.
The Secrets page on repoops.ai shows that copy within the hour, and a member can ask an admin to rotate a key from it. Values stay on the desktop, which is where keys are rotated. It also shows whether each key worked at the desktop's last check, and the day of that check.
Keys on the other pages
Today lists exposed and overdue keys under Keys to rotate. A key already in a rotation run shares one row per run, with Open the run; any other row has Rotate it, which opens the key's page.
On AI Security, the Secrets tab starts with the Key inventory card: keys tracked, exposed, due or overdue, and places. In the Secret scanning table, the Matched key column links each sighting to its key. A GitHub alert carries no value to match, and the column says so.
The case for a leaked key ranks three fixes. Rotate this key and remove it from the code starts a rotation run, then opens a fix PR when a sighting is on a line of code in a tracked repository. Remove it from the code only opens that fix PR without rotating. Dismiss as a test key asks why, keeps the reason in the key's history, turns each open sighting into information and resolves the case. A sighting of the key in a new place opens it again. A request that does not come from the page needs the operator confirmation.
Make a fix PR, on the case and on a rotation run's page, checks that the file still holds the key, then opens a case in that repository naming the file and line. The Remediation fix pipeline writes a brief and a build session opens a pull request that reads the key from the environment. The request carries no value; the build session reads the file on your machine to take the key out, so its model sees that file, key included. Rotate first when the key guards anything real.
Settings in the desktop app has a Rotation keys card, with the same Connect and Check as Keys the button uses, and a Vault backup card that opens the Backup and export tab.
Export, backup and the recovery key
Export on the Secrets page saves the inventory as one file, JSON, CSV or text inside, locked with a password you type (12 characters or more). Key values go in only when you ask for them, and then RepoOps also asks for your recovery key. An inventory-only CSV can be saved with no password. Import an export, beside it, opens a JSON export with its password and lists every key and place in it before anything is added; places arrive unconfirmed, and the switches that rotate a key on its own arrive off.
The Backup and export tab sets up the recovery key. RepoOps shows it once and keeps no copy; paste it back to mark your copy checked. Then add a destination: a folder (a USB drive, a second disk or a network share) or S3-compatible storage (Amazon S3, Cloudflare R2 or Backblaze B2), whose access key goes to this machine's keychain. Each copy is sealed on this machine before it leaves, so a destination only ever holds a locked file. While RepoOps runs it backs up after every rotation and daily at 02:00, a time you can change, and keeps the newest 30 copies at each destination. Test restore opens the newest copy with your recovery key in a throwaway vault and checks that every value opens; the live vault is not touched.
Rotate the recovery key and the data key, on the same tab, seals every value again under a new data key and shows a new recovery key. The old one then opens only backups made before the rotation. It asks for the current recovery key and for the word rotate.
Coming Google Drive as a destination waits for the operator to register RepoOps's Google sign-in client, and is greyed out with that reason until then.A second desktop, and restoring on a new machine
Another RepoOps desktop can keep your backups. On that desktop pick Keep another desktop's copies; it shows a one-time code. On this one, Add destination, Another RepoOps desktop, and type the code. Both screens then show the same six digits: accept on both only if they match. Copies go through the relay sealed to the other desktop's key, and it keeps the newest 30. Unpair, from either desktop, tells the other one. Pairing needs a relay both desktops reach, set as REPOOPS_RELAY_URL and REPOOPS_RELAY_TOKEN; without one the choice is greyed out and says so.
Restore a backup, on a new machine or after a reinstall, takes a backup file, the newest copy at a folder you added, or a copy a paired desktop sent, plus the recovery key it was made with. RepoOps shows what the backup holds first and restores only into a data folder with no vault. It then replaces the restored data key straight away and shows this machine's own recovery key once, so whoever made the backup does not hold the key that seals what you add next.