{"course":{"slug":"building-apps-with-ai","title":"Building Apps with AI","url":"https://addaly.com/learn/building-apps-with-ai","level":"beginner","track":"build"},"lesson":{"slug":"the-built-bundle-and-the-history","title":"Secrets in the built bundle, and in the history","url":"https://addaly.com/learn/building-apps-with-ai/the-built-bundle-and-the-history","position":70,"of":87,"minutes":8,"answer":"Search the built output and the whole git history for keys, because a secret is leaked by the build that inlines it and the commit that once held it.","keyIdea":"Search the built output and the whole git history for keys, because a secret is leaked by the build that inlines it and the commit that once held it, not only by the file you can see today."},"body":{"format":"markdown","text":"## Two places a secret hides\n\nThe first security lesson showed the leak in principle: a key behind a public prefix, or a `.env` file committed. This lesson is the procedure for finding both, because both are invisible in the editor. The key is not in any file you would open; it is in the output of the build, and in commits you have forgotten.\n\n## Searching the bundle\n\nBuild the production output and search it for the shapes of keys:\n\n```bash\nnpm run build\ngrep -rEo \"(sk-[A-Za-z0-9_-]{10,}|AIza[A-Za-z0-9_-]{30,}|AKIA[A-Z0-9]{16}|postgres(ql)?://[^\\\"' ]+)\" .next/static dist build 2>/dev/null | head\n```\n\nAdjust the patterns to the providers you use; most keys have a recognisable prefix, and a database URL is unmistakable. Anything that prints is in a file every visitor downloads.\n\nThe same check from the outside, on the deployed site: open the developer tools, the Sources panel, and use search across all files for the same patterns. This is what a scanner does to every public site, automatically, and it takes it seconds.\n\nSource maps are the quiet version. A production build that ships `.map` files lets anyone reconstruct the original source, including comments, and sometimes server-side files bundled by mistake. Most frameworks disable source maps in production by default; check that yours has not been switched on by an agent debugging a build.\n\n## Searching the history\n\n```bash\ngit log -p --all | grep -nE \"sk-[A-Za-z0-9_-]{10,}|AIza|AKIA|BEGIN (RSA|OPENSSH) PRIVATE KEY\" | head\n```\n\nThat searches every diff of every commit on every branch. It is slow on a large repository and it is worth the wait once. For a proper job, `gitleaks` is free, runs in seconds, and knows several hundred key formats:\n\n```bash\ngitleaks detect --source . --verbose\n```\n\nRun it before making any repository public, and put it in a pre-commit hook or the CI so that a key cannot be committed in the first place. Catching the commit is worth a hundred times the scan afterwards.\n\n## When you find one\n\nThe order matters, and the instinct, delete the file, is the least useful step.\n\n1. **Revoke the key at the provider.** Now, before anything else. A key that has been in a public repository, or in a bundle on a public site, is compromised, and no operation on your side can change that. The provider's dashboard has a revoke or delete button next to every key.\n2. **Issue a new one** and put it in the host's environment settings, server-side, no prefix.\n3. **Check the provider's usage** for the period the key was exposed. A bill you did not expect is the sign somebody found it first.\n4. **Then** tidy the repository. Delete the file, add it to `.gitignore`, commit.\n\nRewriting history to remove the commit (`git filter-repo` does it) is a reasonable extra step for a repository that will be public, and it is not a fix. Anyone who cloned in the meantime has the old history, and GitHub keeps orphaned commits reachable by id for some time after a rewrite. Revocation is the fix. Everything else is housekeeping.\n\n## Where else it lands\n\nLogs. A request that failed with the key in a header, printed in full by a library's debug mode, is in the host's log retention for as long as that is set. An error tracker that captures request context captures the header too. Check what your error tracking is configured to scrub; the good ones scrub `Authorization` by default and nothing else.\n\nChat transcripts. A `.env` pasted into an agent's conversation to \"help it understand the setup\" is in that provider's retention. Agents can read the file themselves; they do not need it pasted, and the rules file can say so.\n\nScreenshots and screen recordings. The demo video with the terminal open showing `DATABASE_URL`. This has happened to companies.\n\n## The variables that are not secrets\n\nNot everything in `.env` is sensitive. A public analytics id, a map tile key designed to be public, the app's own URL: these belong with the prefix and it is fine. The distinction is whether a stranger with the value can cost you anything or read anything: if yes, it is a secret and it never leaves the server; if no, the prefix is honest.\n\n## The check to keep\n\nBefore every public release: the bundle grep, the history scan, and a look at the Network tab for any request leaving the browser with a credential attached. Five minutes. The founding lesson of this block said a key in the browser is published; this is how you make sure you did not publish one.\n\n> Figure: When you find a key where it should not be. Steps: 1. Revoke it (At the provider, now, before anything else); 2. Issue a new one (Into the host's settings, server-side, no prefix); 3. Read the usage (For the whole period it was exposed); 4. Then tidy up (Delete the file, add it to .gitignore, commit). The instinct is to delete the file, and it is the least useful step. A key that has been in a public repository or a public bundle is compromised, and no operation on your side changes that."},"check":{"question":"A key was committed in .env three weeks ago, then .env was added to .gitignore and deleted from the repository, and the repository was later made public. What is the state of the key?","options":[{"text":"Leaked; the key must be revoked at the provider now, and a history rewrite is optional tidying rather than the fix.","correct":true},{"text":"Safe as long as nobody clones the repository, since the history is only downloaded on clone.","correct":false,"tempting":"Every clone brings every commit, and scanners clone public repositories within minutes of them appearing."},{"text":"Safe, because the file no longer exists in the repository and .gitignore prevents it returning.","correct":false,"tempting":"The current tree is clean; the commit from three weeks ago still holds the file, and git history is public along with everything else."},{"text":"Leaked, but rewriting the history with a filter tool to remove the commit will make it safe again, because the key will no longer exist anywhere in the repository.","correct":false,"tempting":"A rewrite removes it from your copy; anyone who fetched in the meantime has it, and the provider cannot know, so revocation is the only certain step."}]},"prev":{"slug":"authorisation-beyond-ownership","title":"Authorisation beyond ownership","url":"https://addaly.com/learn/building-apps-with-ai/authorisation-beyond-ownership"},"next":{"slug":"injection-in-three-places","title":"Injection in three places: the query, the shell, the URL","url":"https://addaly.com/learn/building-apps-with-ai/injection-in-three-places"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Secrets in the built bundle, and in the history', in Building Apps with AI. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/building-apps-with-ai/the-built-bundle-and-the-history (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Secrets in the built bundle, and in the history","courseTitle":"Building Apps with AI","module":{"index":8,"slug":"the-holes-ai-code-has-by-default","title":"The security holes AI code has by default"},"answer":null,"keyIdea":"Search the built output and the whole git history for keys, because a secret is leaked by the build that inlines it and the commit that once held it, not only by the file you can see today.","hero":{"kind":"figure","figure":{"t":"figure","label":"Figure","figure":{"type":"flow","steps":[{"label":"Revoke it","detail":"At the provider, now, before anything else"},{"label":"Issue a new one","detail":"Into the host’s settings, server-side, no prefix"},{"label":"Read the usage","detail":"For the whole period it was exposed"},{"label":"Then tidy up","detail":"Delete the file, add it to .gitignore, commit"}],"title":"When you find a key where it should not be","caption":"The instinct is to delete the file, and it is the least useful step. A key that has been in a public repository or a public bundle is compromised, and no operation on your side changes that."},"alt":"Figure: When you find a key where it should not be. Steps: 1. Revoke it (At the provider, now, before anything else); 2. Issue a new one (Into the host’s settings, server-side, no prefix); 3. Read the usage (For the whole period it was exposed); 4. Then tidy up (Delete the file, add it to .gitignore, commit). The instinct is to delete the file, and it is the least useful step. A key that has been in a public repository or a public bundle is compromised, and no operation on your side changes that."}},"blocks":[{"t":"h2","id":"two-places-a-secret-hides","runs":[{"text":"Two places a secret hides"}]},{"t":"p","runs":[{"text":"The first security lesson showed the leak in principle: a key behind a public prefix, or a "},{"text":".env","code":true},{"text":" file committed. This lesson is the procedure for finding both, because both are invisible in the editor. The key is not in any file you would open; it is in the output of the build, and in commits you have forgotten."}]},{"t":"h2","id":"searching-the-bundle","runs":[{"text":"Searching the bundle"}]},{"t":"p","runs":[{"text":"Build the production output and search it for the shapes of keys:"}]},{"t":"code","lang":"bash","label":"Bash","text":"npm run build\ngrep -rEo \"(sk-[A-Za-z0-9_-]{10,}|AIza[A-Za-z0-9_-]{30,}|AKIA[A-Z0-9]{16}|postgres(ql)?://[^\\\"' ]+)\" .next/static dist build 2>/dev/null | head"},{"t":"p","runs":[{"text":"Adjust the patterns to the providers you use; most keys have a recognisable prefix, and a database URL is unmistakable. Anything that prints is in a file every visitor downloads."}]},{"t":"p","runs":[{"text":"The same check from the outside, on the deployed site: open the developer tools, the Sources panel, and use search across all files for the same patterns. This is what a scanner does to every public site, automatically, and it takes it seconds."}]},{"t":"p","runs":[{"text":"Source maps are the quiet version. A production build that ships "},{"text":".map","code":true},{"text":" files lets anyone reconstruct the original source, including comments, and sometimes server-side files bundled by mistake. Most frameworks disable source maps in production by default; check that yours has not been switched on by an agent debugging a build."}]},{"t":"h2","id":"searching-the-history","runs":[{"text":"Searching the history"}]},{"t":"code","lang":"bash","label":"Bash","text":"git log -p --all | grep -nE \"sk-[A-Za-z0-9_-]{10,}|AIza|AKIA|BEGIN (RSA|OPENSSH) PRIVATE KEY\" | head"},{"t":"p","runs":[{"text":"That searches every diff of every commit on every branch. It is slow on a large repository and it is worth the wait once. For a proper job, "},{"text":"gitleaks","code":true},{"text":" is free, runs in seconds, and knows several hundred key formats:"}]},{"t":"code","lang":"bash","label":"Bash","text":"gitleaks detect --source . --verbose"},{"t":"p","runs":[{"text":"Run it before making any repository public, and put it in a pre-commit hook or the CI so that a key cannot be committed in the first place. Catching the commit is worth a hundred times the scan afterwards."}]},{"t":"h2","id":"when-you-find-one","runs":[{"text":"When you find one"}]},{"t":"p","runs":[{"text":"The order matters, and the instinct, delete the file, is the least useful step."}]},{"t":"ol","start":1,"items":[[{"text":"Revoke the key at the provider.","b":true},{"text":" Now, before anything else. A key that has been in a public repository, or in a bundle on a public site, is compromised, and no operation on your side can change that. The provider’s dashboard has a revoke or delete button next to every key."}],[{"text":"Issue a new one","b":true},{"text":" and put it in the host’s environment settings, server-side, no prefix."}],[{"text":"Check the provider’s usage","b":true},{"text":" for the period the key was exposed. A bill you did not expect is the sign somebody found it first."}],[{"text":"Then","b":true},{"text":" tidy the repository. Delete the file, add it to "},{"text":".gitignore","code":true},{"text":", commit."}]]},{"t":"p","runs":[{"text":"Rewriting history to remove the commit ("},{"text":"git filter-repo","code":true},{"text":" does it) is a reasonable extra step for a repository that will be public, and it is not a fix. Anyone who cloned in the meantime has the old history, and GitHub keeps orphaned commits reachable by id for some time after a rewrite. Revocation is the fix. Everything else is housekeeping."}]},{"t":"h2","id":"where-else-it-lands","runs":[{"text":"Where else it lands"}]},{"t":"p","runs":[{"text":"Logs. A request that failed with the key in a header, printed in full by a library’s debug mode, is in the host’s log retention for as long as that is set. An error tracker that captures request context captures the header too. Check what your error tracking is configured to scrub; the good ones scrub "},{"text":"Authorization","code":true},{"text":" by default and nothing else."}]},{"t":"p","runs":[{"text":"Chat transcripts. A "},{"text":".env","code":true},{"text":" pasted into an agent’s conversation to “help it understand the setup” is in that provider’s retention. Agents can read the file themselves; they do not need it pasted, and the rules file can say so."}]},{"t":"p","runs":[{"text":"Screenshots and screen recordings. The demo video with the terminal open showing "},{"text":"DATABASE_URL","code":true},{"text":". This has happened to companies."}]},{"t":"h2","id":"the-variables-that-are-not-secrets","runs":[{"text":"The variables that are not secrets"}]},{"t":"p","runs":[{"text":"Not everything in "},{"text":".env","code":true},{"text":" is sensitive. A public analytics id, a map tile key designed to be public, the app’s own URL: these belong with the prefix and it is fine. The distinction is whether a stranger with the value can cost you anything or read anything: if yes, it is a secret and it never leaves the server; if no, the prefix is honest."}]},{"t":"h2","id":"the-check-to-keep","runs":[{"text":"The check to keep"}]},{"t":"p","runs":[{"text":"Before every public release: the bundle grep, the history scan, and a look at the Network tab for any request leaving the browser with a credential attached. Five minutes. The founding lesson of this block said a key in the browser is published; this is how you make sure you did not publish one."}]}],"check":{"question":[{"text":"A key was committed in .env three weeks ago, then .env was added to .gitignore and deleted from the repository, and the repository was later made public. What is the state of the key?"}],"options":[{"text":[{"text":"Leaked; the key must be revoked at the provider now, and a history rewrite is optional tidying rather than the fix."}],"correct":true,"tempting":null},{"text":[{"text":"Safe as long as nobody clones the repository, since the history is only downloaded on clone."}],"correct":false,"tempting":[{"text":"Every clone brings every commit, and scanners clone public repositories within minutes of them appearing."}]},{"text":[{"text":"Safe, because the file no longer exists in the repository and .gitignore prevents it returning."}],"correct":false,"tempting":[{"text":"The current tree is clean; the commit from three weeks ago still holds the file, and git history is public along with everything else."}]},{"text":[{"text":"Leaked, but rewriting the history with a filter tool to remove the commit will make it safe again, because the key will no longer exist anywhere in the repository."}],"correct":false,"tempting":[{"text":"A rewrite removes it from your copy; anyone who fetched in the meantime has it, and the provider cannot know, so revocation is the only certain step."}]}],"order":[1,2,0,3],"why":null},"prev":{"slug":"authorisation-beyond-ownership","title":"Authorisation beyond ownership","minutes":9},"next":{"slug":"injection-in-three-places","title":"Injection in three places: the query, the shell, the URL","minutes":9}}}