# p00f Zero-knowledge, ephemeral clipboard for humans and agents. Exchange transient context, secrets, prompts, and intermediate results by URL. The hosted API only ever holds ciphertext; all encryption and decryption happen caller-side. ## Trust model The Fragment Key is 32 random bytes, base64url, carried only in the URL fragment after '#'. The fragment is never sent to the server. The server cannot read content and cannot recover a lost link. Whoever holds the link (and any LLM behind them) can decrypt and will see plaintext. ## Easiest path: the CLI If you can run a shell, the official CLI does everything below for you, with no install: npx @p00f/cli ./file # create from a file, prints the link echo "some text" | npx @p00f/cli # create from stdin npx @p00f/cli get # reveal and decrypt (consumes one reveal) npx @p00f/cli info # inspect, non-consuming Install it for repeated use with: npm i -g @p00f/cli It wraps @p00f/core, so all encryption and decryption happen on your machine and the server only ever sees ciphertext. It talks to https://p00f.me by default; set POOF_BASE to target another deployment. A poof that requires a human captcha to reveal cannot be opened from the CLI; everything else can. The raw HTTP wire format below is only for callers that cannot run the CLI. ## Endpoints - POST https://p00f.me/api/clip Create. Multipart form: meta and content ciphertext blobs, ttlMs, revealBudget, optional pin, optional id, and optional requireTurnstile/allowViewerDelete/revealAnchored set to "1". revealAnchored starts the TTL clock at the first reveal instead of at create (ADR-0017). No Turnstile token is required on the machine path; anonymous create is allowed under an identity-free rate-limit floor. - GET https://p00f.me/c/:id.json (or GET https://p00f.me/c/:id with Accept: application/json) Non-consuming. Returns the encrypted metadata envelope (see below). - POST https://p00f.me/api/clip/:id/reveal Consuming. Returns encrypted content as application/octet-stream. Decrypt caller-side. Reveal is a POST so prefetchers and unfurlers never spend budget. JSON body carries { pin } when the envelope says pinRequired, and { turnstile } when it says turnstileRequired. A poof with turnstileRequired=false needs no human and is revealable headlessly. A successful reveal returns the authoritative burn deadline in the x-poof-expires-at response header (epoch ms); for a reveal-anchored poof (ADR-0017) the deadline only exists once this first reveal starts the clock. - POST https://p00f.me/api/clip/:id/delete body { ownerToken } Owner-gated early burn. The owner token is returned once at create and is never carried in the link. - POST https://p00f.me/api/clip/:id/burn Viewer-initiated early burn, no owner token. Only honored when the envelope says allowViewerDelete is true (the creator opted in); otherwise HTTP 403. Destroys the poof for everyone, not just the caller. - GET https://p00f.me/health ## Limits - Max content size per poof: 25 MB (encrypted blob). An oversized create is rejected with HTTP 413 { error: "too_large", maxBytes }. - TTL: any value up to 30 days (custom, not only presets). Reveal budget: 1 to 100, or unlimited (-1). ## Wire format - Key: 32 random bytes, base64url, carried only in the URL fragment after '#'. The fragment is never sent to the server. - Id: 16 random bytes, base64url. The routing id, distinct from the key. - base64url: RFC 4648 base64url: '-' and '_' alphabet, padding stripped. - KDF: HKDF-SHA-256. salt = the clip id, UTF-8 bytes. info = "poof/metadata/v1" for metadata, "poof/content/v1" for content. PIN: for the content role only, an optional PIN or password (variable length, 4 to 128 chars) is appended to the master key bytes to form the IKM (master || pin); the metadata key is independent of the PIN. - Cipher: AES-GCM-256. nonce = 12 random bytes (IV), prepended to the ciphertext: layout is iv(12) || ciphertext+tag. ## Envelope schema The .json envelope is a JSON object with cleartext protocol fields (id, revealsRemaining, pinRequired, turnstileRequired, allowViewerDelete, hasContent, sizeBucket) and one encrypted field (metadata: base64url AES-GCM ciphertext of the JSON { kind, filename, mime, size, showCountdown, and a TTL field that is either expiresAt (creation-anchored) or ttlMs + revealAnchored:true (reveal-anchored, ADR-0017) }). The exact kind, filename, mime, size, and the expiry are inside the encrypted metadata blob, never in cleartext (ADR-0014). pinRequired and turnstileRequired tell a caller whether it needs the out-of-band PIN/password and/or a human (a turnstileRequired poof cannot be revealed by the machine path). allowViewerDelete is true when the creator let any link-holder burn the poof early via POST /api/clip/:id/burn (ADR-0016). The TTL field has two shapes (ADR-0017): a creation-anchored poof carries an absolute expiresAt; a reveal-anchored poof carries only ttlMs + revealAnchored and has no deadline until its first reveal, at which point the server discloses the authoritative deadline via the x-poof-expires-at response header (epoch ms, CORS-exposed) returned on every successful reveal. ## Revealing as an agent (no browser required) A poof link looks like https://p00f.me/c/#. The part after '#' is the Fragment Key and is NEVER sent to the server. To reveal without a browser: 1. Split the link on '#': the path gives ; the fragment is the base64url key. 2. GET https://p00f.me/c/.json. If turnstileRequired is true, a human Turnstile challenge is required and you cannot reveal headlessly; stop here. If pinRequired is true, you need the PIN/password the sharer gave you. 3. POST https://p00f.me/api/clip//reveal with a JSON body of { pin } if pinRequired (otherwise an empty POST). The response body is AES-GCM ciphertext laid out as iv(12) || ciphertext+tag. Read the x-poof-expires-at response header for the authoritative burn deadline (epoch ms); for a reveal-anchored poof this is the only place the deadline appears, since this reveal started the clock. 4. Derive the content key with HKDF-SHA-256: salt = the clip id, UTF-8 bytes, info = "poof/content/v1", IKM = the 32-byte key (with the PIN bytes appended when a PIN is set). Decrypt the reveal bytes. The metadata blob from step 2 decrypts the same way with info = "poof/metadata/v1" and no PIN. @p00f/core implements all of this; an agent that can run JS can call it directly instead of reimplementing the crypto. ## Reference @p00f/core is the supported reference implementation of this wire format.