secrets
1Password secrets management — vault structure, op:// references, .env.tpl patterns, adding/rotating secrets, cron setup, troubleshooting
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
1Password secrets management — vault structure, op:// references, .env.tpl patterns, adding/rotating secrets, cron setup, troubleshooting
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Read and write the second brain — a git-backed Obsidian vault at ~/SourceRoot/brain (PARA structure + a top-level wiki/ knowledge tree). Use whenever the user mentions "brain", "second brain", "knowledge base", "note", "remember", "look up", "find note", "capture", "wiki", or "okf". Also use for any request to save, retrieve, or organize personal knowledge that belongs in the brain rather than in a repo-specific doc.
Write, draft, and polish prose through a human-owned 7-step pipeline. Use whenever the user says "distill", "write article", "draft", "summarize into", "create content", "synthesize", or "publish". Also use for any task where the output is a piece of long-form writing — the pipeline keeps prose quality out of coding sessions.
RETIRED — manage the (now decommissioned) per-machine mlx-audio TTS/STT stack; TTS/STT moved to the cloud audio-gateway (VPS)
Operate the Mac mini remote dev host — connecting from the MacBook over mosh or herdr --remote, running and reattaching many Claude Code agents, herdr workspaces/panes and its socket API, claude --bg durable daemons, the Uptime Kuma readiness heartbeat, and the failure modes specific to a headless always-on Mac. Use when the user mentions herdr, mosh, the mini, "remote dev", "dev host", detaching or reattaching agents, "did my agent survive", sessions dying on lid-close, or asks how to reach a dev server running on the mini.
Manage the personal image stack — public CDN uploads/transform URLs and the private image-share layer. Use whenever an image needs a URL (public or private) for an article, blog post, vault note, README, OpenGraph tag, or a one-off share; or when the user says "upload this image", "share this image", "private image", "publish image", "host this screenshot", "get me a CDN link", "resize this", mentions "image-share", or asks where an image lives.
Bidirectionally sync ALL git repos across two machines — this MacBook and the always-on Mac mini — over SSH, using GitHub/GitLab as the transport. Commits and pushes uncommitted work on both sides, then rebases/fast-forwards each so they converge, with per-repo subagents resolving rebases and merge conflicts. This is a MULTI-repo, TWO-machine operation — distinct from /commit, /ship, or /git-cleanup, which act on a single repo. Use whenever the user wants to sync their machines/repos, "catch up" the laptop to the Mac mini or push laptop work back, is about to travel or just got back, has uncommitted work scattered across many repos, or wants to reconcile branches that diverged between the two machines. Trigger even on a bare "sync", "sync my machines", or "get my repos up to date".
| name | secrets |
| description | 1Password secrets management — vault structure, op:// references, .env.tpl patterns, adding/rotating secrets, cron setup, troubleshooting |
Two 1Password accounts are configured:
| Workspace | Account |
|---|---|
~/SourceRoot/ (personal) | tkrumm |
~/IuRoot/ (work) | careerpartner |
Preferred: use the workspace-aware helpers from ~/.zsh/conf.d/secrets.zsh (sourced by ~/.zshrc). They resolve the account from $PWD and are worktree-safe via git rev-parse --git-common-dir:
op_account_for_cwd # → "tkrumm" or "careerpartner"
op_run vault list # → op vault list --account <resolved>
op_run read "op://vault/item/field"
op_run run --env-file=.env.tpl -- bun test
If invoking op directly, always pass --account explicitly — without it, op may pick the wrong account or prompt interactively.
secrets-run cache backend (Mac mini)The always-on Mac mini is headless: op is not interactively signed in, so a direct
op read / op run there hangs on the biometric prompt. Secrets resolve instead from an
age-encrypted, op://-keyed cache through secrets-run — a drop-in op shim
(dotfiles/scripts/secrets-run). Per-machine backend marker ~/.config/secrets/backend:
| Backend | Machine | Behavior |
|---|---|---|
cache | Mac mini (headless) | decrypts the offline cache (dotfiles-private/cache/secrets.enc.json); no op, no network, no prompt |
op | MacBook (human present) | passes through to live biometric op |
Same app code + same op:// refs on both; only the marker differs. A SessionStart hook injects
the active backend into context each session. On the mini, never call op directly — use the shim:
secrets-run read op://vault/item/field # ~ op read (one value)
secrets-run run [--env-file=<tpl>]... -- <cmd> # ~ op run (--env-file repeats; last file wins)
secrets-run export [--env-file=<tpl>]... # emit `export K=V` lines (defaults to baseline.env.tpl)
The allowlist + seed. Which refs the mini may hold offline is the explicit list
dotfiles-private/headless.refs (grouped by consumer with # --- <app> --- headers). It is
the tiering boundary: only T0/T1 refs belong there; the seed refuses op://Private/* (name
op:// refs → make secrets-seed.make secrets-seed resolves every ref via biometric op read (present-human) and seals one
age-encrypted cache. It is interactive — run it where a human can approve the biometric: on the
MacBook, or on the mini via an interactive terminal (in a Claude session, have the user run it with
the ! prefix — it can't be driven from a non-interactive tool call, it will hang).make secrets-test (mini-only) / make secrets-lint (anywhere) validate the shim.make secrets-freshness-check pushes the Uptime Kuma staleness heartbeat up — run after a reseed.Guardrail — any edit to secrets-run: make secrets-test + shellcheck + update
dotfiles-private/docs/{design.md,security-review.md} in the same change + an adversarial /review.
It is the sole secret path on the mini; treat it accordingly. Full model:
dotfiles-private/{PRD.md,docs/design.md,docs/runbook.md}.
Before answering questions about vault contents, always query the live state — don't rely on memorized vault structures:
# List vaults accessible to current session
op_run vault list
# List items in a vault
op_run item list --vault <vault>
# Show item fields
op_run item get <item> --vault <vault> --format=json | jq '.fields[] | select(.value != "") | .label'
Vault contents change over time. The patterns below are stable; the specific items are not.
.env.tpl (git-tracked) → op run --env-file=.env.tpl -- <command>
↓
1Password resolves op:// refs at runtime
↓
env vars injected into <command>
.env.tpl committed to git — contains only op:// references and plain config values, never actual secrets.env.tpl per project roothomelab-private shares homelab/.env.tpl via absolute path: op run --env-file=$(HOME)/homelab/.env.tpl --# Secrets — resolved by op run from 1Password
SECRET_VAR=op://vault/item/field
# Plain config — passed through as-is
CONFIG_VAR=some-value
# Comments start with #, empty lines are skipped
# Single/double quotes are stripped from values
# URLs with & must be stored in 1Password (not inline) to avoid shell expansion issues
op://vault/item/field where:
homelab, vps, common)postgres, cloudflare, ntfy)PASSWORD, TOKEN, API_KEY)op_run read "op://vault/item/field"
config) in per-server vaults hold non-secret but sensitive values (Tailscale IPs, internal URLs, push URLs with tokens in the path)service-account) hold the OP_SERVICE_ACCOUNT_TOKEN for each serverPASSWORD, TOKEN) — the item name provides context.env.tplEach server has a dedicated service account with least-privilege vault access:
<vault>/service-account/TOKENOP_SERVICE_ACCOUNT_TOKEN in ~/.bashrc and ~/.profileop vault list shows which vaults a SA can access — use this to verify# New item
op item create --vault <vault> --category "API Credential" --title "<service>" \
'FIELD_NAME[password]=<value>'
# Add field to existing item
op item edit <item> --vault <vault> 'NEW_FIELD[password]=<value>'
op item edit <item> --vault <vault> \
'username[delete]' 'credential[delete]' 'type[delete]' \
'filename[delete]' 'valid from[delete]' 'expires[delete]' \
'hostname[delete]'
.env.tpl:
ENV_VAR_NAME=op://vault/item/FIELD_NAME
.env.tpl changegit pull — the next op run picks it up automaticallyop item edit <item> --vault <vault> 'FIELD[password]=<new-value>'
.env.tpl references are stableop run invocation picks up the new value automaticallydocker compose restart <svc> via op run)Cron does NOT source .bashrc or .profile. Two patterns:
Pattern A — source profile in cron entry (preferred for user crontabs):
*/2 * * * * . /home/jkrumm/.profile; op run --env-file=/path/.env.tpl -- /path/script.sh
Pattern B — inline token in /etc/cron.d/ (for system cron files):
OP_SERVICE_ACCOUNT_TOKEN=<token>
0 3 * * * jkrumm cd /path && op run --env-file=.env.tpl -- ./script.sh
Note: crontab may strip env var lines on some systems. Use /etc/cron.d/ files or the . .profile; pattern instead.
Pattern C — op read in scripts (for scripts that need specific secrets, not full env):
TOKEN=$(op read "op://vault/item/field")
The script's calling environment must have OP_SERVICE_ACCOUNT_TOKEN set.
Non-interactive SSH (ssh server "command") needs OP_SERVICE_ACCOUNT_TOKEN available. The token must be at the top of ~/.bashrc, before the interactive guard:
# MUST be ABOVE the "If not running interactively" guard in ~/.bashrc
export OP_SERVICE_ACCOUNT_TOKEN="<token>"
# If not running interactively, don't do anything
case $- in
*i*) ;;
*) return;;
esac
Why: Ubuntu's default .bashrc exits early for non-interactive shells. Anything below the guard is invisible to ssh server "command". Additionally, homelab uses Tailscale SSH (not OpenSSH), which does NOT support ~/.ssh/environment or PermitUserEnvironment.
With the token at the top of .bashrc, SSH commands work directly:
ssh server "cd ~/repo && make up"
For sudo operations (homelab requires password):
# Root passwords are in Private vault (not accessible to server SAs — local only)
ROOT_PW=$(op read "op://Private/homelab-server/password")
ssh homelab "echo '$ROOT_PW' | sudo -S <command>"
Cloudflare tokens are split by purpose:
common vault, used by Caddy (homelab) and Traefik (VPS) for ACME DNS-01 challengescommon vault, used for Cloudflare API operations (DNS records, tunnel config)Query zones and tunnel IDs dynamically via the manage token — don't store zone/tunnel IDs.
| Symptom | Cause | Fix |
|---|---|---|
No accounts configured | OP_SERVICE_ACCOUNT_TOKEN not in environment | Export it or source .profile |
Token empty in ssh server "cmd" | Token export below .bashrc interactive guard | Move export to top of .bashrc, before case $- |
~/.ssh/environment not working | Tailscale SSH ignores PermitUserEnvironment | Use .bashrc top-of-file approach instead |
could not resolve secret reference | Wrong vault/item/field name in .env.tpl | Check with op item get <item> --vault <vault> |
authorization timeout | Biometric prompt not answered (local op) | Retry — Touch ID prompt may be behind windows |
Caddy rejects cfut_ token | Old caddy-dns/cloudflare plugin | Rebuild caddy: docker compose build caddy |
| Cron job not firing | Missing OP_SERVICE_ACCOUNT_TOKEN in cron env | Use . .profile; prefix or inline token |
More than one vault matches | Vault name ambiguity (e.g. shared vs Shared) | Use vault ID instead of name |
Docker compose "variable not set" warnings on down | Normal — docker compose down doesn't need env vars | Harmless, can ignore |
.env files with actual secret values.env.tpl → op run at runtimeOP_SERVICE_ACCOUNT_TOKEN is the only secret on disk.env.tplPrivate vault (local-only, not accessible to server SAs) — use sudo -S pattern, never SSH as root