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.
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.
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.
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.
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.
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 drivesanyvendor 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.
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.
secret-shuttle provision --secret INTERNAL_CRON_SECRET \
--from random_32_bytes \
--environment production \
--to vercel:production
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.
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.
-
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 refrotating
; 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 vaultsecret-shuttle secrets delete <ref>
β remove a secret from the vault
- Hardware-backed key storage (HSM / Secure Enclave) β note: OS-keychain key storage
doesship (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 currentgh
/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.
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
) β seedocs/templates-deferred.mdfor the reason and re-open criteria - Signed desktop binaries
- Secret export workflows (rotation and
.env
import ship; export does not)
skills/secret-shuttle/SKILL.mdβ the canonical agent operating manualdocs/security-model.mddocs/threat-model.mddocs/cli-reference.mddocs/architecture.mddocs/roadmap.md
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.
MIT