1Password Credential Injection
In Secrets Without Exposure you stored credentials in your OS
keychain. This module shows a stronger pattern: keep your secrets in 1Password
and let sbx inject them at the network boundary. The real secret never enters
the sandbox - the agent only ever sees a sentinel value.
Why this matters
- Your API keys stay in 1Password, the vault you already trust.
- The sandbox gets a placeholder (
proxy-managed), never the real token. - The proxy swaps the placeholder for the real key only on recognized outbound HTTPS calls - and only for domains you allow.
Prerequisites
- Docker Sandboxes (
sbx) installed - see Pre-flight Checklist - A 1Password account with the desktop app installed and unlocked
- An agent CLI (Claude, Codex, Gemini, etc.)
Step 1 - Install the 1Password CLI
macOS
brew install --cask 1password-cli
Linux (Debian/Ubuntu)
# After adding the 1Password apt repo
sudo apt install 1password-cli
Windows
winget install AgileBits.1Password.CLI
Verify:
op --version
Step 2 - Authenticate
Recommended - desktop integration:
- Open and unlock the 1Password desktop app.
- Go to Settings → Developer.
- Enable Integrate with 1Password CLI.
- (Optional) Enable biometric unlock.
Alternative - manual sign-in:
eval $(op signin)
Confirm your session:
op whoami
You should see your account URL, email, and user ID. If it says "not currently signed in," authentication failed - fix it before continuing.
Step 3 - Find your secret reference
References use the format op://<vault>/<item>/<field>.
# List your vaults
op vault list
# List items in a vault
op item list --vault Employee
# Inspect an item to find the field name (don't assume "token")
op item get "OpenAI API Key" --vault Employee
Field names matter
API Credential items store the secret in a field named credential,
not token.
Avoid special characters in references
A reference like op://Employee/OpenAI API Key (docker work)/credential
will fail:
ERROR: invalid character in secret reference: '('
Instead: rename the item to a clean handle, use its 26-character UUID, or copy the reference directly from the desktop app.
Choose an injection pattern
Pattern 1 - Persistent (reuse across future sandboxes)
set -o pipefail
op read "op://Employee/OpenAI/credential" | sbx secret set -g openai
Update an existing secret:
sbx secret rm -g openai
op read "op://Employee/OpenAI/credential" | sbx secret set -g openai
Verify:
sbx secret ls
Pattern 2 - Ephemeral (fresh per launch)
The key resolves fresh each run and is cleared on exit - never stored.
OPENAI_API_KEY="op://Employee/OpenAI/credential" op run -- sbx run codex
ANTHROPIC_API_KEY="op://Employee/Anthropic/credential" op run -- sbx run claude
Pattern 3 - Multiple providers via env file
Create .sbx-secrets.env:
ANTHROPIC_API_KEY=op://Employee/Anthropic/credential
OPENAI_API_KEY=op://Employee/OpenAI/credential
Launch with the file:
op run --env-file=.sbx-secrets.env -- sbx run claude
Add it to .gitignore
.sbx-secrets.env holds credential references. They aren't the secrets
themselves, but keep the file out of git anyway.
Step 4 - Prove containment
The sandbox should never see your real key:
sbx run --name op-test shell -d
sbx exec op-test -- bash -lc 'echo "OPENAI_API_KEY=$OPENAI_API_KEY"'
sbx rm op-test
Inside the sandbox the value shows proxy-managed (the sentinel) - never
your real key.
Step 5 - Enable injection via domain binding
Back up your config first:
cp ~/.config/sbx/credentials.yaml ~/.config/sbx/credentials.yaml.bak
Edit ~/.config/sbx/credentials.yaml and add under bindings::
openai:
discovery: []
allowedDomains:
- api.openai.com
Start a fresh sandbox (existing ones cache the old config) and verify the proxy injects the real key on an allowed outbound call:
sbx run --name inj-test shell -d
sbx exec inj-test -- bash -lc \
'curl -s -o /dev/null -w "%{http_code}\n" https://api.openai.com/v1/models \
-H "Authorization: Bearer $OPENAI_API_KEY"'
sbx rm inj-test
A 200 confirms the proxy swapped the sentinel for your real key at the
network boundary.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
You are not currently signed in |
No live op session |
eval $(op signin) or enable desktop integration; check op whoami |
Enter secret: input cannot be empty |
op read failed upstream |
Add set -o pipefail; fix the auth issue |
isn't an item in the ... vault |
Vault doesn't exist on your account | Run op vault list, use your real vault |
invalid character in secret reference: '(' |
Hand-built reference with special chars | Rename item, use UUID, or copy from app |
not injecting warning |
Service has no domain binding | Add an allowedDomains entry in credentials.yaml |
How it works
The sandbox runs a credential proxy that intercepts outbound calls. Your
secret stays in 1Password on the host and is retrieved only when needed. Inside
the container it exists only as a sentinel (proxy-managed); the proxy swaps it
for the real value at the network boundary - and only for recognized providers
with an allowedDomains binding.
✅ Checkpoint
Confirm:
op whoamishows an active 1Password sessionsbx secret lslists your stored credential (Pattern 1) or your env file resolves at launch (Patterns 2 & 3)- Inside a sandbox,
$OPENAI_API_KEYreadsproxy-managed, not your real key - An allowed outbound call returns
200
Next: controlling what the agent can reach on the network in Network Policy.