# 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.