DEV Community

Leon
Leon

Posted on AI-assisted

Secrets, Agents, and Too Many Approval Buttons

There is a special kind of optimism in giving several Codex agents real work
and then discovering that you have also promoted yourself to Chief Approval
Button Officer.

Every agent needs one credential. Then another. One task is waiting for an API
token, another needs a webhook secret, and somewhere behind everything a
password manager is politely asking whether you approve. Again.

I recently spent far too much time looking for a secure, dependable way to
share credentials with concurrent local agents. Every promising solution
eventually introduced its own plot twist: quotas, concurrency problems,
context-free approval dialogs, plaintext files, or enough local infrastructure
to make me wonder whether I had accidentally started a platform team.

1Password is not a requirement for the pattern in this article. It is simply
the password manager I use, and therefore the character that appears most
often in this story. The broader problem exists with any secret store: how do
you give an application the credentials it needs without turning every agent
operation into another trip to the vault?

The answer that finally worked was almost disappointingly architectural:

Resolve secrets once at the application boundary. Let agent commands inherit
only an explicit application-wide allowlist.

I tested the whole assortment discussed below. Two approaches remained
practical enough to use. Choose one:

  1. Put the actual values in ~/.codex/.env.
  2. Keep the values in a secret store and start ChatGPT with op run. It reads an env file containing 1Password references, authenticates through 1Password desktop, resolves the values, and passes them to ChatGPT.

The first option is gloriously boring. The second keeps 1Password as the only
durable secret store. Both are compromises, but useful ones. I chose the option
that concentrates its weirdness at startup.

These are alternatives, not layers. If you migrate from ~/.codex/.env to
op run, remove the plaintext file rather than leaving two credential sources
behind.

I tested this on macOS on 2026-08-27 with ChatGPT desktop/Codex app
26.820.71523, Codex CLI 0.150.1, and 1Password CLI 2.39.0. The
undocumented behavior described below may change in later builds.

The Suspiciously Easy Option: ~/.codex/.env

On the ChatGPT/Codex desktop build I tested, ChatGPT loads this file when it
starts:

~/.codex/.env
Enter fullscreen mode Exit fullscreen mode

It is an ordinary dotenv file:

EXAMPLE_API_TOKEN=<actual value>
EXAMPLE_WEBHOOK_SECRET=<actual value>
Enter fullscreen mode Exit fullscreen mode

That is it. No sidecar and no broker.

I verified the behavior with both a fresh ChatGPT launch and a fresh
codex exec process, the non-interactive entry point to the same local Codex
tooling. OpenAI's documentation lists the
environment variables that Codex supports directly,
but it does not document automatic loading of this file. I therefore treat
~/.codex/.env as observed behavior of the tested setup, not a permanent
cross-platform contract.

Protect the file and never commit it:

chmod 600 ~/.codex/.env
Enter fullscreen mode Exit fullscreen mode

Changes take effect only on the next full launch. Closing a ChatGPT window does
not quit the application, however final the disappearing window may look.

This is the smallest setup and the easiest one to debug. Its price is equally
simple: the credentials remain on disk in plaintext. File permissions reduce
exposure, but the file is still a durable copy that must be protected, rotated,
and kept in sync.

The Option I Actually Use: Resolve References Once

I wanted 1Password to remain the source of truth without inviting it into
every agent operation. The order here matters: you run op run first. It then
asks 1Password desktop to unlock or approve the request, if needed, resolves
the references, and finally launches ChatGPT.

Launch sequence: run op run, authenticate through 1Password desktop if needed, resolve the references, and launch ChatGPT with allowed variables for Codex agents.

The runtime sequence has three stages:

  1. You start op run with ~/.codex/.env-run, which maps environment-variable names to 1Password references.
  2. op run contacts 1Password desktop. The desktop app unlocks or asks for approval if needed; op run then resolves the references and launches ChatGPT with the resulting environment.
  3. After ChatGPT starts, ~/.codex/config.toml decides exactly which variables Codex may forward to agent commands.

.env-run Is the Map, Not the Treasure

The file looks like this:

EXAMPLE_API_TOKEN=op://<vault>/<item>/<field>
EXAMPLE_WEBHOOK_SECRET=op://<vault>/<item>/<field>
Enter fullscreen mode Exit fullscreen mode

