DEV Community

Leon
Leon

Posted on Edited on AI-assisted

How I Stopped Codex Agents from Hammering 1Password

One afternoon, every Codex task that needed a secret stopped at once. Not one agent. All of them. A tiny distributed-systems incident, staged entirely inside my laptop.

op service-account ratelimit delivered the diagnosis with admirable bureaucratic calm: the shared daily allowance was exhausted, and roughly 13 hours remained before reset. The agents were ready to work; 1Password had clocked out for the day.

The agents already had unattended access through a service-account token limited to the vault they needed—the behavior I wanted. There was no approval loop. The problem was that every agent contacted 1Password independently, and together they exhausted a shared limit none of them could see. They had achieved impressive teamwork, just in the least useful direction possible.

That changed the question from “how should each agent retrieve a secret?” to “why should each agent retrieve it at all?”

The replacement had to support:

  • several concurrent local agents;
  • unattended work after application startup;
  • project and non-project tasks, including dynamically created working directories;
  • an explicit environment-variable allowlist;
  • preferably no plaintext copy of the credentials on disk.

The answer that finally worked was almost disappointingly architectural:

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

I then took a surprisingly comprehensive tour of ways to move one secret from A to B before settling on two practical choices:

  1. Put the actual values in ~/.codex/.env.
  2. Keep the values in 1Password and start ChatGPT with op run, using an env file that contains only 1Password references.

These are alternatives, not layers. The first is wonderfully boring but stores credentials on disk. The second keeps 1Password as the only durable secret store and concentrates the unusual part at startup. I chose the second.

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.

Why Per-Agent Vault Reads Failed

The original setup gave agents a service-account token limited to the vault they needed. That was unattended, easy to automate, and initially looked like exactly the right solution.

It also made every agent responsible for talking to 1Password. An agent could avoid repeating its own reads, but it could not share that work with the others. Every agent was responsibly optimized in private. Collectively, they were still eating the same allowance.

1Password documents a daily limit shared by all service accounts in an account. On individual and Families accounts, that allowance is 1,000 requests per 24 hours, and some CLI commands use more than one request.

That is why creating another token would not have rescued the day: the daily limit belonged to the account, not the token. It would have been a new tap connected to the same empty tank.

The incident was useful in one respect: it made the hidden dependency visible. My agents did not merely receive secrets from 1Password. They depended on 1Password being available and below quota throughout their work.

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. No broker. No daemon with a YAML file and opinions.

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 Use: Resolve References Once

I wanted 1Password to remain the source of truth without inviting it into every agent operation. The order matters: op run starts first, authenticates through 1Password desktop if needed, resolves the references, and then launches ChatGPT.

The runtime sequence has three stages:

  1. 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 startup approval if needed; op run 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 asks 1Password desktop to unlock or approve the startup 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 authentication method. The op run documentation describes how secret references are resolved and passed to a child process as environment variables.

Once ChatGPT starts, its agents inherit the resolved environment through the policy below. They do not return to 1Password individually, so parallel work no longer multiplies vault reads.

The Other Half: 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 limiting the environment 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.

The current OpenAI configuration reference documents filters as the canonical mechanism and the older include_only array as legacy. It also documents ignore_default_excludes = true as the default; I keep it explicit because the example names contain TOKEN and SECRET, and I prefer the intent to be visible beside the allowlist.

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 rather than a project directory, this works for project and non-project tasks, including tasks whose working directories are created dynamically.

What I Tried Before Settling on This

1Password has many relevant features. Each made sense on its own. The awkwardness appeared when I asked for concurrent local agents, unattended work, a clear allowlist, and no plaintext credential copy at the same time.

Service-Account Token

This was the original design, not a rejected theoretical option. It worked until parallel agents exhausted the shared daily allowance. Making each agent more careful about its own requests did not coordinate the group.

Desktop CLI Calls from Agents

This avoided the service-account quota, but put a human back into the per-agent path. In my testing, approval appeared in a separate 1Password window without clearly identifying the requesting Codex task or downstream operation. I rejected that as a steady-state design; I was not operating that way before the quota incident.

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, prompted on first read, and relocked with 1Password. In concurrent tests, fresh Codex processes alternated between receiving all variables and receiving none. Excellent suspense; poor configuration delivery.

1Password Connect

Connect can centralize and cache vault access, which directly addresses repeated reads. It also meant deploying and maintaining an API container, a sync container, a credentials JSON file, an access token, a cache volume, ports, and lifecycle management. The Connect API includes create, replace, update, and delete operations, so I would also need to verify and enforce the token's effective permissions rather than assume a read-only boundary.

That can be the right architecture for an organization. For one desktop application, it was more infrastructure than I wanted to own. I wanted credentials, not a new on-call rotation.

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 common problem was not that any one feature was bad. It was that “can this process receive the credential it needs?” kept acquiring quota state, concurrency behavior, human intervention, or a local server.

What This Solves—and What It Does Not

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.

In the build I tested, Codex added 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 remains part of the model's and 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 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 important change was not a better retry policy. It was removing 1Password from the per-agent hot path.

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

This arrangement is not perfect. It trades per-agent vault access for an application-wide runtime environment, and it does not provide a distinct, least-privilege identity to each agent. It is simply the least cumbersome setup I have found that survives concurrent work without turning secret retrieval into a distributed-systems hobby.

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, context-free approval windows, or a local platform team.

If you have a cleaner pattern—especially one that works for general-purpose desktop agents with dynamically created working directories—I genuinely want to hear how you handle it.

Top comments (0)