Skip to main content
Glama
secret-paste

secret-paste

README.md
# secret-paste

An MCP server that lets an agent request an API key or other secret from you
without you pasting it into the chat. The value never passes through the
conversation, a tool argument, or a tool result. "pbcopy for agents."

The agent calls `request_secret`. A native password dialog pops up (pinentry,
zenity, kdialog, or a macOS dialog), you type the value, and it goes straight
to `.env`, your OS keychain, or 1Password. No browser tab, no "save this
password?" nag. If no dialog tool is available, a local browser page is used
instead. The agent only ever sees `{ name, target, stored }`.

This keeps the secret out of the request path, not out of the agent's reach.
Once stored, an agent with shell access can still read it (`cat .env`,
`secret-tool lookup`, `op item get`), and anything it reads lands in the
transcript. Pair this with permission rules that deny reading `.env` or running
those commands if that matters to you.

## Install

```sh
cd /path/to/secret-paste
npm install && npm run build
claude mcp add --scope user secret-paste -- node /path/to/secret-paste/dist/server.js
```

Restart Claude Code (or run `/mcp`) and confirm `secret-paste` is connected.

## Tool

`request_secret({ name, target?, description? })`

| field | notes |
|---|---|
| `name` | required, must match `^[A-Z_][A-Z0-9_]*$` |
| `target` | `env` (default), `keychain`, or `op` |
| `description` | optional, shown on the page |

Result on success: `{ "name": "...", "target": "...", "stored": true, "path"?: "...", "readHint": "...", "via": "<backend>" }`.
Result otherwise: `{ "stored": false, "reason": "cancelled" | "timeout" | "<error>", "via": "<backend>" }`.

`via` is one of `pinentry`, `pinentry-mac`, `zenity`, `kdialog`, `osascript`,
`elicitation` (browser page opened through the client), or `browser`
(browser page opened with `xdg-open` / `open`).

The value, its length, a prefix, or a hash is never returned, logged, or echoed.

### Targets

- `env`: upserts `NAME=value` into `.env` in the server's working directory
  (the directory Claude Code was started in). Other lines are preserved, an
  existing line for that name is replaced, the file is created with mode 0600
  if missing, and `.env` is appended to `.gitignore` when the directory is a
  git repo. Values are written unquoted when possible, otherwise wrapped in
  `'`, `` ` ``, or `"` (whichever the value doesn't contain) so dotenv and
  Node's `process.loadEnvFile` read them back verbatim. Values that can't be
  quoted losslessly are rejected with a hint to use `keychain` or `op`.
- `keychain`: macOS `security add-generic-password` (fed over stdin via
  `security -i`, so the value never appears in `ps`), or Linux `secret-tool
  store` (requires `libsecret-tools`).
- `op`: `op item create` with a JSON item template on stdin (1Password CLI),
  saved to your default vault. Read it back with
  `op item get NAME --fields label=password --reveal`.

## How it works

### Native dialog (default)

Backends are tried in order until one is available:

| platform | order |
|---|---|
| Linux | `pinentry`, `zenity`, `kdialog`, browser |
| macOS | `pinentry-mac`, `pinentry`, `osascript`, browser |
| Windows | browser |

- `pinentry` is GnuPG's passphrase prompter and the preferred backend. It
  holds the value in locked memory, draws a native dialog (GNOME/GCR, Qt, GTK,
  or macOS depending on the installed flavour), and enforces the timeout
  itself. The server speaks the Assuan protocol to it over stdin/stdout and
  decodes the `D` response at the byte level. Terminal-only flavours
  (`curses`, `tty`) are skipped because the server has no terminal to give
  them. `SETKEYINFO` is deliberately not sent, so pinentry never offers to
  cache the value in your keyring.
- `zenity --password` and `kdialog --password` print the value to stdout,
  which the server reads as bytes and zeroes after storing.
- `osascript` runs a `display dialog ... with hidden answer` AppleScript.

Set `SECRET_PASTE_PROMPT` to override the order, e.g. `zenity,browser` or
`browser` to force the old behaviour:

```sh
claude mcp add --scope user -e SECRET_PASTE_PROMPT=browser secret-paste -- node /path/to/secret-paste/dist/server.js
```

Dialogs are spawned detached from the server's process group and killed on
timeout or when the tool call is aborted.

### Browser fallback

1. A one-shot HTTP server starts on `127.0.0.1` on a random port with a random
   nonce in the path. Requests with the wrong nonce, Host, or Origin are rejected.
2. The URL is handed to Claude Code via MCP URL-mode elicitation, which shows a
   native "open this link?" prompt. If the client doesn't support that, the
   server falls back to `xdg-open` / `open`.
3. The page is static HTML with no JavaScript and `Cache-Control: no-store`.
4. On submit, the value is parsed at the byte level into a `Buffer`, written to
   the target, then zeroed. It is never held as a JavaScript string.
5. The server shuts down after one submit or cancel, or after 5 minutes.

The tool is tagged `_meta["anthropic/requiresUserInteraction"] = true`, so
Claude Code prompts for permission on every call, even in auto-accept modes.

## How to use it (for Claude)

When you need a credential, call `request_secret({ name: "SERVICE_API_KEY" })`
and then reference it as `process.env.SERVICE_API_KEY` (or the `readHint` in
the result). Never ask the user to paste a secret into chat.

## Development

```sh
npm test        # unit tests: .env upsert, form decoding, dialog backends via fake binaries (node --test)
npm run build   # tsc -> dist/
```

TDQS

A4.1/5.0

Scored across 1 tool

Disambiguation5/5

There is only one tool, so there is no possibility of misselection or overlap. Its purpose (securely obtain and store a secret without exposing it in chat) is unmistakable.

Naming Consistency5/5

The single name request_secret follows a clear verb_noun convention and is self-descriptive. With one tool there is nothing to be inconsistent with.

Tool Count3/5

The server is narrowly scoped to secret ingestion, so one tool is defensible, but it sits at the thin end of the range. A secret-management surface usually warrants at least a companion tool for checking, rotating, or removing stored secrets.

Completeness3/5

Writing a secret to .env, keychain, or 1Password is covered, but the lifecycle is a dead end: there is no way to verify a secret already exists, rotate it, or delete it. Agents must guess or re-prompt the user, which risks redundant dialogs and stale credentials.

Maintenance

ActivityMaintained
ResponsivenessNo recent community issues