Every key, pinned
to its account.

keypin keeps a map of every app key across your projects, then publishes each one to GitHub Actions and Cloudflare Workers. It checks the account before every write and shows you the plan first. Values stay in your vault.

$ bun add -g keypin

Needs Bun 1.4.4 or newer and the 1Password desktop app.
Publishing to Cloudflare works on macOS only for now. Setup is in the README.

~/code/api: keypin statusexit 1 · 3 problems
$ keypin status
keypin: reading rotation fields (no values) from 1Password account my.1password.com, vault "App Secrets": "api dev api token", "api prod api token", "web prod session secret"
KEY             ENV   VARIABLE        OWNER  ITEM                     ROTATED     EVERY  DUE
# /Users/dev/code/api
api-token       dev   API_TOKEN       api    api dev api token        2026-08-01  180d   2027-01-28
api-token       prod  API_TOKEN       api    api prod api token       2026-05-20  180d   2026-11-16
    -> github-actions:acme/api:API_TOKEN (account acme): no receipt
    -> cloudflare-worker:api-edge:API_TOKEN (account 3b1f0c9a7d2e4f6a8b5c1d0e9f7a6b4c): no receipt
# /Users/dev/code/web
session-secret  prod  SESSION_SECRET  web    web prod session secret  2026-08-02  90d    2026-10-31
    -> cloudflare-worker:web:SESSION_SECRET (account 3b1f0c9a7d2e4f6a8b5c1d0e9f7a6b4c): last published 2026-08-02T10:14:51.330Z by dev@acme.example, generation a5e3c8f1d7092b64

Problems (3):
  missing-receipt: api-token (prod): no receipt for github-actions:acme/api:API_TOKEN, so what it holds is unknown
  missing-receipt: api-token (prod): no receipt for cloudflare-worker:api-edge:API_TOKEN, so what it holds is unknown
  overdue: session-secret (prod): was due 2026-10-31 (rotated 2026-08-02, every 90d)

Notices (2):
  due-soon: api-token (prod): is due 2026-11-16 (rotated 2026-05-20, every 180d)
  authority-unknown: 2 keys (api-token, session-secret): what each grants is unknown, because keypin didn't create it
01

How it works

01 / MAP

map

Each project lists its keys in a committed keypin.toml: the owner, the vault item, and every place the key is published. It never holds a value.

# names and places, never values
[vaults.personal]
kind = "1password"
account = "my.1password.com"
vault = "App Secrets"

[keys.api-token]
env = "API_TOKEN"
owner = "api"

[keys.api-token.prod]
vault = "personal"
item = "api prod api token"

[[keys.api-token.prod.publish]]
to = "github-actions"
repo = "acme/api"
02 / RUN

run

Run a command with the project's dev keys in its environment. keypin reads them from 1Password while the command runs and writes them nowhere on disk, so there's no .env to leak.

$ keypin run -- bun dev
keypin: reading dev values from
  1Password account my.1password.com,
  vault "App Secrets":
  "api dev api token"
  ➜  Local: http://localhost:3000/
03 / PUBLISH

publish

keypin checks each destination is the account the map pins, shows the plan, and waits for you. Receipts let a rerun skip what's already written.

Account checks:
  ok github-actions:acme/api:…
  ok cloudflare-worker:api-edge:…

Plan for generation 87c5ad3105ddfcc3:
  write github-actions:acme/api:…
    needs approval: first write
  write cloudflare-worker:api-edge:…
    needs approval: first write
Type yes to write: yes
  wrote github-actions:acme/api:…
  wrote cloudflare-worker:api-edge:…
02

Commands

keypin init [folder]
Register a project and write a commented keypin.toml. An existing one stays as it is.
keypin unregister [folder]
Stop listing a project. Its keypin.toml stays.
keypin status [--json]
Every key, its rotation dates and receipts, then problems. Exit 0 means none.
keypin run -- <command>
Run a command with the project's dev keys in its environment.
keypin publish <key-id>
Check accounts, show the plan, then write the key to every destination: GitHub Actions secrets and Cloudflare Worker secrets. A first write or an account change waits for you to type yes; --yes never gives that approval.