Popular searches
//

Zero chance for AI: securely load secrets from the password manager

1.10.2026 | 9 minutes reading time

Credentials for local development are stored as plaintext in .env files in many projects. That is convenient, but risky. And ever since AI coding agents started accessing project directories with read permissions, it has also become a growing security risk. In my current project, we have therefore replaced the .env files with references to entries in 1Password, which are only resolved when the application starts. This article describes the approach and shows, using three practical examples, how it can be integrated into existing workflows - from npm scripts to curl calls to skills for AI agents that keep the credentials hidden.

Plaintext .env Files Are a Security Risk

Credentials do not belong in source code; instead, they are read from environment variables at build or runtime - this has long been established in software projects. For local development, however, exporting these variables into the shell is neither secure nor convenient: all applications gain access to the values there, and updating them becomes tedious. Most programming languages and frameworks therefore offer integrated mechanisms for managing configuration and environment-specific values - such as profiles in Spring Boot or .env files in Node.js.

While these files do bundle the configuration in a central location, they end up as plaintext in the project directory. Common countermeasures - excluding the .env from being committed via .gitignore and checking in a .env.example as a template - are error-prone: the example file quickly falls behind the real configuration, and it is not uncommon for the sensitive .env files to be shared between developers via chat. With the rise of AI agents that access the local project directory with at least read permissions, and the increase in supply chain attacks such as the Shai-Hulud worm that specifically steal credentials from development environments, such plaintext files are definitively no longer secure.

An alternative is offered by password managers such as 1Password: using the 1Password CLI, credentials can be read from the manager at startup time and passed to the application as environment variables - they then exist only in the respective process.

The Basic Principle of Password Manager References

The approach moves the storage of credentials out of the .env file and into the password manager. The file itself no longer contains any plaintext, but merely a reference to the respective entry in the password manager. In 1Password, this reference follows the schema op://Vault/Item/Field - it points to a vault, an item contained within it, and a specific field, such as the password. Since such a file contains no secret values, it can be safely committed to the repository. The previous split into a git-ignored .env and a committed .env.example is thus eliminated; configuration and references are versioned together and no longer diverge.

At the same time, the password manager solves the organizational problems of the previous distribution via chat. Vaults can be shared with the team and maintained by multiple people, and access rights can be revoked at any time - for example, when someone leaves the team. The credentials thus remain centrally managed, instead of being scattered as copies across chat histories.

The references are only resolved when the application starts. The 1Password CLI provides the op run command for this purpose, which reads a .env file, loads the contained op:// references from the manager, and passes the values as environment variables to the child process:

The credentials thus end up neither in the shell nor on the hard drive, but exist exclusively as environment variables in the started process. The prerequisite is a configured 1Password CLI that is authenticated against your own account.

If credentials are needed for multiple environments - such as a development and a production environment of the connected service - a separate file is created for each environment, for example .env.development and .env.production. Each file references its own entry in the password manager, so that the credentials of the environments can be cleanly separated from one another. The desired file is selected at startup via the --env-file parameter.

This approach is deliberately limited to local development: the CI build continues to use its own credential store (such as AWS SSM or GitLab CI variables), which developers do not need access to, and remains unaffected by the local files.

The following three examples show the same approach in different contexts. Resolving the references via op run is not tied to the build process of a specific language, but works for any tool that can read environment variables.

Example 1: npm Scripts with op run

In Node projects, the calls are typically stored as scripts in the package.json. A separate script is created for each environment, combining op run with the respective .env file. The files .env.staging and .env.production contain, in addition to non-sensitive configuration values, only the references to the credentials of the respective environment:

The corresponding scripts in the package.json invoke the start command via op run with the appropriate file. Sections that are not needed are omitted here:

The commands next dev and next build come from Next.js, but can be replaced with any start command. When running npm run dev:staging, op run resolves the references from .env.staging and starts the development server with the injected values.

