Skip to content

Use 1Password CLI for Local Development

If your local development workflow still depends on .env files full of live credentials, you are not doing configuration management, you are just normalizing secret sprawl. The 1Password CLI, op, gives you a better model: keep secrets in a vault, inject them into a child process only when needed, and let your terminal stay fast without turning your laptop into a plaintext key dump.

This post is intentionally about local development only. No CI runners, no GitHub Actions, no Kubernetes side quests. Just a developer workstation, a terminal, and a sane way to run Python, Go, PowerShell, Zsh, and Bash without leaving credentials all over the filesystem.

Recorded Demos

The command and language examples in this post are backed by VHS tapes and a deterministic local demo harness with fake secret values.

That keeps the demos rerenderable without exposing credentials for these examples.

Why the .env File Needs to Go

The problem with a local .env file is not that it is convenient. The problem is that it is convenient in all the wrong ways:

  • It is plaintext at rest
  • It gets copied into random directories and forgotten
  • It gets picked up by editors, shell sessions, and backup tools
  • It gets accidentally staged with git add .
  • It turns secret rotation into archaeology

Stop committing your API keys like it's 2012.

op run changes the model. Instead of writing secrets to disk and hoping your .gitignore and self-control both hold, you store references in a template file and let 1Password resolve them at process launch time. The target program receives environment variables in memory, just like it would with a traditional .env, but the secret values never need to live in a local file.

That is not magic, and it is not an excuse to log your config object to stdout. Environment variables can still leak if you dump them, expose them to child processes unnecessarily, or let your tooling print them. But compared to a pile of plaintext .env files, op run is a much better default.

Quick Start Reference

These are the commands you will use most often in a local workflow:

Command What it Does Example
op plugin init Emits shell integration for 1Password Shell Plugins op plugin init zsh
op run Resolves secret references and launches a process with injected environment variables op run --env-file=.env.fill -- go run ./cmd/api
op item get Fetches an item or field metadata from a vault, useful for inspection and scripting op item get "GitHub CLI" --vault Development
op read Reads a single secret reference directly, ideal for one-off lookups op read "op://Development/OpenAI/api_key"

1Password CLI quick start demo

Set Up Biometric Authentication Once

The first rule of using a password manager in the terminal is simple: do not make the terminal the place where you type your account password.

The clean setup is to let the 1Password desktop app handle unlock state and biometrics, then let the CLI piggyback on that session.

  1. Install the 1Password desktop app and the op CLI on the same machine.
  2. In the 1Password app settings, enable CLI integration.
  3. Enable Touch ID on macOS or Windows Hello on Windows in the 1Password app.
  4. Sign in once with op signin.

After that, the CLI can use the desktop app's session state. On a Mac, that usually means Touch ID instead of typing your account password into a terminal window. On Windows, it means Windows Hello instead of copy-pasting secrets like a raccoon with admin access.

This matters for both security and ergonomics. You remove password entry from terminal history and screen recordings, and you stop training yourself to paste sensitive material into shells.

Build Around a .env.fill Template

A reference file is the right compromise between zero-config startup and zero secrets on disk. Call it .env.fill, .env.op, or anything else that makes sense to you.

Example:

APP_ENV=development
GITHUB_TOKEN=op://Development/GitHub CLI/credential
OPENAI_API_KEY=op://Development/OpenAI/api_key
API_KEY=op://Development/Demo Service/api_key
REDIS_PASSWORD=op://Development/Redis/password

Then launch a program with those values injected:

op run --env-file=.env.fill -- python app.py
op run --env-file=.env.fill -- go run ./cmd/api
op run --env-file=.env.fill -- zsh

or:

op run --env-file=.env.fill -- bash
op run --env-file=.env.fill -- pwsh -NoProfile -File .\app.ps1

The file contains references, not secret values. That usually makes it safe to share inside a repo, but be honest about your naming. Vault names, item names, and field names can still reveal internal systems, customer names, or production topology. If the reference itself is sensitive, keep the file local.

Four Local Runtime Patterns

The nice part about op run is that your application code barely changes. Your runtime gets environment variables the same way it always did, but the secret source moves out of the repo.

import os

api_key = os.environ.get("OPENAI_API_KEY")
service_api_key = os.environ.get("API_KEY")

if not api_key:
    raise RuntimeError("OPENAI_API_KEY is required")

print("Python app started with injected secrets.")
print("API_KEY is set:", bool(service_api_key))

Run it with:

op run --env-file=.env.fill -- python app.py

Python 1Password CLI demo

package main

import (
    "fmt"
    "os"
)

func main() {
    apiKey := os.Getenv("OPENAI_API_KEY")
    serviceAPIKey := os.Getenv("API_KEY")

    if apiKey == "" {
        panic("OPENAI_API_KEY is required")
    }

    fmt.Println("Go app started with injected secrets.")
    fmt.Println("API_KEY is set:", serviceAPIKey != "")
}

Run it with:

