A Clean, Low-Leakage API Key Workflow for Local Dev

Integrating 1Password Environments into a practical engineering workflow

Cole Mei

Tutorial

2055 Words … ⏲ Reading Time:9 Minutes, 20 Seconds

2026-02-03 09:35 +0000


Introduction

In 2026, local development commonly involves multiple API providers and multiple identities. It is normal to switch between work, personal, and research contexts within the same machine and even the same terminal session.

Despite this, API key management during local development is still largely informal. .env files are copied around, shell environments silently accumulate secrets, and key rotation is postponed because it is operationally painful.

The core problem is not “where to store API keys”. Most developers already know the answer is “a password manager or secret manager”. The real problems are subtler.

These issues create friction and, more importantly, increase the risk of accidental leaks. The idea of separating config from code traces back to The Twelve-Factor App, which popularized environment variables over config files. That was a meaningful step forward — but it led to .env files storing credentials in plaintext, protected by nothing more than a .gitignore entry.

What we need is not just secure storage, but explicit lifecycle control.

This post documents a workflow I use daily to manage API keys locally in a way that is explicit, low-leakage, and scalable. The focus is not on tools themselves, but on how to integrate them into a coherent engineering design.


Design principles

Before choosing tools, I defined a few constraints:

  • Secrets should have a single source of truth. No duplication, no secondary copies.
  • Secrets should not live longer than necessary. A key used for one command should not survive in the shell afterwards.
  • Projects should be isolated by default. Entering a directory should not implicitly grant access to unrelated credentials.
  • The workflow must remain ergonomic. Security that disrupts daily work will eventually be bypassed.

These principles guided all design decisions below.


High-level architecture

At a high level, the system separates responsibility into three layers:

SECRET LIFECYCLE ARCHITECTURE1Password EnvironmentsSingle source of truth · Encrypted · SyncedDistribution LayerLocal .env (FIFO) · op run · SDK (Go / Python / JS)Project-Scoped.env FIFO + direnvSecrets live whileyou're in the directoryCommand-Scopedop run --environmentSecrets live forone command onlyProgrammaticSDK / op env readSecrets fetchedon demand in codeLifecycle GuaranteesNever on disk as plaintext · Never in Git · Never in shell history · Scoped lifetimeStorageDistributionConsumption
Figure 1 — Three-layer architecture: 1Password owns the secrets, the distribution layer mediates access, and each consumption pattern enforces a different lifetime scope.

Long story short:

1Password owns the secrets. The distribution layer provides multiple mechanisms to surface them — FIFO-mounted .env files, the CLI, or native SDKs. Different consumption patterns then control when and how long those secrets are available.

There are three supported usage patterns. Keeping them distinct is intentional.


Pattern 1: Project-scoped mount (primary workflow)

This pattern answers the question: “Which credentials does this project use?”

A 1Password Environment (e.g. Dev-Work) is mounted directly to the project root as ./.env. Under the hood, 1Password creates a UNIX named pipe (FIFO) — not a regular file. Your existing dotenv libraries read it transparently, but no plaintext secrets ever land on disk. The mounted file remains available as long as 1Password is running and locks automatically when 1Password locks.

How the FIFO works. When your app reads the .env path, 1Password intercepts the read via the named pipe and streams the secret values directly to the process through a UNIX pipe. The data passes through memory only. Because the mount is not a regular file, it won’t be staged, committed, or pushed by Git — even without a .gitignore entry.

Step 1: Create and mount an Environment

In the 1Password desktop app, navigate to Developer → View Environments. Create environments such as Dev-Work and Dev-Personal, keeping variable names identical across environments.

Under the Destinations tab, select Configure destination for “Local .env file”, then choose the project root as the mount path:

project/.env

You can import an existing .env file directly, or add key-value pairs manually. 1Password will begin serving the FIFO immediately.

Git housekeeping. If you already have a plaintext .env tracked by Git at that path, delete it and commit the removal before mounting. Otherwise git status may report the FIFO as a changed file. The contents can never actually be staged, so your secrets remain safe — but the noise is annoying.

