# secretdrop Receive a secret (API token, password, key) from your human **without it ever appearing in the chat**. You run a small CLI. It prints a link. The human opens the link, pastes the secret and presses Send. Their browser encrypts it to a key that exists only inside your waiting CLI process. The CLI decrypts it and writes it to a file or a pipe. The server only ever sees ciphertext. ## Instructions for AI agents If your human gave you this address, they want you to request a secret this way. Follow these steps: 1. **Download the CLI** (one file, Node.js ≥ 20, no dependencies): ```bash curl -fsSLo /tmp/secretdrop.mjs https://secrets.levinkeller.de/secretdrop.mjs ``` 2. **Start a request in the background.** The command waits until the human has answered (default up to 15 minutes), so run it in the background or with a long timeout. Name what you need precisely. Write the secret to a file: ```bash node /tmp/secretdrop.mjs request --name "Cloudflare API token (DNS edit, zone example.com)" --out /tmp/cf-token ``` Or pipe it straight into the program that needs it, so it never touches disk: ```bash node /tmp/secretdrop.mjs request --name "GitHub token for CI" | gh secret set GH_TOKEN ``` 3. **Give the human the link** printed on stderr, complete with the part after `#`, for example: ```text https://secrets.levinkeller.de/r/2fQk…#7KQF-M2XD ``` Ask them to open it, paste the secret and press Send. They do not need to reply to you; your CLI is already waiting. 4. **Wait for the CLI to exit.** Exit code 0 means the secret is in the file or pipe. Then use it without printing it, e.g. `CLOUDFLARE_API_TOKEN="$(cat /tmp/cf-token)" wrangler deploy`. ### Rules - **Never print the secret.** Do not `cat` the file, echo it, or run the CLI with stdout going into your own transcript. Your shell tool's stdout is usually captured into the conversation, which is exactly what this service avoids. Use `--out FILE` or a pipe into the consuming program. - **Never ask the human to paste a secret into the chat.** Send them a secretdrop link instead. - If the CLI exited (killed, timed out, expired), the request is useless: its private key is gone. Run the command again and send the new link. - Delete the file when you no longer need it. ### Options | Option | Default | Meaning | | --- | --- | --- | | `--name TEXT` | (required) | What you ask for, shown on the page (max 100 characters). | | `--ttl 15m` | `15m` | How long the link stays valid, `1m` to `24h`. | | `--out FILE` | stdout | Write the secret to FILE (mode 0600). Refuses to overwrite unless `--force`. | Exit codes: `0` ok, `1` error, `2` usage (or stdout is a terminal), `3` expired, `4` decryption failed. ### Troubleshooting - `fetch failed` / HTTP 403 from a proxy: your sandbox's network policy must allow `secrets.levinkeller.de`. Behind an HTTP proxy, Node ≥ 22.21 needs `NODE_USE_ENV_PROXY=1`. - HTTP 429: too many requests from your IP; wait a minute. ## How it works - The CLI creates an ephemeral X25519 key pair in memory (never written anywhere) and registers the public key. The browser seals the secret to it with HPKE (RFC 9180: X25519, HKDF-SHA256, AES-256-GCM), bound to the request id and name. - The ciphertext can be fetched exactly once; then it is deleted. Everything is deleted at the TTL. - Data is stored in a Cloudflare Durable Object in the EU jurisdiction. - No accounts. Request ids are 128-bit random; only someone with the link can fill in a request, and only once. - The part after `#` is a check code derived from the request id, name and public key. Browsers never send it to the server. The page recomputes it from the key it is about to encrypt to and refuses on a mismatch, so a swapped key is caught automatically. - Free to use. Requests are rate-limited per IP. Source: