A localhost-only, single-use secret intake form. Run it, a browser tab opens,
you paste a secret, it lands in Proton Pass via
pass-cli. The value goes straight from the form POST into pass-cli over
stdin — never a CLI argument, never a file, never logged.
The point: hand a secret to Proton Pass without it passing through a terminal you're sharing, a chat transcript, or shell history. Whoever started the process (you, or an agent acting on your behalf) sees only the exit code.
Need more than one value? Add fields in the form — each typed Secret,
Text, TOTP, or Timestamp — and the item is stored with all of them
(e.g. an S3 access-key-id + secret-access-key, or a username + password
endpoint). Every value still rides in over stdin; none touch argv.
Straight from GitHub — no npm registry, no build step:
bun install -g "github:grunt-it/secret-tap#main"Then run it:
secret-tap cloudflare-grunt-ai-gateway-runtime
# or pick the vault explicitly
secret-tap my-item-title --vault grunt
# preset the form's fields (names + types only — values are pasted in the browser)
secret-tap r2-token --vault grunt-ai \
--field access-key-id:text --field secret-access-key:secret
# prefill the non-secret rows so you only paste the one value that matters
secret-tap proton-smtp --vault grunt \
--field SMTP_HOST:text=smtp.example.com \
--field SMTP_PORT:text=587 \
--field SMTP_USERNAME:text=user@example.com \
--field SMTP_PASSWORD:secret--field <name>[:<type>[=<value>]] is repeatable and pre-fills the form's
rows. <type> is secret (default) · text · totp · timestamp. With no
--field, the form opens with a single secret field (stored as a login item).
=<value> pre-fills a row and is accepted for text rows only — passing
a value for a secret row is rejected, because secrets must never appear on
argv. When some rows are prefilled the cursor lands on the first row still
empty, which is the secret you came to paste.
--version / -V prints the installed version and exits. Unknown flags are
rejected rather than silently treated as the item title.
Or run without installing:
bunx "github:grunt-it/secret-tap#main" my-item-titleTo pick up updates, re-run the bun install -g line.
- A server starts on
127.0.0.1on an OS-assigned random port. - The form lives behind an unguessable single-use path token — anything else gets a flat 404.
- Your browser opens to it. Paste the value, optionally tweak the title. Add more fields if the credential has several parts; each field has a name, a type (Secret / Text / TOTP / Timestamp), and a value.
- Upsert step: if an active item in
<vault>already has the chosen title, its current fields are read in-process and merged into the submission: a row you filled in overrides the field of the same name, and every field you did not resupply is carried forward unchanged. So naming three fields of an eight-field item rotates three and keeps five, instead of replacing the item with a three-field stub. If the existing item cannot be read, the tap refuses and changes nothing rather than replacing it lossily. The old item is then renamed to a unique title (<title> (replaced <timestamp>)) and trashed — title only, no secret in argv. Renaming before trashing is deliberate: a same-title trashed item can shadow the new one inpass://resolution, so the rename guarantees the new item resolves cleanly. - The value(s) are piped into
pass-cli item create … --from-template -:- a single, untouched Secret field → a login item (
password), sopass://<vault>/<title>still resolves it by default — identical to the original single-secret behaviour; - anything else (multiple fields, a renamed field, a non-secret type) → a
custom item whose typed fields are each addressable as
pass://<vault>/<title>/<field-name>. Either way the values ride in on stdin — never argv.
- a single, untouched Secret field → a login item (
- Success → a little checkmark (heading reads "Replaced" if step 4 superseded
an existing item, "Stored" otherwise), the tab self-closes, the server stops
and the process exits
0. - Failure → the
pass-clierror is shown in the page. If step 4 trashed an item, the tool tries tountrashit as rollback so the vault isn't left in a worse state. The server stays up so you can fix and retry (re-login, change vault, etc.).
A hard timeout means the tap never lingers — 5 minutes by default, or set
--timeout <seconds> (e.g. --timeout 1800 for 30 minutes) when you need
longer to fetch the value.
The final stdout line is the machine-readable contract:
secret-tap:result {"status":"stored","action":"stored"|"replaced","title":"…","vault":"…","itemType":"login"|"custom","fields":["…"],"preservedFields":["…"]} —
callers (humans or agents) read action to know whether they created a new
item (stored) or superseded an existing one (replaced), fields to know
which names to reference as pass://<vault>/<title>/<field>, and
preservedFields for the subset carried forward from the superseded item.
action is replaced, not updated: this path creates a new item and trashes
the old one, and calling that an update is what once let a field-destroying
replacement read as a routine field edit.
The exit code is the coarse signal (0 stored, 1 timed out). The detail —
including the final item title, which you can edit in the form — is the
last line of stdout:
secret-tap:result {"status":"stored","action":"stored","title":"my-item","vault":"grunt","itemType":"login","fields":["password"],"preservedFields":[]}
secret-tap:result {"status":"stored","action":"replaced","title":"r2-token","vault":"grunt-ai","itemType":"custom","fields":["access-key-id","secret-access-key"],"preservedFields":["access-key-id"]}
secret-tap:result {"status":"timeout"}
Parse that line (not the command-line argument) to learn what the item was actually saved as — the final title (you can edit it in the form), the itemType, and the field names for a custom item. It never contains a secret value.
- Binds to
127.0.0.1only — never0.0.0.0. - The secret travels: browser → localhost POST body → process memory →
pass-clistdin. It is never written to stdout/stderr/disk by this tool. - The one remote call is
pass-cli→ Proton Pass — that is where the secret is meant to land. There is no other network egress. - Single submit, then the process exits. Re-run for the next secret.