The right-hand sides are references, not copied credential values. I still
keep the file local, uncommitted, and mode 0600, because vault and item names
can reveal useful metadata. A treasure map may not contain the treasure, but
there is no reason to hand it out at the train station.

One Command, One Boundary

Quit ChatGPT completely, then run:

op run --env-file="$HOME/.codex/.env-run" -- /usr/bin/open -a ChatGPT
Enter fullscreen mode Exit fullscreen mode

That is the whole launch mechanism. op run begins first, asks 1Password
desktop to unlock or approve the request if necessary, replaces the references
with their values, and starts ChatGPT inside the resolved environment. There is
no second launcher or background service. The command does the interesting
work once and then gets out of the way.

This assumes that the 1Password CLI's desktop-app integration is enabled and
that the shell is not forcing a different 1Password authentication method. The
op run documentation
describes how it replaces secret references and gives the resolved values to
its child process.

Once ChatGPT starts, its agents inherit the resolved environment through the
policy below. They do not need to return to 1Password individually.

The Bouncer: shell_environment_policy

Getting values into the ChatGPT process is only half the job. config.toml
still decides which variables Codex may forward to commands.

An include entry creates an allowlist. Once any include filter exists,
variables that do not match one are removed. This is excellent for security
and slightly less excellent if you include two API tokens but forget that
programs also enjoy having a PATH.

A practical baseline looks like this:

[shell_environment_policy]
inherit = "all"
ignore_default_excludes = true

[shell_environment_policy.filters]
"PATH" = "include"
"HOME" = "include"
"USER" = "include"
"SHELL" = "include"
"PWD" = "include"
"TMPDIR" = "include"
"LANG" = "include"
"LC_*" = "include"
"TERM" = "include"
"CODEX_HOME" = "include"
"EXAMPLE_API_TOKEN" = "include"
"EXAMPLE_WEBHOOK_SECRET" = "include"
Enter fullscreen mode Exit fullscreen mode

The exact non-secret baseline depends on the commands you run. The goal is not
to recreate your entire login shell inside Codex. Preserve the ordinary
process context your tools need, then add credentials by exact name.

ignore_default_excludes = true matters because the example names contain
TOKEN and SECRET; the explicit include filters still form the allowlist.
The official OpenAI
configuration reference
documents the precedence and recommends filters instead of the legacy
include_only setting.

Adding a value to .env or .env-run is therefore only half a configuration
change. You must also include its name in shell_environment_policy. The
secret source and the allowlist must agree on the name; computers remain
stubbornly literal about spelling.

Because the environment belongs to the ChatGPT process, this works for project
and non-project tasks, including tasks whose working directories are created
dynamically.

The 1Password Obstacle Course

1Password has many relevant features. Each one made sense on its own. My
problem was asking for all of these properties at once:

  • several local agents;
  • concurrent work;
  • an explicit credential allowlist;
  • no repeated human intervention;
  • preferably no plaintext secret copy on disk.

That combination turned the product tour into an obstacle course. The following
observations come from the specific account and software setup identified in
the version note above; they are not timeless guarantees about every 1Password
configuration.

Service-Account Token

In one incident on my individual 1Password account, parallel agents repeated
reads until the shared daily allowance reached 1000 / 1000. By the time I
checked, the token's hourly counter was back at 0 / 1000, but the daily
account limit was still exhausted and dependent tasks had to wait roughly 13
hours. 1Password documents
that the 1,000-request daily limit for individual and Families accounts is
shared by all service accounts. A new token would have given me a new token,
not a new day.

Desktop CLI Calls from Agents

Every agent effectively acquired a doorbell. Approval appeared in a separate
1Password window without clearly identifying the requesting Codex task, the
downstream operation, or enough context to understand what I was approving.
That is human involvement, but not especially informed human involvement.

1Password Environments

In my test, values in an Environment were separate stored values; putting an
op:// reference inside one did not resolve it dynamically. The local dotenv
destination used a named pipe rather than plaintext, but prompted on first
read, relocked with 1Password, was not designed for concurrent access, and
fresh Codex processes alternated between receiving all variables and receiving
none. That made it unsuitable for this workflow.

