AI-assisted development

What Happens When Your AI Coding Assistant Can Read Your `.env` File?

If a coding assistant can read .env, it can potentially read whatever authority the file contains. Reduce exposure by changing the workspace and credential boundary.

If a coding assistant or agent can inspect your workspace, assume it may be able to read files in that workspace unless the current tool or execution boundary explicitly prevents it. A leading dot and a .gitignore rule are not access-control mechanisms.

This is not a claim that an agent is secretly searching for keys. It is a reminder that file access, command execution, environment inheritance, logs, and copied text all form part of the trust boundary.

Chat-only assistants and workspace-enabled agents

A chat-only assistant sees the information you send it. If you paste a configuration file, request body, stack trace, or environment dump, that pasted content becomes part of the conversation. The assistant does not need filesystem access for a secret to leave your machine.

A workspace-enabled local agent may have a different set of capabilities. Depending on the product, mode, approvals, and local integration, it may search files, run commands, inspect command output, edit code, and start development processes. Codex, Claude Code, and Cursor should therefore be discussed conditionally: the product name alone does not prove that a particular session can read .env, execute a shell command, or retain output.

Check the actual session:

  • Is the repository the only directory in scope, or is the agent allowed to reach a parent directory?
  • Can it read hidden files?
  • Can it run a command without asking?
  • Does it have network access?
  • Where are transcripts, tool output, or generated changes retained?

A chat request such as “review this error” and a local request such as “run the test suite and fix failures” are different trust arrangements, even if they use the same model.

The workspace filesystem boundary

In a typical repository:

~~~text app/ ├── src/ ├── tests/ ├── .gitignore └── .env ~~~

If the workspace boundary includes app/, .env is a sibling of the source and test directories. A tool that can list or open the project files may be able to open it. Some tools offer file exclusions, ignored paths, approval prompts, or sandboxing; use those controls when available, but verify what they cover.

A file exclusion may prevent a direct read while still allowing an application command to disclose the value. If the agent can run php artisan config:show, node scripts/debug-config.js, or a test that prints resolved settings, the secret can still cross the boundary through command output. The important boundary is not only “can the tool open this filename?” It is also “can anything the tool starts read and report this value?”

Commands run as the developer

A local command normally runs under the developer’s operating-system account or under a sandbox identity configured by the tool. That account may already have access to the repository, home directory, SSH configuration, cloud credential files, package caches, browser profiles, and other projects.

The agent does not need root access to expose a production key. If your shell user can read .env, a command launched as that user may read it. If the agent can start a development server as that user, the server may also inherit the same access.

Treat command approval as part of credential design. A request to run git status is not equivalent to a request to run env, find .. -type f, or a script that uploads diagnostics. Review broad commands before approving them, especially commands that combine configuration loading with logging or network access.

Inherited environment variables

A process can receive secrets without opening the .env file. A shell may have loaded STRIPE_SECRET_KEY with export, a dotenv loader may have populated the application process, or a deployment tool may inject variables before starting a command.

A child process normally inherits the environment of its parent. That means a test runner, subprocess, package script, worker, or development server can receive the same variable even if the assistant only asked to run a narrow command.

For example, this diagnostic discloses the value:

~~~php var_dump(getenv('STRIPE_SECRET_KEY')); ~~~

This one checks configuration without printing the credential:

~~~php echo getenv('STRIPE_SECRET_KEY') !== false ? 'configured' : 'missing'; ~~~

The same rule applies to JavaScript, Python, shell scripts, framework configuration commands, and health endpoints. Prefer checking one named value, its presence, or a carefully controlled non-reversible fingerprint. Do not dump the whole configuration object to find one missing setting.

Test runners, subprocesses, and development servers

Test runners are frequent disclosure paths because they collect output, snapshots, failures, and environment metadata. A failing assertion can include a resolved configuration array. A snapshot test can serialize a client object containing a token. A fixture generator can copy a live URL or authorization header into a file that is later committed.

Subprocesses create the same risk. A package script might run a formatter, browser test, migration, queue worker, or CLI command with inherited environment variables. A development server may expose a debug page that prints configuration values or includes them in an exception report. A background worker can write the value to a persistent log long after the original command ends.

Before running a command with provider configuration available, ask what it can emit:

  • command output and verbose request traces;
  • debug pages and framework configuration panels;
  • exception reports and crash dumps;
  • test snapshots and generated fixtures;
  • shell history, including expanded command arguments;
  • copied prompts, issue reports, or support transcripts.

