GitHub - pdumicz/secret-shuttle: Let AI agents move secrets without seeing them. Capture, store, and inject secrets for AI agents — without exposing the values.
> **Reading this as an AI coding agent?** Your starting point is skills/secret-shuttle/SKILL.md (raw URL: `https://raw.githubusercontent.com/pdumicz/secret-shuttle/main/skills/secret-shuttle/SKILL.md`). The SKILL is your operational manual; this README is for humans.
Let AI agents use secrets without seeing them.

**▶ Walk through the interactive demo →** · opens on the one-approval `provision` flow, then a 9-scene click-through of the low-level mechanics — a dev shipping a Stripe webhook secret to Vercel prod without ever seeing the value.
> **Status: 0.5.0 — beta.** The architecture has been through multiple bursts of adversarial security review with fixes shipped at each gate. Not yet independently audited; recommend test accounts and rotating tokens until that audit lands. Suitable for development workflows and prototype deployments.
Secret Shuttle is a local bridge that lets coding agents — Claude Code, Codex, Cursor, browser-using agents — capture, generate, store, compare, and inject secrets through browser and CLI workflows. The agent sees only refs like `ss://stripe/prod/STRIPE_WEBHOOK_SECRET`, fingerprints, field metadata, and status — never the raw value.
Why not Doppler / Infisical / 1Password CLI / Vercel envs?
[](https://github.com/pdumicz/secret-shuttle#why-not-doppler--infisical--1password-cli--vercel-envs) Those tools sync secrets across environments — they assume a human or a CI runner is the consumer. Secret Shuttle assumes an AI coding agent is the consumer, and treats every plaintext touch as a leak vector.
| Tool | Where secrets live | Who sees plaintext | Agent-aware? | | --- | --- | --- | --- | | Doppler / Infisical | Cloud vault | Anyone with read access (incl. agents querying it) | No — sync model | | 1Password CLI | OS keychain | Caller process; `op read` writes to stdout | No | | Vercel envs (et al.) | Vendor backend | Engineers via dashboard; build runners via env | No | | **Secret Shuttle** | Local daemon vault | Only the daemon's child processes (templates) | **Yes** — agent sees only refs |
If your secrets already live in a sync tool and an agent never touches them, you don't need Secret Shuttle. If you have an agent writing code that needs to ship secrets to Vercel/GitHub/etc, and you want the agent to do that without the bytes entering its context — that's the gap this closes.
30-Second Install
[](https://github.com/pdumicz/secret-shuttle#30-second-install)
npx secret-shuttle init
This starts the local daemon and walks you through creating a vault passphrase in a local web window that only the daemon owns — the CLI never reads it. (Touch ID isn't a first-run prompt: it's how _later_ unlocks work once the vault key is enrolled in the OS keychain. `init` enrols the keychain by default when it creates the vault — pass `--no-keychain` to opt out, or run `secret-shuttle keychain enable` later.) After `init` completes the daemon is running and you are ready to use the CLI or hand it to an agent.
For Agents
[](https://github.com/pdumicz/secret-shuttle#for-agents) If you're configuring an agent to use Secret Shuttle, paste this raw skill URL into the agent (it's the canonical operating manual):
``` https://raw.githubusercontent.com/pdumicz/secret-shuttle/main/skills/secret-shuttle/SKILL.md ```
If you have the CLI installed locally, run one of these from your project root and the platform-specific instructions file is written for you:
secret-shuttle agent install claude # → .claude/skills/secret-shuttle/SKILL.md secret-shuttle agent install codex # → AGENTS.md snippet (marker-managed) secret-shuttle agent install cursor # → .cursor/rules/secret-shuttle.mdc secret-shuttle agent install copilot # → .github/copilot-instructions.md snippet (marker-managed) secret-shuttle agent print-skill-url # → the raw URL (one line, paste it)
Snippet targets (AGENTS.md, .github/copilot-instructions.md) wrap the Secret Shuttle block in `<!-- secret-shuttle:begin -->` / `<!-- secret-shuttle:end -->` markers — re-running `agent install` only replaces the marked block, never the surrounding content.
How It Works
[](https://github.com/pdumicz/secret-shuttle#how-it-works)
``` Agent CLI (untrusted client) | | localhost HTTP, bearer token from ~/.secret-shuttle/daemon-socket.json v Secret Shuttle daemon
- vault key (in memory only, after passphrase unlock through web UI)
- approval grants (single-use, 2-min TTL, bound to action/ref/domain/target/field/template)
- browser owner — talks raw CDP over a pipe
- filtered CDP WebSocket proxy exposed to the agent
- safe command-template runner (no shell, no arbitrary commands)
```
The daemon owns every secret moment. The agent sees refs and status, never raw values, never the raw Chrome CDP URL, never the vault key.
Two paths, one guarantee
[](https://github.com/pdumicz/secret-shuttle#two-paths-one-guarantee)
- **CLI templates (most reliable).** Push a secret to Vercel / GitHub Actions / Cloudflare / Supabase — the value goes to the vendor's own CLI, never through the agent. This is the path the hero demo above shows.
- **Universal browser handoff (experimental).** Your agent drives _any_ vendor portal with its normal browser tool, and hands off only the secret moment — it marks the field, the daemon types/reads it under blind mode with the agent's view severed. No per-vendor recipe; it works on a portal nobody integrated.
[](https://github.com/pdumicz/secret-shuttle/blob/main/demo/agent-browser-handoff.svg)
The browser path is early (v0.5, best-effort, pending real-page verification): the guarantee is "the agent never sees the plaintext," **not** "a hostile destination page can't exfiltrate a secret you enter." See SECURITY.md; for untrusted destinations, prefer the CLI path.
Quickstart
[](https://github.com/pdumicz/secret-shuttle#quickstart)
secret-shuttle provision --secret INTERNAL_CRON_SECRET \ --from random_32_bytes \ --environment production \ --to vercel:production
(production secret — approve in the window the daemon opens)
secret-shuttle secrets list --env production secret-shuttle secrets get-ref ss://local/prod/INTERNAL_CRON_SECRET secret-shuttle audit --since=1d
For the full browser walkthrough see examples/stripe-to-vercel/walkthrough.md.
Templates Instead of Arbitrary Commands
[](https://github.com/pdumicz/secret-shuttle#templates-instead-of-arbitrary-commands)
secret-shuttle template list secret-shuttle template run vercel-env-add \ --ref ss://stripe/prod/STRIPE_SECRET_KEY \ --param name=STRIPE_SECRET_KEY \ --param environment=production
Templates run vetted binaries with `shell: false`, absolute paths only, and never echo stdout/stderr back to the agent.
What Works Today (0.5.0)
[](https://github.com/pdumicz/secret-shuttle#what-works-today-050)
- TypeScript CLI distributed as `secret-shuttle`
- Local daemon with bearer-authenticated HTTP API on 127.0.0.1
- Passphrase-derived envelope around the vault master key (scrypt + AES-256-GCM)
- `ss://source/env/name` refs
- Generate, capture (focused field / selection), inject, compare — all routed through the daemon
- Inject runs inside a daemon-managed blind window (no manual `blind start`)
- Agentic blind transactions: `inject-submit` writes a stored secret into a marked field, clicks the marked submit control, waits for the approved success marker, and proves the raw secret is absent from every daemon-observable surface before auto-resuming. `reveal-capture` clicks a marked reveal control, captures the now-visible bytes from a marked field or ancestor container, hides them, and writes them to the vault only after the absence proof passes
- `secret-shuttle browser mark focused|pick --as <label>` records fields/controls before blind mode by opaque label; only non-secret element metadata is stored
- Vault-keyed HMAC fingerprints; production `compare` is approval-gated + rate-limited
- Fail-closed domain policy (empty allow-list = injectable nowhere); approvals show the scope
- Approval-integrity invariant: scope params with leading/trailing whitespace are rejected, so the destination the human approves always matches the argv that actually executes
- `secret-shuttle status` health-check (daemon, vault, browser, policy, local files, agentic-flows availability)
- Daemon bearer token is scrubbed from the daemon and all child process envs
- Approval UI with one-shot, context-bound grants for production actions
- Daemon-owned Chrome over `--remote-debugging-pipe`
- Filtered WebSocket CDP proxy that blocks screenshots, DOM, accessibility, runtime, console, log, and network-body reads during blind mode
- Built-in templates (stdin delivery): `vercel-env-add`, `github-actions-secret-set`, `cloudflare-secret-put`
- Built-in template (daemon-owned `0600` env-file delivery with crash-safe startup + periodic sweep): `supabase-edge-secret-set`
- `secret-shuttle agent install <claude|codex|cursor|copilot>` writes the canonical Secret Shuttle skill into your project; `secret-shuttle agent print-skill-url` prints the raw GitHub URL for any agent that accepts a remote skill URL
- Exact-by-default domain matching (`*.example.com` for wildcards)
- Migration command: `secret-shuttle migrate secure-vault`
- OS-keychain master-key storage (`secret-shuttle keychain enable|disable|status`) — `init` enrols the vault master key in the OS keychain by default when it creates the vault (opt out with `--no-keychain`); these subcommands enable/disable/inspect it afterwards, so later unlocks can use the system keychain / Touch ID instead of re-entering the passphrase
- `secret-shuttle secrets rotate <ref>` — generates a fresh secret and marks the old ref `rotating`; you then re-push the new value to its destinations and delete the old ref (it does not yet auto-re-push to existing bindings)
- `secret-shuttle import --env-file <path>` — import secrets from a `.env` file into the vault
- `secret-shuttle secrets delete <ref>` — remove a secret from the vault
What Does Not Work Yet
[](https://github.com/pdumicz/secret-shuttle#what-does-not-work-yet)
- Hardware-backed key storage (HSM / Secure Enclave) — note: OS-keychain key storage _does_ ship (see `keychain enable` above); this entry is only the hardware-backed tier
- Team vaults, cloud sync, MCP server, browser extension
- Template argv-vs-`--help` gates ([P2b] PENDING): the shipped templates' argv vectors have not been verified against the current `gh` / `wrangler` / `supabase``--help` output on a per-release basis
> **The honest steady state:** the first time you use a provider, you log in once in the Secret Shuttle browser. After that, one approval ships everything for providers with a recipe (see the coverage matrix above). Providers whose secrets can't be revealed (OpenAI/Anthropic) are human-paste — you supply a key you created; the daemon never reveals it.
Provider coverage
[](https://github.com/pdumicz/secret-shuttle#provider-coverage) What's automated, by provider and direction. Browser recipes drive the page hands-off (one approval); CLI templates push via the vendor CLI. "Real-page verified" is a human-attested dogfood date (CI has no provider creds).
| Provider | Direction | Mechanism | Status | Real-page verified | Notes | | --- | --- | --- | --- | --- | --- | | Stripe | capture (secret key) | browser recipe | 🆕 this increment | (set on dogfood) | revealable in dashboard | | Supabase | capture (service_role) | browser recipe | ⬜ planned | — | revealable in settings/api | | OpenAI / Anthropic | capture | human-paste | n/a | n/a | create-once; cannot be revealed |
| Vercel | inject (env) | browser recipe **and** CLI (`vercel-env-add`) | CLI shipped; browser recipe URL-configurable — set `url_params: { team, project }` in yml (selectors still best-effort, pending dogfood) | — | CLI push is the robust, project-general default. The browser recipe's URL is now templated (`vercel.com/{team}/{project}/settings/environment-variables`); the user supplies `url_params` per destination in their yml. Selectors are still best-effort and pending real-page dogfood verification (`verified_against_real_page: "2026-06-01-needs-dogfood"`); URL addressability is independent of selector verification. | | GitHub Actions | inject (secret) | CLI (`github-actions-secret-set`) | ✅ shipped | n/a | repo-scoped only | | Cloudflare | inject (secret) | CLI (`cloudflare-secret-put`) | ✅ shipped | n/a | | | Supabase edge | inject (secret) | CLI (`supabase-edge-secret-set`) | ✅ shipped | n/a | |
The absence proof is fail-closed: the daemon won't hand the agent back its view unless it can confirm the secret is gone from the page's DOM/URL (otherwise it stops with `manual_recovery_required`). That guarantees the **agent** never sees the plaintext — it does **not** hook network/clipboard/storage, so it can't stop a hostile destination page from transmitting a value you deliberately entered. Only run browser flows against pages you trust (SECURITY.md). Every new provider is a new row.
- Deferred provider templates (`github-actions-env-secret-set`, `github-actions-org-secret-set`, `railway-variable-set`, `netlify-env-set`, `clerk-env-set`) — see docs/templates-deferred.md for the reason and re-open criteria
- Signed desktop binaries
- Secret export workflows (rotation and `.env` import ship; export does not)
Docs
[](https://github.com/pdumicz/secret-shuttle#docs)
- skills/secret-shuttle/SKILL.md — the canonical agent operating manual
Build from Source
[](https://github.com/pdumicz/secret-shuttle#build-from-source) For contributors and anyone who wants to hack on the code:
git clone https://github.com/pdumicz/secret-shuttle.git cd secret-shuttle npm install npm run build npm link secret-shuttle daemon start secret-shuttle unlock
`unlock` opens a local web window — you enter the passphrase there. The CLI never reads it.
License
[](https://github.com/pdumicz/secret-shuttle#license) MIT