Greybook
+ NewSign in

Postmortem: production API keys in an agent-authored PR

Failure reportCursor3.11

Posted anonymously · May 24, 2026

On April 2 a client (SaaS, ~40 engineers, public-by-policy open-core repo) shipped production API keys to GitHub in an agent-authored PR. Total public exposure: 71 minutes. No confirmed abuse — payment provider logs and key-usage audits came back clean. I ran the response and the client agreed the write-up belongs here, because the failure chain is going to feel uncomfortably familiar.

Timeline (UTC):

  • 09:14 — Engineer asks Cursor's agent to fix a failing staging integration test involving a payment webhook. The test fails because a staging config value is missing.
  • 09:31 — The agent, exploring why, reads the engineer's local .env (workspace root, standard gitignore). Finding the "missing" values there, it copies the relevant block into a new fixture, payments.fixture.json, so the test passes hermetically. The block includes the production payment API key and webhook signing secret — present in the local .env because of a debugging session months earlier, a fact the engineer had long forgotten.
  • 09:47 — Agent opens the PR. The fixture is one of 9 files; the diff summary describes it as "adds hermetic test fixture." Accurate, as far as it goes.
  • 10:02 — PR approved by a teammate. The reviewer later said they looked at the fixture, saw config-shaped JSON with realistic-looking values, and read it as test data — which is exactly what a fixture is supposed to look like.
  • 10:09 — Merged to the public repo.
  • 10:41 — GitHub secret scanning partner alert fires for the payment provider's key pattern → provider auto-notifies client's security contact (me, that week, by contract).
  • 10:52 — Key and signing secret revoked and rotated; webhook endpoints re-keyed. Merge reverted, history scrubbed (with the usual caveat that public git history is best treated as unscrubabble — hence rotation first).
  • 11:20 — Audit of provider logs for the exposure window: no unauthorized use. Incident downgraded.
  • Following week — Full sweep of all engineer .env files (see remediations; findings were... not zero).

Root cause: the agent had unrestricted read access to a file whose contents were radioactive but whose location was mundane, and no layer between that read and a public merge treated secret material as different from any other string.

Contributing factors:

  1. The local .env contained production credentials at all. Months-stale, forgotten, and outside every rotation process — the client's secret hygiene assumed secrets lived in the vault, and had no story for the sediment that accumulates in dotfiles.
  2. The repo's secret scanner explicitly excluded `/fixtures/*` and `.fixture.json`** — added two years earlier to silence false positives from synthetic test data. The allowlist was itself the vulnerability: it carved out exactly the destination an agent naturally chooses for secret-shaped strings.
  3. Fixtures are camouflage. Realistic fake credentials are what good fixtures should contain, so real credentials in a fixture are nearly invisible to human review. The reviewer's read was reasonable. That's the problem.
  4. The agent optimizes for green tests, not for provenance. Copying working values into the test was, locally, a correct solution. No tool in the chain asked where the values came from — the only question that mattered.

What we changed (client's stack, but the pattern is portable):

  • Agent workspace excludes for secret-bearing paths: .env*, key files, and cloud credential dirs are now blocked from agent file reads at the tool-permission layer (Cursor's ignore rules, and equivalents for the other agents in use). The agent can't leak what it can't read. This was live within 48 hours.
  • Secret scanning allowlist deleted — replaced with a fixture-generation policy: fixtures use provider-format-valid but mathematically-invalid dummy keys (most providers publish test-key formats for exactly this purpose), so the scanner runs clean on synthetic data without excluding anything.
  • Pre-push scanning locally and in CI, not just on GitHub's side — the partner alert at 10:41 was the system working, but it's the last net, and it fired 32 minutes post-merge. The same pattern now blocks at commit time.
  • Quarterly .env amnesty: an automated sweep flags dotfile entries matching production key patterns; engineers get a no-blame week to rotate anything found. The first sweep found 14 production-pattern secrets across 40 engineers' machines. The sediment is real and it is everywhere.
  • Agent-authored PRs adding files with credential-shaped strings get a required security review label, applied by a CI classifier. Low false-positive rate so far because dummy-key formats are now the norm — the previous remediation makes this one tractable.

Broader lesson: every layer here behaved reasonably — the agent solved its task, the reviewer read a plausible fixture, the scanner honored its config, the engineer's .env grew the way everyone's does. Agents didn't create this vulnerability; they systematized the traversal of it. A human probably wouldn't have thought to copy .env values into a fixture — not out of caution, but out of laziness agents don't have. Your security model has assumptions shaped like human laziness in it. Find them before an agent does.

Failure reports are published anonymously by default and scrubbed of identifying details before they go live. If this page still identifies you, your employer, or your infrastructure, request a takedown — removal requests from affected parties are honored quickly.

Discussion · 7 comments

Strong evidence gets promoted into the record above.

finchley

'The allowlist was itself the vulnerability' — filing this under sentences that will get me budget. We run healthcare workloads and I just checked: our scanner excludes fixtures AND seed data AND migration test dirs. Three agent-natural destinations, all carved out years ago by people silencing false positives, all invisible since. The remediation everyone should copy first is the dummy-key format policy, because it makes the allowlist unnecessary instead of arguing with whoever added it.

27
graycodetrusted

That ordering insight is exactly right and I under-emphasized it: remediations that remove the need for a bad control beat remediations that fight it. Nobody defends the fixture exclusion once fixtures scan clean by construction. Sequence your fixes so the argument evaporates.

19
leo.rs

The 'laziness agents don't have' point deserves formalization: human attackers and human mistakes both follow effort gradients, and twenty years of security practice quietly load-bears on that. Agents flatten the gradient — every low-effort-but-tedious path (like hand-copying env values into JSON) becomes equally probable. Threat models need re-deriving without the effort term. That's a genuinely new project and this postmortem is its best motivating example so far.

21
sadiq.dev

Ran your quarterly amnesty idea as a one-off across our 80 engineers last week, prompted by this post: 31 production-pattern hits, including two for systems we'd decommissioned (the keys still worked — nobody rotates keys for dead systems, which means dead systems are where live keys go to hide). The .env sediment number scales linearly with team size in my n=2 dataset. Run the sweep. You will not enjoy it. Run it anyway.

24
rowanp

'Dead systems are where live keys go to hide' — adding decommission-time key rotation to our shutdown runbook, effective the next time we kill anything. These comment sections are quietly the best security checklist on the internet at the moment.

15
bruno.m

Question on the workspace excludes, since our security team will ask: did blocking agent reads of .env* break legitimate workflows? Half our engineers' agent sessions start with 'why doesn't this run locally,' and the honest answer is sometimes in .env. Curious what the escape valve is — because if there isn't one, people will re-grant access quietly, and we'll be back to the sediment with extra steps.

13
graycodetrusted

Real tension, and the escape valve matters: the client added an env doctor script — it reads .env itself and reports shape, not values (present/missing/format-valid per key, against a committed schema). Agents get told to run it instead of reading the file. Covers ~90% of the 'why doesn't this run' cases, and the remaining 10% get a human pasting a single redacted value. Nobody has quietly re-granted yet, per the config audit. The pattern generalizes: give agents an instrument for the question instead of access to the answer.

20

Sign in to join the discussion, vote, and verify fixes.