Step 2: Install and enable direnv

direnv is optional but strongly recommended. It manages shell-level visibility: enter a directory and variables appear; leave and they disappear.

brew install direnv
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc
source ~/.zshrc

Step 3: Define a global loader for .env

Create a helper that tells direnv how to load project secrets:

mkdir -p ~/.config/direnv/lib

cat > ~/.config/direnv/lib/1password_env.sh <<'SH'
use_1password_env() {
  # Ensure dotenv is available
  type -t dotenv >/dev/null 2>&1 || {
    log_error "direnv stdlib 'dotenv' not available"
    return 1
  }

  # Load project .env (FIFO or regular file)
  if [ -e .env ]; then
    dotenv .env
  else
    log_status "no .env found in this directory"
  fi

  # Optional local overrides (non-secret config)
  type -t dotenv_if_exists >/dev/null 2>&1 && dotenv_if_exists .env.local
}
SH

This helper never stores secrets. It only consumes .env if it exists.

Step 4: Project root .envrc

In the project root:

# .envrc
use 1password_env

Then allow it once:

direnv allow

From now on, entering the project directory automatically injects the secrets; leaving it removes them.

Subdirectories and runtime environments

In real projects, subdirectories often need their own .envrc — for example to activate a conda environment. Because direnv executes only the nearest .envrc, inheritance must be explicit:

# sub-project/.envrc
source_up
source ~/miniforge3/etc/profile.d/conda.sh
conda activate "$(pwd)/.conda"

This ensures secrets loaded at the project root remain available while runtime configuration stays local.


Pattern 2: Command-scoped injection (one-off usage)

This pattern answers a different question: “I just want to run this command once with specific credentials.”

Sometimes there is no project context. You just want to test an API, run a script, or make a quick request. For this case, inject secrets for a single command using op run. Secrets exist only for the lifetime of that process.

Using op run with Environment IDs

Since February 2026, op run supports loading variables directly from a 1Password Environment using the --environment flag. This is cleaner than maintaining separate FIFO mount files for ad-hoc use:

# Find your Environment ID in 1Password:
# Developer → View Environments → Manage environment → Copy environment ID

op run --environment env_1234567890abcdef -- curl -s https://api.openai.com/v1/models

Stdout masking. By default, op run monitors stdout and stderr and automatically conceals any secret values that appear in output. If you need to see the actual values (e.g. for debugging), pass the --no-masking flag. In production scripts, leave masking on.

Using central FIFO mounts (alternative)

If you prefer not to pass Environment IDs, you can maintain central FIFO mounts and reference them by path:

~/.envs/dev-work.env
~/.envs/dev-personal.env

Then inject via:

op run --env-file ~/.envs/dev-work.env -- <command>

Convenience wrapper

For daily use, a small shell function keeps things fast:

# ~/.zshrc
dev-env() {
  local which="$1"; shift
  local env_id

  case "$which" in
    work)     env_id="env_work_1234567890" ;;
    personal) env_id="env_personal_abcdef01" ;;
    *)
      echo "usage: dev-env {work|personal} <command...>"
      return 1
      ;;
  esac

  op run --environment "$env_id" -- zsh -l -c "$@"
}

Usage:

dev-env work 'curl -s https://api.openai.com/v1/models'

Secrets exist only for the lifetime of that command and never pollute the shell.


Pattern 3: Programmatic SDK access

For applications that need secrets at runtime — not just in shell environments — 1Password now offers native SDKs in Go, Python, and JavaScript. This is particularly useful for scripts, internal tools, or CI pipelines that need to fetch credentials on demand.

Reading an Environment with the CLI

The op environment read command returns all variables from an Environment as key-value pairs:

# Read all variables from an Environment
op environment read env_1234567890abcdef

# Pipe to other tools
op environment read env_1234567890abcdef | grep API_KEY

Reading an Environment with the Python SDK

