| name | manage-secrets |
| description | 1Password-based secret storage workflow: vault layout, 1p:// references in .envrc, op run for local execution. Use when storing or rotating credentials with 1Password. (Other backends not covered.) |
| allowed-tools | Read, Grep, Glob, Edit, Write, Bash(op *), Bash(git *) |
| metadata | {"title":"Secrets with 1Password","category":"cli","order":0} |
Secrets with 1Password
Philosophy
Secrets never sit in plaintext in the repo, in shell history, or in long-lived
shell env. 1Password is the only source of truth. Files commit references
(op://vault/item/field); resolution happens at command time, in the child
process only, via op run or op inject. Scanning and leak response stay in
[[audit-security]]; this skill owns the storage and injection workflow.
Prerequisites
op --version
op whoami
op vault list
The 1Password desktop app unlocks biometric integration and enables the op
shell plugins (op plugin init aws|gh|...). For headless / CI, use a service
account token instead (see CI section).
The reference format
op://<vault>/<item>/[<section>/]<field>
Examples: op://Dev/aws/access_key_id, op://Dev/snowflake/credentials/password.
Vaults and items can be addressed by name or UUID; UUIDs survive renames and
are preferred in committed files for projects with churn.
Store a secret
For ad-hoc storage, prefer creating a typed item over stashing things in a Secure
Note (typed items get the right field names, masking, and shell-plugin support):
op item create --vault Dev --category "API Credential" --title myservice \
credential="$(pbpaste)"
op item create --vault Dev --category login --title github-bot \
username=ci-bot password=- url=https://github.com
op item edit myservice --vault Dev "credential[concealed]=$(pbpaste)"
After creation, get the reference (do not echo the value):
op item get myservice --vault Dev --format json | jq -r '.fields[] | "\(.label) -> op://Dev/myservice/\(.id)"'
Never paste a secret value as a literal argument. Use =- to read from
stdin, or $(pbpaste) immediately after copying from 1Password, so the value
does not enter shell history. Pair with setopt HIST_IGNORE_SPACE and prefix
the command with a space when in doubt.
Migrate a project off plaintext secrets
When you find committed secrets (or a plaintext .env):
-
Scan first (delegated to [[audit-security]]): gitleaks detect --no-banner.
-
For each finding, store the value in 1Password (typed item, correct vault),
then replace the plaintext in the file with its op:// reference:
- AWS_ACCESS_KEY_ID=AKIA...
- AWS_SECRET_ACCESS_KEY=wJalrXUt...
+ AWS_ACCESS_KEY_ID=op://Dev/aws/access_key_id
+ AWS_SECRET_ACCESS_KEY=op://Dev/aws/secret_access_key
-
Rotate every secret that was ever committed. A reference in the current
file does not undo the past leak. Rotation is mandatory, not optional.
-
Decide what stays gitignored. A .env containing only op:// references
is safe to commit and helps onboarding. A .env with any plaintext stays
gitignored (.gitignore: .env, .env.local, *.pem, *.key).
-
Verify the new file resolves end-to-end:
op run --env-file=.env -- env | grep -E 'AWS_|DB_' | sed 's/=.*/=<resolved>/'
Run commands with secrets injected
The default for invoking any command that needs secrets:
op run --env-file=.env -- <command>
op run resolves op:// refs in the env file, exports them to the child
process only, and masks them in stdout/stderr by default. Examples:
op run --env-file=.env -- terraform apply
op run --env-file=.env -- pytest -q
op run --env-file=.env -- node scripts/migrate.js
For a single secret without a file: op read "op://Dev/aws/access_key_id".
Prefer op run over op read | export because op read puts the value in the
shell's environment (and potentially history) for the rest of the session.
Config files with embedded refs
For YAML/JSON/INI/dotfile templates that need secrets baked in (a generated
config, a kubeconfig, a ~/.npmrc), use op inject:
op inject -i template.yaml -o config.yaml
chmod 600 config.yaml
Add the resolved output to .gitignore. Re-run op inject whenever the
template changes; never edit the resolved file directly.
direnv
direnv is for non-secret, per-project shell config (paths, profile names,
flags). See [[setup-devenv]] for the .envrc patterns. Do not put op read
into .envrc: it resolves at cd time, caches the secret in the shell session,
and defeats the point. Pattern:
export APP_ENV=development
export AWS_PROFILE=dev
PATH_add bin
If a command needs both direnv'd config and resolved secrets, wrap it:
exec op run --env-file=.env -- "$@"
Then dev terraform apply does the right thing.
CI / non-interactive
GitHub Actions: use
1password/load-secrets-action@v2
with a service account token stored as OP_SERVICE_ACCOUNT_TOKEN in the
repository's GitHub Actions secrets. Map each needed op:// ref to a job-scoped
env var; never echo the resolved values, never set continue-on-error on the
load step. For other CI providers, set OP_SERVICE_ACCOUNT_TOKEN and run
op run/op read the same way as locally.
Safety rules
- Never echo, log, or print resolved secrets. Not even partially. Mask before
logging (
sed 's/=.*/=<redacted>/').
- Never bake resolved values into committed files. Only
op:// references.
- Always rotate a secret that ever existed as plaintext in git history, even
briefly. A force-push does not erase forks, clones, or caches.
- Prefer typed items (API Credential, Login, Database) over Secure Notes so
fields are masked, structured, and discoverable via shell plugins.
- One vault per trust boundary (Dev, Prod, Shared, Personal). Do not store
prod secrets in the Dev vault to save a click.
- For leak scanning, response, and
.gitignore hygiene, see [[audit-security]].