Fix: gitignore the .env that adk create writes so scaffolded agents cannot commit their API key - #364
Open
AmaadMartin wants to merge 2 commits into
Open
Fix: gitignore the .env that adk create writes so scaffolded agents cannot commit their API key#364AmaadMartin wants to merge 2 commits into
AmaadMartin wants to merge 2 commits into
Conversation
added 2 commits
July 30, 2026 22:48
file_utils has no plain-text read: loadFileData is JSON-only and throws on anything that is not JSON, so a caller that needs the raw contents of a text file has to reach around the module into node:fs. Add the missing primitive.
adk create writes a raw GOOGLE_API_KEY into <agent>/.env and creates no .gitignore, so the first 'git add . && git commit' in a freshly scaffolded agent folder commits a live credential. Write a .gitignore listing .env alongside the .env itself, matching _ensure_dotenv_gitignored in adk-python's cli_create.py. An existing .gitignore is appended to rather than overwritten, because removeFolder() only logs when deletion fails and scaffolding then proceeds into a folder whose .gitignore survived; entries are kept verbatim and .env is listed at most once.
This was referenced Aug 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
Closes: #issue_number
Related: #issue_number
Problem:
adk createwrites the user's rawGOOGLE_API_KEYinto<agent>/.env(generateEnvFile,dev/src/cli/cli_create.ts) and creates no.gitignore. The scaffolded folder is a brand-new directory in the user'scwd, so the repo's own root.gitignoredoes not protect it, and the very firstgit init && git add . && git commitin that folder commits a live credential. A search forgitignoreacrossdev/returned zero hits before this change.adk-pythonalready closes this hole insrc/google/adk/cli/cli_create.py(_ensure_dotenv_gitignored, called from_generate_filesimmediately after the.envwrite). This ports that guarantee.Solution:
generateFiles()now callsensureDotenvGitignored(agentDir)directly after writing.env, so a.gitignorelisting.envalways sits next to the secret it protects. A.gitignorethat is already present is appended to, never overwritten — its entries are kept verbatim and.envis listed at most once.Why the append path exists rather than a one-line
saveToFile(gitignorePath, '.env\n'):generateAgentFolder()does not guarantee an empty directory.removeFolder()(dev/src/utils/file_utils.ts) catchesfs.rmfailures and only logs, andcreateFolder()swallowsmkdirfailures the same way, so scaffolding proceeds into a folder that survived deletion. I reproduced this against the built CLI (see E2E below): with the parent directory read-only,demo2/.gitignoresurvived and the unconditional-write version would have destroyed the user's existing entries. The rationale is recorded in the function's doc comment so the branch is not deleted later as dead code.Design notes:
cli_create.tsperforms no directnode:fsI/O — all of its file access goes throughdev/src/utils/file_utils.ts. That module had no plain-text read (loadFileDatais JSON-only and throws on anything else), so this adds one generically named primitive,readTextFile, a 3-line delegation tofs.readFile(path, {encoding: 'utf-8'}). It is an internal util and is not re-exported fromdev/src/index.ts./\r?\n/, not'\n'. Python'sstr.splitlines()splits on\r\n, so a CRLF-authored.gitignorewhose line reads.env\r\nis correctly recognised there; the naivesplit('\n')yields'.env\r'and would append a duplicate entry on Windows-authored files. This is parity with the reference, and it has its own regression test.ensureDotenvGitignoredis module-private incli_create.ts, mirroring the reference's placement; only the genericreadTextFileprimitive went intofile_utils.ts..env,agent.{ts,js},package.jsonortsconfig.json. The one user-visible difference is the extra.gitignorein the "Created the following files in ..." listing, which is picked up automatically fromlistFiles(agentDir)..envwrite now uses the sameDOTENV_FILE_NAMEconstant as the ignore entry, so the file written and the entry ignoring it cannot drift apart.Collision check (required before starting):
gh pr list --repo AmaadMartin/adk-js --state open --limit 100returned no PR adding a.gitignoreto scaffolded agents. The two open PRs touching the same files are adjacent only, in disjoint regions: #286 (GOOGLE_GENAI_USE_ENTERPRISErename insidegenerateEnvFile, plus.envcontent assertions) and #313 (assertions incli_create_test.ts). Neither lands this change and neither conflicts with it, so this branches frommainrather than stacking.Testing Plan
Please describe the tests that you ran to verify your changes. This is required for all PRs that are not small documentation or typo fixes.
Unit Tests:
[x] I have added or updated unit tests for my change.
[x] All unit tests pass locally.
Seven new cases in
dev/test/cli/cli_create_test.ts(all driven through the publiccreateAgent(...)entry point) and two indev/test/utils/file_utils_test.ts. No existing test was edited, renamed, skipped or deleted; the only changes to existing test scaffolding are additive — two symbols added to thefile_utilsmock factory and two default mock values inbeforeEach.New-code coverage measured with
@vitest/coverage-v8overdev/src/cli/cli_create.tsanddev/src/utils/file_utils.ts: every added line and every added branch is executed (uncovered lines in both files are all pre-existing code untouched by this PR). Coverage thresholds invitest.config.tswere not modified.Proof the tests can fail. Each new test was run against mutated source and observed to fail:
await ensureDotenvGitignored(agentDir);fromgenerateFiles...when none exists,...Vertex AI backend too, both append cases, empty-file case.expected "spy" to be called with arguments: [ StringContaining ".gitignore", …(1) ]content.split(/\r?\n/)->content.split('\n')expected "spy" to not be called with arguments: [ StringContaining ".gitignore", …(1) ]separatorcomputation (always append'\n' + entry)'node_modules\n\n.env\n'/'\n.env\n'saveToFile(gitignorePath, '.env\n')readTextFile->.catch(() => '')(swallow)readTextFile throws when readFile rejectsreadTextFile-> drop{encoding: 'utf-8'}readTextFile returns the file contents as textTwo pre-existing conditions in this sandbox, unrelated to this PR and identical on
main:dev/test/cli/cli_create_test.ts > should handle Vertex AI selection with gcloud defaultsfails whenGOOGLE_CLOUD_LOCATION/GOOGLE_CLOUD_PROJECTare set in the ambient environment (the suite is not hermetic; already addressed by open PRs Fix: scrub ADK environment variables from unit test runs #302/Fix: scrub ambient DATABASE_URL across the whole dev CLI unit suite #348). Verified failing onmainwithout this change; the runs above unset those two variables.npm run ts:checkexits 2 with 280 errors, all incore/test/**andtests/integration/**. The output is byte-identical with and without this branch (diffof both logs is empty), and none of the errors reference the four files changed here.Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.
All three paths were exercised against the built CLI with no mocks.
.gitignorethat survived folder creation. Made the parent directory read-only soremoveFolder()'sfs.rmfails and is swallowed, exactly as it can in the wild:The user's
node_modulesentry is preserved,.envis appended, and no blank line is introduced.node_modules\n.env\n: the file came back byte-identical, andgit add .staged no.env.Checklist
[x] I have read the CONTRIBUTING.md document.
[x] I have performed a self-review of my own code.
[x] I have commented my code, particularly in hard-to-understand areas.
[x] I have added tests that prove my fix is effective or that my feature works.
[x] New and existing unit tests pass locally with my changes.
CI note (fork validation run)
All checks pass on the final run:
run-tests (ubuntu-latest),run-tests (windows-latest),run-tests (macos-latest),run-tests(cross-language),check-license,auto-assign.Two pre-existing, unrelated timeout flakes on the non-Linux legs needed re-runs, and both were reproduced on sibling PRs that touch none of these files:
tests/integration/app_loader/app_loader_test.ts > should discover apps vs agents across directories and standalone files,Test timed out in 40000ms. Same failure onfeat/js-environment-tools; open PR Fix: attribute cold AgentLoader discovery cost to beforeAll to stop macOS CI flake #260 targets exactly this cold-discovery flake.core/test/code_executors/unsafe_local_code_executor_test.ts > should execute shell code and return stdout,Test timed out in 5000ms.Both are green on the final run. Ubuntu passed on every attempt, including the first.