Use when SSHing into remote machines, looking up credentials, renaming Tailscale devices, or accessing services on the tailnet. Symptoms - SSH auth failures, too many authentication failures, command not found tailscale, need to find credentials for remote machines.
Infra config: Machine names, IPs, and credentials come from
~/.claude/infra.toml. See~/.claude/infra.toml.examplefor the template. Load vars in scripts withsource ~/.claude/skills/lib/infra-load.sh.
Joe's infrastructure uses 1Password CLI (op) for credential management and Tailscale for networking between machines. SSH is configured to use the 1Password agent, which introduces specific quirks that must be handled correctly.
The tailscale command is available two ways:
| Method | Path | Notes |
|---|
| App Store app | /Applications/Tailscale.app/Contents/MacOS/Tailscale | Always available on macOS machines. tailscale ssh NOT available on App Store builds. |
| Homebrew CLI | tailscale (in PATH) | Installed on $INFRA_DEV_HOST. Provides tailscale ssh if using standalone build. |
QUIRK: The bare tailscale command may not be in PATH on remote macOS machines. Always try the full app path as fallback:
# Try short form first, fall back to app path
tailscale status 2>/dev/null || /Applications/Tailscale.app/Contents/MacOS/Tailscale status
# View all devices
tailscale status
# Rename a device (run ON that device)
tailscale set --hostname=new-name
# Get detailed info (tailnet name, self info)
tailscale status --json | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('MagicDNSSuffix',''))"
Joe has two 1Password accounts:
| Account | Use |
|---|---|
$INFRA_OP_PERSONAL | Personal — SSH keys, machine credentials, personal services |
toptal.1password.com | Work — work-related credentials |
IMPORTANT: Always specify --account=$INFRA_OP_PERSONAL for infrastructure/machine credentials. The default account may be the work one.
| Vault | Contents |
|---|---|
Personal | General credentials, some SSH keys |
cli | SSH keys, API credentials, programmatic access items |
QUIRK: Items may be in either vault. If you can't find an item, search without vault filter first:
op item list --account=$INFRA_OP_PERSONAL
Then check which vault it's in:
op item get "<item-name>" --account=$INFRA_OP_PERSONAL --format=json | python3 -c "import sys,json; print(json.load(sys.stdin).get('vault',{}).get('name',''))"
Search by keyword — item titles don't always match device names:
op item list --account=$INFRA_OP_PERSONAL --format=json | python3 -c "
import sys,json
items=json.load(sys.stdin)
for i in items:
title = i.get('title','').lower()
if any(k in title for k in ['keyword1','keyword2']):
print(f\"{i['title']} ({i.get('category','')}) [{i.get('id','')}]\")
"
CRITICAL QUIRK: op read outputs SSH keys in a format that ssh -i rejects ("invalid format" or "Load key: invalid format"). You MUST extract keys via JSON:
# ❌ WRONG — produces invalid key format
op read "op://vault/item/private key" > /tmp/key
# ✅ CORRECT — extract via JSON with --reveal
op item get "<item-id>" --account=$INFRA_OP_PERSONAL --reveal --format=json | python3 -c "
import sys, json
item = json.load(sys.stdin)
for f in item.get('fields', []):
if f.get('label') == 'private key':
print(f.get('value', ''))
" > /tmp/key
chmod 600 /tmp/key
Always clean up temp key files after use:
rm -f /tmp/key
The 1Password SSH agent offers ALL stored SSH keys to the server. If you have many keys (Joe has 5+), the server rejects the connection before finding the right one.
Fix: Bypass the 1Password agent and specify the key directly:
ssh -o IdentitiesOnly=yes -o IdentityAgent=none -i /path/to/extracted/key user@host "command"
The global SSH config routes all connections through 1Password:
Host *
IdentityAgent "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
This is why -o IdentityAgent=none is needed when using a specific key file.
For machines that use password auth, use sshpass (installed via Homebrew):
sshpass -p 'password' ssh -o IdentitiesOnly=yes -o IdentityAgent=none user@host "command"
For commands requiring sudo:
sshpass -p 'password' ssh -o IdentitiesOnly=yes -o IdentityAgent=none user@host "sudo -S command" <<< 'password'
| Item | Vault | Use |
|---|---|---|
age-key-dotfiles | cli | age private key for sops secrets in the dotfiles repo — auto-restored to ~/.config/sops/age/keys.txt by setup-secrets.sh if missing |
To manually restore or back up the age key:
# Restore key from 1Password
op item get "age-key-dotfiles" --account=$INFRA_OP_PERSONAL --fields notesPlain > ~/.config/sops/age/keys.txt
chmod 600 ~/.config/sops/age/keys.txt
# Save updated key to 1Password (creates new item)
op item create --category "Secure Note" --title "age-key-dotfiles" \
"notesPlain=$(cat ~/.config/sops/age/keys.txt)" --account=$INFRA_OP_PERSONAL
| Machine | Tailscale IP | SSH User | Auth Method | 1Password Item |
|---|---|---|---|---|
$INFRA_MAC_MINI_HOST | (see infra.toml) | $INFRA_MAC_MINI_USER | SSH key (1Password agent) | "rentamac" (secure note), "Rent a Mac" (login) |
$INFRA_VPS_HOST | $INFRA_VPS_IP | $INFRA_VPS_USER | Password | $INFRA_VPS_OP_ITEM (login, Personal vault) |
$INFRA_DEV_HOST | $INFRA_DEV_IP | $INFRA_DEV_USER | Local | N/A |
$INFRA_MAC_MINI_HOST:
ssh $INFRA_MAC_MINI_USER@$INFRA_MAC_MINI_ADDR "command"
$INFRA_VPS_HOST:
source ~/.claude/skills/lib/infra-load.sh
sshpass -p "$(op item get $INFRA_VPS_OP_ITEM --account=$INFRA_OP_PERSONAL --fields password --reveal)" \
ssh -o IdentitiesOnly=yes -o IdentityAgent=none -o PreferredAuthentications=password \
$INFRA_VPS_USER@$INFRA_VPS_IP "command"
CRITICAL: The shell environment on $INFRA_DEV_HOST has many op:// references already set (ANTHROPIC_API_KEY, OPENAI_API_KEY, FIRECRAWL_API_KEY, etc. — all pointing to Personal or cli vaults in $INFRA_OP_PERSONAL). When you run op run --account=toptal.1password.com, it tries to resolve ALL op:// references in the current environment, including those personal vault ones — and fails.
Rule: Always use --account=$INFRA_OP_PERSONAL with --env-file=~/.secrets. Never use the toptal account from an interactive shell — the shell env and ~/.secrets only reference personal vaults.
~/.secrets is the canonical op:// env file — all cli and Personal vault references, no work vault refs.
# ✅ CORRECT — personal account + ~/.secrets
op run --account=$INFRA_OP_PERSONAL --env-file=~/.secrets -- <command>
# ❌ WRONG — toptal account can't resolve op://Personal/... or op://cli/... refs
op run --account=toptal.1password.com --env-file some.env -- <command>
If a tool has its own .env with op://Employee/... (toptal vaults), ignore it — the equivalent key is already in ~/.secrets pointing to the personal vault.
The shell env on $INFRA_DEV_HOST is loaded by direnv via /Users/joe/dev/.envrc (and per-project .envrc files that call source_up). The parent .envrc runs op run --account=$INFRA_OP_PERSONAL --env-file=~/.secrets -- env and exports all resolved secrets into the shell.
~/.secrets is the canonical secret file — op:// references for all CLI/Personal vault keys. When you need a secret available in shell (and therefore in mise run, uv run, etc.), it must be in ~/.secrets.
ANTHROPIC_API_KEY="op://Personal/<api-key-item>/ANTHROPIC_API_KEY"
QUIRK: The cli vault has an item called Anthropic API Key whose credential field contains sk-ant-oat01-... — an OAuth token, not an API key. This resolves successfully via op run but is rejected by the Anthropic API with "Invalid API key". Always use the Personal/vps-anthropic-api item.
~/.secretsAfter editing ~/.secrets, run direnv reload (or cd out and back in) to re-export the new values:
direnv reload
# or
cd .. && cd -
mise run doesn't see env varsmise run inherits the shell environment, so direnv must be loaded in the calling shell. If running from a shell without direnv (e.g., a CI script, a nushell subprocess, or a run_in_background Bash tool call), source the env manually:
eval "$(op run --account=$INFRA_OP_PERSONAL --env-file=~/.secrets -- env | grep -E '^(ANTHROPIC|OPENAI|GEMINI)' | sed 's/^/export /')"
| Mistake | Fix |
|---|---|
Using bare tailscale on remote macOS | Use /Applications/Tailscale.app/Contents/MacOS/Tailscale |
| SSH failing with "too many auth failures" | Add -o IdentitiesOnly=yes -o IdentityAgent=none -i /path/to/key |
op read for SSH keys gives invalid format | Use op item get --reveal --format=json and extract via python |
| Searching wrong 1Password account | Always specify --account=my.1password.com for infra |
| Assuming item title matches device name | Search broadly, then filter — titles are inconsistent |
tailscale ssh on macOS App Store build | Not available — use regular ssh instead |
| Forgetting to clean up temp key files | Always rm -f /tmp/keyfile after use |
sudo over SSH without TTY | Use sudo -S command <<< 'password' or avoid sudo when possible |
op run --account=toptal from interactive shell | Shell has op://Personal/... env vars; use --account=$INFRA_OP_PERSONAL instead |
op://cli/Anthropic API Key/credential | Returns OAuth token (oat01), not API key — use the Personal vault API key item |
env vars missing in mise run | direnv must be loaded in the calling shell; check with direnv status |