1Password Connect

Connect can provide a vault-scoped access token and cache data locally, but the
token is not inherently read-only: the
Connect API includes create,
replace, update, and delete operations. The deployment I evaluated also meant
an API container, a sync container, a credentials JSON file, a token, a cache
volume, ports, lifecycle management, and ideally a broker so agents would not
hold a write-capable credential. This can be the right architecture for an
organization. For one desktop app, I had accidentally founded a small
secret-management civilization.

1Password MCP or Agent Tool Calls

These are useful when the task is “operate on 1Password.” They do not
transparently inject environment variables into arbitrary programs, and they
keep password-manager calls inside the agent workflow—the dependency I was
trying to remove. Useful tool, different plumbing.

The repeated failure was not that any one feature was bad. It was that a simple
question—“can this process receive the credential it needs?”—kept acquiring
quota state, approval dialogs, concurrency behavior, or a local server.

My goal was never to eliminate 1Password. It was to remove it from the
per-agent hot path. One authorization initiated by op run during application
startup is much easier to reason about than repeated context-poor interruptions.

Security Has Entered the Chat

None of this turns environment variables into an enchanted security boundary.

The plaintext option keeps credentials on disk. The op run option avoids that
extra durable copy, but depends on desktop authentication, requires starting
ChatGPT from a terminal command, and concentrates trust in that launch step.

Both options place secrets in the ChatGPT process environment.
shell_environment_policy does not authorize commands; it only filters the
variables inherited by commands that Codex launches. A command that inherits
an allowlisted credential can use it, so the configuration and the code being
run still need to be trusted. Environment variables improve delivery and
separation of concerns; they do not make an untrusted process trustworthy.

The allowlist is application-wide, not task-specific. Every task and subagent
running inside that ChatGPT process should be treated as capable of launching
a command that inherits every listed secret. It limits what can leave the
application environment; it does not decide which agent gets which credential.
True per-agent isolation would require separate processes or a credential
broker, and neither preserves the simplicity of this setup.

At startup, Codex adds the names exposed to its command environment to the
initial prompt, not their values. Instructions can therefore say, “To access
the API, use the environment variable SOME_API_KEY.” The model can refer to
that name in a command, and the program reads the value from its environment
without the value appearing in the command itself. That distinction is useful,
but it is not a force field. An agent can still dump its environment or make a
program log a credential. Avoiding those operations is part of the model's and
the tool's responsibility.

Resolved values remain in the application's runtime environment until the app
exits. Locking 1Password, changing a vault value, or removing vault access does
not rewrite the copy already injected into a running ChatGPT process. Fully
quit and relaunch through the chosen route to discard the old environment and
receive the current values.

Finally, ~/.codex/.env loading is observed rather than documented, and the
1Password-backed route depends on several products behaving as tested. A
small, non-secret startup check is worthwhile. It is nicer to discover a broken
environment before an agent begins real work and develops opinions about the
error message.

The Least-Worst Setup Wins

The useful idea was not a particular password-manager feature. It was choosing
one boundary for secret resolution and giving every agent the same boring
contract: receive a small, explicit set of environment variables.

Use ~/.codex/.env when the smallest possible setup matters most and a
protected plaintext file is acceptable. Use a launch-time injection mechanism
when the secret store should remain the only durable source and one
authentication step triggered by op run at startup is acceptable. In my
case, that mechanism is op run with ~/.codex/.env-run.

This is the least cumbersome arrangement I have found. In secret plumbing,
“least cumbersome” is practically a five-star review, not a declaration that
the problem has been solved forever.

Disclosure: I used ChatGPT Codex to help draft and edit this article. The
experience, conclusions, and final publication decision are mine.

The unresolved question is how desktop agents (and subagents) should receive a
well-attributed, least-privilege identity without persistent tokens, plaintext
files, or context-free approval windows.

Process-scoped injection solves secret delivery, not agent identity. If you
have a cleaner pattern for giving concurrent Codex agents unattended access to
credentials—especially one that avoids plaintext files, repeated approvals,
quota pressure, and a local server—I genuinely want to hear it.

Top comments (0)