Keep secrets out of URLs and command arguments where possible. Shell history can retain a value even after the terminal closes. A copied prompt can retain it in the assistant transcript or a team system. Redaction must happen before the content is copied, not after it has been shared.

Generated code can turn a temporary leak into a permanent one

Suppose an assistant is asked to diagnose a failed Stripe request. Someone runs a command that prints the full configuration, then asks the assistant to “make a reproducible test.” The generated test may include the real sk_test_... or sk_live_... value copied from the output. A generated README, fixture, curl example, or migration can preserve the same secret in a more discoverable location.

Use placeholders in examples:

~~~text STRIPE_SECRET_KEY=replace_with_a_test_key ~~~

Ask the assistant to redact Authorization headers and replace credentials before generating code. Review diffs for secrets before committing. Search generated files and fixtures for provider prefixes, but do not treat a search result as proof that the credential is safe; values may be encoded, truncated, or stored in logs outside the repository.

.gitignore and agent exclusions solve different problems

.gitignore controls whether Git considers a path untracked by default. It helps prevent accidental commits, but it does not hide the file from a local process, editor, agent, test runner, backup tool, or filesystem search.

An agent exclusion or workspace rule controls whether a particular tool is supposed to inspect a path. It may reduce direct exposure to that tool, but it does not stop Git from committing the file, another tool from reading it, or a child process from inheriting the value.

Use both when appropriate, and understand their limits:

~~~text .gitignore → version-control tracking behavior agent exclusion → one tool's intended inspection boundary OS permissions → which local accounts can open the file secret manager → who may retrieve a stored value relay boundary → whether the application receives the provider credential ~~~

None of these labels should be treated as interchangeable.

Production and test credentials deserve different treatment

Do not put a production Stripe key in a local workspace merely because the application is easier to test that way. Use a restricted Stripe test key locally, with test-mode resources and a deliberately limited account. A test key can still expose test data or incur cost, but it should not authorize a live charge or production customer lookup.

Likewise, keep production AWS identities in deployment or workload infrastructure rather than in a developer’s .env. A local application can use a role or key limited to a development account, region, bucket, or SES test path. The production deployment can receive its separate secret at runtime.

A safer pattern is:

~~~text Local workspace → restricted test credential CI/deployment → production credential injected at deploy/runtime Source control → names and placeholders only ~~~

The strongest local setup is one where the coding assistant has no production credential to expose. Environment separation is more effective than hoping a tool will recognize which value is sensitive.

What to do after exposure

If a credential appears in an assistant transcript, retained tool log, exception report, shell history, issue, or generated file, treat it as exposed.

  1. Identify the provider, credential, environment, and likely audience.
  2. Revoke or rotate the provider credential promptly; do not wait for a repository cleanup.
  3. Check provider audit logs for use during the exposure window.
  4. Remove the value from current files, snapshots, logs, transcripts, and shared issue systems where you control them.
  5. Replace examples with placeholders and add a regression check or secret scanner.
  6. Check whether the exposed identity could access other accounts, environments, or secrets.
  7. Record the incident and the new credential location.

Deleting the latest copy does not erase an earlier transcript or a cached log. Rotation is what invalidates the provider authority. If the credential was production-capable, treat the event as an operational incident even if you cannot prove that anyone used it.

Change the boundary when the application does not need the key

Use deployment-platform secret storage or a dedicated secret manager when the application genuinely needs to call the provider directly. Those systems improve storage and controlled delivery; the running application still receives the credential.

Use a provider operation boundary when the application needs an authorized action rather than the provider key. Gateman’s POST /stripe/request can keep the configured Stripe credential outside the application while the application submits a supported native Stripe request. Generic secret retrieval is different: secrets:read returns the approved value to the application, so it remains the credential-distribution model.

The application’s Gateman identity is still sensitive. A compromised application can abuse every capability and environment granted to that identity, even if it cannot extract the Stripe key. The boundary reduces provider-key exposure; it does not remove authorization, logging, monitoring, or incident-response responsibilities.

An AI coding assistant with access to .env has access to whatever authority that file contains. The practical response is to minimize production authority, separate environments, scope credentials, avoid secret-bearing diagnostics, and keep provider keys outside the application when the application does not need them.

For a broader implementation workflow, see How to Give AI Coding Agents Access to APIs Without Exposing Provider Credentials and Gateman’s Contract v1.

← All articles