AWS credential management via 1Password. Spawn shells and run commands with AWS credentials fetched directly from your 1Password vault.
- Shell sessions with AWS credentials injected as environment variables
- Exec mode to run single commands with credentials
- MFA support with automatic TOTP retrieval from 1Password, or from any external command (
pass-otp,rbw,ykman, KeePassXC, …) - Flexible sources — keys can come from a 1Password item or straight from
~/.aws/credentials - Rate-limit protection — auth failures trigger a local cooldown so retries don't hammer the upstream credential source (1Password,
pass/gpg-agent, etc.) - Per-directory defaults — a
.vopfile naming a profile lets you drop the profile argument inside a repo, or across a whole tree of them - Key rotation via
vop rotate - Dual backend — each profile can independently use:
- CLI backend: uses the
opCLI binary (interactive auth, biometric) - SDK backend: uses the 1Password Go SDK with a service account token (no
opbinary needed)
- CLI backend: uses the
- No external dependencies for SDK-backend profiles — the AWS SDK for Go v2 replaces the
awsCLI, so the binary is fully self-contained - Credential files in a secure per-user directory (
/run/user/$UID/vop/on Linux,$TMPDIR/vop-$UID/vop/on macOS), cleaned up on exit - Backward compatible with Vaulted — sets both
VOP_PROFILEandVAULTED_ENV
curl -fsSL https://raw.githubusercontent.com/NodeSpy/vop/main/install.sh | bashThe installer auto-detects available package managers and offers the best install method:
- Homebrew -- if
brewis in your PATH, offersbrew install vop - AUR -- on Arch Linux with
yayorparu, offers thevop-binAUR package .deb-- on Debian/Ubuntu (and derivatives), offersapt installof the official.deb- Binary download -- always available as a fallback, downloads to
/usr/local/bin
When multiple methods are available, you'll be prompted to choose. To force a specific method or override the install directory:
# Force a specific method
VOP_METHOD=brew curl -fsSL https://raw.githubusercontent.com/NodeSpy/vop/main/install.sh | bash
VOP_METHOD=aur curl -fsSL https://raw.githubusercontent.com/NodeSpy/vop/main/install.sh | bash
VOP_METHOD=deb curl -fsSL https://raw.githubusercontent.com/NodeSpy/vop/main/install.sh | bash
VOP_METHOD=binary curl -fsSL https://raw.githubusercontent.com/NodeSpy/vop/main/install.sh | bash
# Custom install directory (binary method only)
VOP_INSTALL_DIR=~/.local/bin curl -fsSL https://raw.githubusercontent.com/NodeSpy/vop/main/install.sh | bashbrew tap NodeSpy/vop https://github.com/NodeSpy/vop
brew install vopTo upgrade:
brew upgrade vopWith an AUR helper:
yay -S vop-bin
# or
paru -S vop-bin# amd64
curl -fsSLO "https://github.com/NodeSpy/vop/releases/latest/download/vop_$(curl -fsSL https://api.github.com/repos/NodeSpy/vop/releases/latest | grep -oP '"tag_name": *"v\K[^"]+')_amd64.deb"
sudo apt install ./vop_*.debapt install resolves 1password-cli (recommended) and integrates with apt list --installed / apt remove. Upgrade on each release with sudo apt install --only-upgrade vop or re-run the install one-liner.
Download the binary for your platform from the releases page, make it executable, and place it in your PATH:
chmod +x vop-linux-amd64
sudo mv vop-linux-amd64 /usr/local/bin/vopAvailable binaries:
| Platform | Binary |
|---|---|
| Linux x86_64 | vop-linux-amd64 |
| Linux ARM64 | vop-linux-arm64 |
| macOS x86_64 | vop-darwin-amd64 |
| macOS ARM64 (Apple Silicon) | vop-darwin-arm64 |
Requires Go 1.25+ and CGO (for the 1Password SDK):
git clone https://github.com/NodeSpy/vop.git
cd vop
go build -o vop .
sudo mv vop /usr/local/bin/If installed via the install script or manual download:
vop updateThis prompts for confirmation, then downloads and installs the latest release.
If installed via Homebrew, use brew upgrade vop instead.
curl -fsSL https://raw.githubusercontent.com/NodeSpy/vop/main/install.sh | bash -s -- uninstallThis detects all vop installations on your system (Homebrew, AUR, .deb, standalone binary) and offers to remove them. If you have both a package-managed install and a standalone binary (e.g. from a previous one-liner install), it will warn about the conflict and default to removing only the standalone binary.
To uninstall a specific method manually:
# Homebrew
brew uninstall vop && brew untap NodeSpy/vop
# AUR
yay -R vop-bin
# Debian/Ubuntu (.deb)
sudo apt remove vop
# Standalone binary
sudo rm /usr/local/bin/vop# Check prerequisites
vop check
# Add a profile (CLI backend — uses op CLI)
vop add prod
# Add a profile (SDK backend — uses service account token)
vop add ci
# List profiles
vop ls
# Open a shell with AWS credentials
vop prod
# or equivalently:
vop shell prod
# Run a single command
vop exec prod aws s3 ls
# or with -- separator:
vop exec prod -- aws s3 ls
# vop's own messages go to stderr, so the command's stdout stays parseable
vop exec prod -- aws ec2 describe-instances --output json | jq .
# Test credentials
vop test prod
# Rotate access keys
vop rotate prod
# Show profile configuration
vop show prod- CLI backend profiles: 1Password CLI (
op) - SDK backend profiles: no external tools needed (service account token configured in profile)
Run vop check to verify your setup.
Profiles are stored in ~/.config/vop/profiles.json (chmod 600). This file is intentionally excluded from version control since it may contain service account tokens.
| Field | Required | Description |
|---|---|---|
op_account |
CLI only | 1Password account (e.g. my.1password.com) |
op_item |
1P source only | 1Password item name (e.g. AWS - prod) |
op_vault |
SDK only | 1Password vault name (for op://vault/item/field refs) |
service_account_token |
SDK only | 1Password service account token |
aws_credentials_profile |
File source | Profile name in ~/.aws/credentials to read/rotate keys against (see below) |
aws_credentials_file |
No | Override path for the shared credentials file (defaults to ~/.aws/credentials) |
credentials_command |
Command source | Shell command whose stdout is the AWS keys (2-3 lines: access key, secret, optional session token) |
credentials_write_command |
No | Shell command that receives new keys on stdin (line 1: access key, line 2: secret). Enables vop rotate for command sources. |
mfa_totp_item |
No | 1Password item that holds the MFA TOTP |
mfa_totp_command |
No | Shell command whose stdout is the current TOTP (see below) |
mfa_serial |
No | Explicit MFA device ARN (overrides file/1P/IAM lookup) |
iam_username |
No | IAM username (for key rotation) |
description |
No | Profile description |
role_arn |
Role only | ARN of an IAM role to sts:AssumeRole into |
source_profile |
Role only | Name of another vop profile whose creds are used to assume the role |
role_session_name |
No | Session name passed to sts:AssumeRole (defaults to vop) |
external_id |
No | ExternalId condition value for role assumption |
Commands that take a profile can find one on their own, so you don't have to
name it every time. A .vop file holds a single profile name:
echo tap > ~/work/tap-infra/.vopFrom anywhere in that repo, the profile is implied:
cd ~/work/tap-infra/modules/vpc
vop exec -- terraform plan
# >>> Profile: tap (from ~/work/tap-infra/.vop)vop looks for .vop in the current directory and each parent, stopping at
$HOME or the filesystem root. The nearest file wins, so a subdirectory can
override the default a parent set. Blank lines and # comments are ignored.
Repository roots are not boundaries, so the search keeps climbing past a
directory containing .git. If your checkouts are grouped by client or company,
one file at the top covers all of them:
echo acme-prod > ~/Projects/acme/.vop
# applies in ~/Projects/acme/api, ~/Projects/acme/infra, …~/.vop works the same way as a personal fallback for everything else.
A .vop inside a repo still takes precedence, since the nearest file wins.
To exempt a single repo from an inherited default, drop an empty (or
comments-only) .vop at its root — a file with no profile name in it means "no
default here" and stops the search rather than inheriting a parent's:
echo '# no default: name the profile explicitly here' > ~/Projects/acme/sandbox/.vopHighest first:
- An explicit argument —
vop exec tap -- … VOP_PROFILE(exported byvop shell), thenAGENT_DECK_VOP_PROFILE- The nearest
.vopfile - The interactive picker, for commands that have one
The environment deliberately beats .vop. A .vop file is a property of
wherever you happen to be standing — often checked in, often written for a whole
tree of repos — and letting it silently redirect credentials to a different AWS
account would defeat the point of pinning a session to one profile. When a
.vop file is passed over for that reason, vop says so:
!!! ~/work/tap-infra/.vop requests profile 'tap' — ignored, this session is
pinned to 'ednition' by AGENT_DECK_VOP_PROFILE
A repo-specific .vop is safe to commit (it names a profile, not a secret); the
alternative is .git/info/exclude. A .vop above your checkouts isn't inside
any repo, so there's nothing to ignore.
For vop exec, the -- separator is required when you omit the profile, so the
two forms stay unambiguous:
vop exec tap -- aws s3 ls # explicit profile
vop exec -- aws s3 ls # profile from env or .vop
vop exec aws s3 ls # 'aws' is read as the profile name — not what you want.vop resolution depends on where you're standing, so a script or agent that
runs one command in a repo and the next one in /tmp loses the profile. vop profile prints what resolves here — the bare name on stdout, its origin on
stderr — so you can pin it once and stop depending on the directory:
$ vop profile
>>> Profile: ednition (from ~/Projects/ednition/.vop) # stderr
ednition # stdout
export VOP_PROFILE=$(vop profile -q) # pin for the rest of the session
eval "$(vop profile --export)" # same thing, in one stepIt exits non-zero when nothing resolves, so $(vop profile) fails loudly
instead of quietly expanding to an empty profile.
Each profile independently chooses its backend:
- No
service_account_token= CLI backend (usesopbinary, interactive auth) - Has
service_account_token= SDK backend (pure Go, no external binaries)
By default, vop reads AWS keys from a 1Password item. Three optional fields let you source the base keys and/or the TOTP from somewhere else:
aws_credentials_profile— read the keys straight from~/.aws/credentialscredentials_command— read the keys from any shell command (e.g.pass aws/prod)mfa_totp_command— read the TOTP from any shell command (e.g.pass otp aws/prod,ykman,rbw,keepassxc-cli)
These are independent — mix and match freely. See the Combining sources matrix below.
Set aws_credentials_profile to the section name in your shared credentials file. vop will read the access key and secret from there instead of 1Password, and vop rotate will write the new keys back to the same section (preserving comments, other profiles, and file permissions).
{
"profiles": {
"ednition": {
"aws_credentials_profile": "ednition",
"mfa_totp_command": "pass otp aws/ednition"
}
}
}vop discovers mfa_serial automatically from ~/.aws/config (or ~/.aws/credentials, wherever the AWS CLI put it) — no need to duplicate it in the vop profile. Override via the mfa_serial field only if you need something different from the AWS CLI's view. Override the credentials file path with aws_credentials_file if it isn't at ~/.aws/credentials.
credentials_command runs an arbitrary shell command and parses its stdout as the base AWS keys. vop uses sh -c, inherits your environment (so gpg-agent / DBus / TTY prompts keep working), and expects:
- Line 1:
aws_access_key_id - Line 2:
aws_secret_access_key - Line 3 (optional):
aws_session_token
Blank lines and lines starting with # are ignored, so you can annotate your entries.
The recommended encrypted-at-rest setup: both the access-key pair and the TOTP live in pass, guarded by GPG. gpg-agent's cache TTL keeps you from re-entering the passphrase on every call, and nothing sensitive sits on disk in plaintext.
# 1. Store the access key + secret as a two-line pass entry.
pass insert -m aws/prod
# Editor opens; paste:
# AKIAIOSFODNN7EXAMPLE
# wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# 2. Store the TOTP seed (via pass-otp).
pass otp insert aws/prod
# Paste the otpauth:// URI or the base32 secret.
# 3. Point vop at both.Then in ~/.config/vop/profiles.json:
{
"profiles": {
"prod": {
"credentials_command": "pass aws/prod",
"mfa_totp_command": "pass otp aws/prod",
"mfa_serial": "arn:aws:iam::123456789012:mfa/daniel"
}
}
}That's it — vop prod unlocks pass (once per gpg-agent window) and hands you an MFA-authenticated shell. No op CLI, no 1Password service account, no plaintext keys on disk.
Any command that produces the 2-3-line format works directly. For tools that don't, wrap them:
# ~/.local/bin/aws-from-1p — mixing 1Password items with pass-otp
#!/bin/bash
op read "op://Personal/AWS $1/access key id"
op read "op://Personal/AWS $1/secret access key""credentials_command": "aws-from-1p prod"vop rotate works for command sources if you also configure credentials_write_command — a command that accepts the new keys on stdin (line 1: access key, line 2: secret) and updates the source. For pass:
{
"prod": {
"credentials_command": "pass aws/prod",
"credentials_write_command": "pass insert -m -f aws/prod",
"mfa_totp_command": "pass otp aws/prod",
"mfa_serial": "arn:aws:iam::123456789012:mfa/daniel"
}
}Now vop rotate prod creates a new IAM key, pipes aws_access_key_id\naws_secret_access_key\n into pass insert -m -f aws/prod (which overwrites the entry), verifies via STS with the new key + a fresh TOTP, and deletes the old key. On any failure, vop rolls the pass entry back to the previous keys automatically.
If credentials_write_command isn't set, vop rotate refuses cleanly and points you here.
Set mfa_totp_command to a shell command that prints the current 6-digit code. vop runs it via sh -c, inherits your environment (so gpg-agent, DBus, and TTY prompts keep working), and uses the trimmed last non-empty line as the code. Takes precedence over mfa_totp_item when both are set.
Examples:
"mfa_totp_command": "pass otp aws/prod"
"mfa_totp_command": "rbw get --raw code aws-prod"
"mfa_totp_command": "ykman oath accounts code -s aws-prod"
"mfa_totp_command": "keepassxc-cli show -a otp ~/vault.kdbx aws-prod"The fields are independent — mix and match freely:
| Base keys | TOTP | Config |
|---|---|---|
| 1Password | 1Password | (default) — op_item, mfa_totp_item |
| 1Password | External | op_item + mfa_totp_command |
| AWS file | 1Password | aws_credentials_profile + mfa_totp_item |
| AWS file | External | aws_credentials_profile + mfa_totp_command |
| Command | 1Password | credentials_command + mfa_totp_item |
| Command | External | credentials_command + mfa_totp_command |
Precedence when multiple sources are set: credentials_command > aws_credentials_profile > op_item. When a profile has no 1P interaction at all, vop skips the op sign-in entirely — you can use these profiles without the op CLI installed.
vop addandvop editdon't prompt for these fields yet; add them by editing~/.config/vop/profiles.jsondirectly.
The SDK backend requires a 1Password service account token. To create one:
- Sign in to your 1Password account at my.1password.com
- Go to Developer in the sidebar (or Settings > Developer depending on your plan)
- Under Infrastructure Secrets, click Service Accounts
- Click Create Service Account
- Give it a name (e.g.
vop) - Grant it access to the vault(s) that contain your AWS credential items -- it needs read and write permissions (write is needed for key rotation and migration)
- Copy the service account token (starts with
ops_)
Then use it when adding or migrating a profile:
# When adding a new profile
vop add prod
# Answer "yes" to "Use a service account token?" and paste the token
# When migrating from Vaulted
vop migrate prod
# Answer "yes" to "Use a service account token?" and paste the tokenThe token is stored in ~/.config/vop/profiles.json which is chmod 600 and should not be committed to version control.
| Command | Description |
|---|---|
vop ls |
List configured profiles |
vop shell [profile] |
Open a shell with AWS credent 83C9 ials (picker if no arg) |
vop <profile> |
Shorthand for vop shell <profile> |
vop exec [profile] [--] <cmd> |
Run a command with credentials (profile from .vop/env if omitted) |
vop profile |
Print the profile that resolves here (name on stdout, origin on stderr) |
vop refresh [profile] |
Refresh credentials in an active shell |
vop cred-process <profile> |
Output credentials for AWS credential_process |
vop add [profile] |
Add a new profile |
vop edit <profile> |
Edit an existing profile |
vop rm <profile> |
Remove a profile |
vop show [profile] |
Show profile configuration |
vop dump [profile] |
Dump raw credentials |
vop test [profile] |
Test the credential source and validate with STS |
vop cat |
Print profiles config (redacts tokens) |
vop rotate [profile] |
Rotate IAM access keys (picker if no arg) |
vop unlock <profile> |
Clear a profile's failure cooldown after a fixed auth issue |
vop migrate [vault] |
Migrate from Vaulted |
vop skill |
Print the AI-agent instructions for this vop version |
vop skill install |
Install the agent skill stub (~/.claude/skills/vop/) |
vop check |
Check prerequisites and configuration |
vop version |
Print version information |
vop update |
Update to the latest version |
Useful when vop is driven by a script, CI job, or AI agent — a retry loop against a locked credential store is the fastest way to earn a cooldown.
| Code | Meaning | Should you retry? |
|---|---|---|
0 |
Success | — |
1 |
Generic failure | Maybe — read the error |
11 |
Profile is inside its failure cooldown | Not until the wait elapses |
12 |
Credential source is locked or couldn't prompt | No — a human must unlock it |
A locked source (locked gpg-agent, 1Password not signed in, a pinentry prompt with no terminal to show it on) is reported separately from an auth failure: it never reached the upstream provider, so it doesn't escalate the rate-limit backoff, and the error names the command to run to unlock it.
>>> mfa_totp_command failed (exit status 2): pass otp aws/prod
gpg: decryption failed: No secret key
hint: the credential store looks locked. Run `pass otp aws/prod` in an
interactive terminal to unlock it, then retry
vop carries its own agent instructions. vop skill prints them — how profiles
resolve, how to run commands, the safety policy, what each exit code means — and
because the text lives in the binary, it always describes the vop that's actually
installed. It also names the profile resolved in the current directory, so an
agent doesn't have to work that out:
$ vop skill | head -15
# vop — AWS credentials via 1Password
vop v0.8.0. These are the authoritative instructions for this installed
version...
## Which profile
Resolved in this directory: **`ednition` (from ~/Projects/ednition/.vop)**To make an agent aware of vop in the first place, install the skill stub:
vop skill install # ~/.claude/skills/vop/SKILL.md
vop skill install --project # ./.claude/skills/vop/SKILL.md
vop skill install --dir <path> # anywhere else
vop skill status # missing | current | outdated | unmanagedThe stub is deliberately tiny: it states the non-negotiables (credentials come
from vop, -- is required, stop on exit 11/12, ask before mutating anything)
and tells the agent to run vop skill for the rest. That's the whole point —
the file on disk carries no version-specific detail, so upgrading vop never
leaves a stale skill behind. Nothing to re-sync, and vop skill install only
needs re-running if the stub itself changes (vop check says when).
Installing refuses to overwrite a SKILL.md vop didn't write, so a hand-rolled
skill is safe; pass --force to replace it.
If you currently use Vaulted, vop can import your profiles:
# Migrate a single vault
vop migrate <vault-name>
# Interactive — lists all vaults and offers to migrate them
vop migrateThis unlocks the Vaulted vault, extracts the AWS credentials (access key, secret key, MFA serial, region), and then asks which 1Password backend to use:
- Service account token (SDK): provide your token and vault name -- no
opCLI needed - op CLI: select your 1Password account and vault interactively
It then creates a new 1Password item containing those credentials and sets up a vop profile pointing to it. If the vault has MFA configured, you'll be prompted to link an existing 1Password TOTP item.
MIT