The scripts apply exclusively to local development; the CI build invokes the build commands without op run and obtains the credentials from its own credential store (such as AWS SSM or GitLab CI variables).

Example 2: HTTP APIs with curl and Tokens

The same approach can be applied outside of a build process, for example for directly calling an HTTP API. The endpoint expects an auth header - a token or even a password. The .env file provides the required credentials as references:

The call is made via op run, so that the credentials are read from the environment variables and appear neither in the command itself nor in the shell history. A token is set in the Authorization header:

If the API instead expects basic authentication with username and password, the call looks accordingly:

Since op run only passes the variables to the curl process, the credentials are not available in the shell afterwards and are not accidentally reused in further commands.

Example 3: Skills for AI Agents without Plaintext Credentials

The curl call from the previous example is hard to remember due to its length and the parameters for different endpoints. For recurring queries, it therefore makes sense to encapsulate the call in a script.

The following TypeScript script takes up the curl call from Example 2. It expects the token as an environment variable and outputs only the factual response - not the token:

Such a script can be stored in the project as a reusable tool and also used as a skill for AI coding agents. These autonomously execute commands and read files in the project directory - if the credentials were stored there as plaintext, they would be directly visible to the agent. If the agent is to retrieve data from the API, it therefore receives neither the token nor a curl command, but only the call of the script, into which op run injects the credentials:

The agent executes this command and processes the JSON response on standard output. It never gets to see the token - the script handles the resolution of the op:// references and their use in the Authorization header. So that the agent knows the script and uses it correctly, it is described to it as a skill. It is common to place a short Markdown file next to the script that defines the call and behavior:

Two points are crucial here:

  • On the one hand, the token exists exclusively as an environment variable in the subprocess and is never stored as a file.
  • On the other hand, the script does not output the token on standard output - the agent only sees the API response.

Since the script only reads environment variables, the pattern works independently of the build process of the development language and can be transferred to any tools.

Limitations and Alternatives

The presented approach is particularly suitable when a password manager is already in use in your own company. In this case, it is worth checking the documentation to see whether it offers a comparable CLI such as op run --env-file. In addition to the 1Password CLI, other managers also provide corresponding tools - such as Bitwarden with bw or KeePassXC with keepassxc-cli, although these use their own mechanisms for resolving credentials and not the op:// reference schema in .env files.

The approach shown here is therefore not tied to a specific product, but can be transferred to other password managers.

For acceptance in everyday development, the developer experience is also relevant. The key factor is how conveniently the password manager can be unlocked with each call. If your own hardware has a fingerprint sensor, a short biometric approval is sufficient to release the entry. If such an option is missing, a complex master password must be entered with each call, depending on company policy. With twenty or more unlocks per hour, this leads, in my experience, to rejection and circumvention of the procedure, for example by storing credentials in files again. Biometric approval is therefore more than a convenience feature: it is a prerequisite for the procedure to actually be accepted in everyday use.

The approach is primarily suitable for small to medium-sized projects with a single password manager and a focus on local development. If, on the other hand, multiple password managers, declarative management of credentials, different profiles, or audit logging are the priority - for example in larger or heterogeneous projects - SecretSpec is a suitable alternative (see the blog article by my colleague Florian B.).

A related use case is described by my colleague Paul Severin in his article Configuring MCP Servers Securely with Password Manager CLIs (DE): it deals with securing MCP server configurations for coding agents such as Claude Code or Cursor.

Conclusion

In summary, credentials can be resolved at runtime using a password manager CLI, instead of storing them as plaintext in .env files. The .env files contain only references and can therefore be committed to the repository, while the actual credentials exist exclusively as environment variables in the respective process. Especially in combination with AI coding agents that access the project directory with read permissions, the credentials thus remain outside the agent's reach - a gain in security without noticeable additional effort in everyday development.

//

More articles in this subject area

Discover exciting further topics and let the codecentric world inspire you.