op run --env-file=.env.fill -- go run .

Go 1Password CLI demo

If you want a whole shell session with secrets exported, start a subshell under op run:

op run --env-file=.env.fill -- zsh

or:

op run --env-file=.env.fill -- bash

Everything inside that child shell sees the exported variables. When you exit the shell, the session-scoped secret exposure goes with it.

You can verify the environment without printing raw secret values:

test -n "$OPENAI_API_KEY" && echo "OPENAI_API_KEY is set"
test -n "$API_KEY" && echo "API_KEY is set"

Shell 1Password CLI demo

For Windows-native workflows, run PowerShell itself under op run and consume the values via $env:...:

$ApiKey = $env:OPENAI_API_KEY
$ServiceApiKey = $env:API_KEY

if (-not $ApiKey) {
    throw "OPENAI_API_KEY is required"
}

Write-Output "PowerShell session started with injected secrets."
Write-Output ("API_KEY is set: {0}" -f [bool]$ServiceApiKey)

Launch it with:

op run --env-file=.env.fill -- pwsh -NoProfile -File .\app.ps1

If you want an interactive session instead:

op run --env-file=.env.fill -- pwsh -NoLogo

PowerShell 1Password CLI demo

Across all four patterns, the model stays the same: the process gets what it needs, and the vault stays the source of truth.

Python and Go: The Old Way vs. the New Way

The point of 1Password CLI is not that it invents a new configuration API. The point is that your application code can stay boring while your secret handling gets dramatically better.

Python Comparison

The old pattern usually looks like this:

from dotenv import load_dotenv
import os

load_dotenv()

api_key = os.getenv("OPENAI_API_KEY")

That works, but now you need:

  • A local .env file full of secrets
  • A dependency such as python-dotenv
  • Extra rules to keep secrets out of Git
  • Trust that nobody copies the file into Slack, a ticket, or a screenshot

The new pattern is simpler:

import os

api_key = os.environ.get("OPENAI_API_KEY")

Run it with:

op run --env-file=.env.fill -- python app.py

Your Python code stays standard-library simple. The security boundary moves outside the app.

Go Comparison

The old Go anti-pattern is usually one of two things: hardcoded config for local testing, or a hand-rolled .env loader that only exists because secrets were living in a file to begin with.

Here is the hardcoded version nobody wants to admit still exists:

const openAIAPIKey = "sk-live-please-do-not-do-this"

And here is the version you actually want:

apiKey := os.Getenv("OPENAI_API_KEY")

Run it with:

op run --env-file=.env.fill -- go run ./cmd/api

Go is especially nice here because its standard library already gives you the only API you need. No extra package, no secret file parser, no cleverness.

Cross-Platform Scripting Without Drama

If your team jumps between macOS, Linux, and Windows, the easiest pattern is to standardize on the same .env.fill file and wrap execution per shell.

#!/usr/bin/env bash
set -euo pipefail

exec op run --env-file=.env.fill -- "$@"

Usage:

./dev.sh go test ./...
./dev.sh python app.py
param(
    [Parameter(ValueFromRemainingArguments = $true)]
    [string[]] $Command
)

& op run --env-file=.env.fill -- @Command

Usage:

.\dev.ps1 go test ./...
.\dev.ps1 python .\app.py

That gives you one secret reference file and two thin launchers. The application code does not care which shell started it, and your local workflow stops drifting by operating system.

Read a Secret Directly When You Need One

op run is the best default for applications, but op read is perfect for one-off commands. The shell syntax differs slightly:

export GITHUB_TOKEN="$(op read 'op://Development/GitHub CLI/credential')"
gh auth status
$env:GITHUB_TOKEN = op read "op://Development/GitHub CLI/credential"
gh auth status

Use this for targeted cases. If you are launching a full application, op run is usually cleaner because it scopes the environment to a child process instead of your current shell.

Offload SSH Keys to the 1Password SSH Agent

If you want to use 1Password for Git SSH authentication too, the right tool is the 1Password SSH agent exposed by the desktop app.

A companion post covers the setup, ~/.ssh/config, verification, and migration path in detail: Use 1Password SSH Agent for SSH Keys and Git.

Pro Tip: Update Vault Items Without Leaving the Terminal

Once you start building local workflows around op, you will eventually want to update a secret without tabbing back into the desktop app. That is where op item edit earns its keep.

Example:

op item edit "OpenAI" --vault Development api_key="op://Development/OpenAI/api_key"

op item edit demo

More commonly, you will use it to update fields interactively or patch metadata after rotating a credential. The exact syntax matters less than the workflow: your terminal can become the control plane for local secrets, not just the place where you consume them.


The best local secret workflow is the one developers will actually keep using. op run hits that sweet spot: no plaintext .env files, no custom secret loader bolted into every app, and no master password typed into a shell.

Keep your application code boring, and put the secret complexity in the password manager. Your future self, your incident response notes, and the poor soul who inherits your dotfiles will all be much less annoyed.