from onepassword import Client

client = Client.authenticate(
    integration_name="my-script",
    integration_version="0.1.0"
)

# Fetch all variables from a 1Password Environment
env_id = "env_1234567890abcdef"
response = client.environments.get_variables(env_id)

for var in response.variables:
    print(f"{var.name}={'*****' if var.hidden else var.value}")

Local authentication. The SDK can authenticate through the 1Password desktop app using biometrics or your account password. No service account tokens needed for local development. This is a human-in-the-loop approval model — 1Password prompts you for consent, then the SDK receives a scoped session.

When to use which pattern

PatternBest forSecret lifetime
Project mount (FIFO + direnv)Day-to-day development in a specific projectWhile in directory
Command injection (op run)One-off API calls, quick tests, CI stepsSingle command
SDK / CLI readInternal tools, automation, Python/Go/JS appsApplication-controlled

Why direnv is optional but powerful

Nothing in this design requires direnv. Projects can read .env directly, command-scoped injection works independently, and the SDK pattern bypasses the shell entirely.

However, direnv dramatically improves the developer experience by managing shell-level visibility:

  • Enter directory → variables appear.
  • Leave directory → variables disappear.

This keeps the shell clean while remaining convenient.

The important point is that direnv is a lifecycle helper, not a secret manager. It never stores secrets; it only executes logic. The .envrc file itself can be safely committed to version control because it contains no secrets — only the instruction to load .env.


Known limitations & caveats

The 1Password Environments feature is still in beta, and there are some sharp edges worth knowing about.

Concurrency

FIFO-mounted .env files are not designed for concurrent access. If multiple processes try to read the FIFO simultaneously — for example, an IDE and a terminal session — only the first reader succeeds. Others may hang or return empty data.

Practical impact. If you have your .env open in an IDE (some will auto-read it for IntelliSense), other applications may fail to read the same file. Close the IDE’s file handle or use op run --environment as a workaround for the second consumer.

Platform support, limits, and desktop app dependency

Local .env file mounts are currently supported on Mac and Linux only. Windows support is being explored but is not yet available; on Windows, use op run --environment or the SDK instead. On all platforms, you can have up to 10 enabled local .env file mounts per device. For most workflows this is sufficient, but if you manage many microservices locally, you may need to combine some Environments or lean on the CLI/SDK patterns. All of this also depends on the 1Password desktop app being running and unlocked — these mechanisms are strictly developer workstation features, not something to run on servers or headless CI. For CI/CD, use 1Password Service Accounts instead.

AI agent integration

If you use AI-assisted IDEs like Cursor or GitHub Copilot, 1Password provides an agent hook that validates your FIFO mounts before the agent executes shell commands. You can configure which files to validate using a .1password/environments.toml file at the project root:

# .1password/environments.toml
mount_paths = [".env"]

The hook prevents command execution if any required .env FIFO is missing or disabled, which keeps the agent from running against misconfigured environments.


Final notes

Once this workflow is in place, the benefits compound quickly. Key rotation becomes straightforward: rotate in 1Password, and consumers pick it up on the next read, with no find-and-replace across scattered files. Environment switching is explicit and reversible, whether you remount a different Environment for a project or change the Environment ID in a wrapper command. Just as importantly, secrets avoid the common leak paths: FIFO mounts do not write plaintext to disk, op run helps conceal secret values in command output, and direnv removes variables when you leave a project directory.

The operational impact is bigger than just security posture. Onboarding gets simpler because you share Environment access instead of sending .env files around, and auditing improves because access logs exist by default. Over time, the mental overhead drops: you stop “remembering where keys are” and start relying on a system with explicit lifetimes.

At a design level, this is the main takeaway: treat local secret handling as an engineering system, not a collection of ad-hoc habits. The specific tools can change, but the principles hold — one source of truth, scoped injection, and clear lifecycle control. If you already use 1Password Environments, this workflow aligns with the feature’s strengths; if you do not, the same pattern still applies to other secret managers.