{"id":"fe89d65e-b83e-4ee6-82d9-f220943709a9","slug":"secureinput","kind":"mcp","name":"secureinput","summary":"Local MCP server for API keys and secrets: inline or browser input, command stdin, and .env delivery.","owner":"r69shabh","repoUrl":"https://github.com/r69shabh/secureinput","license":"MIT","licenseTier":"permissive","trust":"community","stars":0,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-11-02T13:14:55.000Z","addedAt":"2026-11-08T04:32:26.523Z","logoUrl":"https://avatars.githubusercontent.com/u/143521130?u=8de85bf88529112546748737f6c370778ba02cc6&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["cloud-infra","browser-web","developer-tools"],"tags":["ai-agents","api-keys","claude-code","coding-agents","cursor","local-first","secrets","security"],"runtime":"node","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-11-08T04:38:38.169Z","sample":false,"url":"https://conduits.dev/tools/secureinput","language":"TypeScript","transports":["stdio"],"install":{"npm":"secureinput"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# SecureInput\n\n**Your agent needs the token. It doesn't need to see it.**\n\n[![CI](https://github.com/r69shabh/secureinput/actions/workflows/ci.yml/badge.svg)](https://github.com/r69shabh/secureinput/actions/workflows/ci.yml)\n[![MCP Registry](https://img.shields.io/badge/MCP_Registry-io.github.r69shabh%2Fsecureinput-2F4060)](https://registry.modelcontextprotocol.io/v0/servers?search=secureinput)\n[![License: MIT](https://img.shields.io/badge/license-MIT-2F4060)](LICENSE)\n\nSecureInput is an open-source, local-first MCP server for API keys, passwords, and tokens. When your coding agent needs a credential, you type it into a small input that appears **inside the chat**, styled like the host's own composer. The value goes straight to a local command (stdin or env) or a `.env` file. The agent only gets back `{ \"ok\": true, \"status\": \"completed\", \"exitCode\": 0 }`.\n\nWorks with Claude Code, Claude Desktop, Cursor, Codex, VS Code / GitHub Copilot, Windsurf, Cline, and any stdio MCP client. macOS and Linux, Node.js 22+.\n\n**[Install](#install)** · **[How it works](#how-it-works)** · **[Security model](docs/security-model.md)** · **[FAQ](#faq)** · [Watch the 20-second overview](docs/media/secureinput-overview.mp4)\n\n## Install\n\nPick your client. Every option runs `npx -y secureinput mcp` (Node.js 22+), which starts the shared local daemon on first use. Try it with a **fake** key first.\n\n[![Add to Cursor](https://img.shields.io/badge/Add_to-Cursor-2F4060?logo=cursor)](https://cursor.com/en/install-mcp?name=secureinput&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsInNlY3VyZWlucHV0IiwibWNwIl19)\n[![Install in VS Code](https://img.shields.io/badge/Install_in-VS_Code-2F4060?logo=visualstudiocode)](https://insiders.vscode.dev/redirect?url=vscode%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522secureinput%2522%252C%2522type%2522%253A%2522stdio%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522-y%2522%252C%2522secureinput%2522%252C%2522mcp%2522%255D%257D)\n[![Download for Claude Desktop](https://img.shields.io/badge/Download_for-Claude_Desktop-2F4060?logo=anthropic)](https://github.com/r69shabh/secureinput/releases/latest)\n\n### 1. Connect your agent\n\n<details open>\n<summary><b>Claude Code</b></summary>\n\n```bash\nclaude mcp add --scope user secureinput -- npx -y secureinput mcp\nclaude mcp list     # secureinput: ✔ Connected\n```\n</details>\n\n<details open>\n<summary><b>Claude Desktop</b>: one-file bundle, no terminal</summary>\n\n1. Download the latest `secureinput-release-*.mcpb` from [GitHub Releases](https://github.com/r69shabh/secureinput/releases/latest). The bundle is unsigned; review the release before installing.\n2. Open **Settings → Desktop app → Manage** next to Extensions (older versions: **Settings → Extensions**), then **Advanced settings → Install Extension…** and pick the file. See [Claude's guide](https://support.claude.com/en/articles/10949351-getting-started-with-local-mcp-servers-on-claude-desktop).\n3. Ask Claude to save a fake key with SecureInput.\n\nTo upgrade, install the newer `.mcpb`.\n</details>\n\n<details>\n<summary><b>Cursor</b>: use the button above, or <code>~/.cursor/mcp.json</code></summary>\n\n```json\n{ \"mcpServers\": { \"secureinput\": { \"command\": \"npx\", \"args\": [\"-y\", \"secureinput\", \"mcp\"] } } }\n```\n</details>\n\n<details>\n<summary><b>VS Code / GitHub Copilot</b>: use the button above, or <code>.vscode/mcp.json</code></summary>\n\n```json\n{ \"servers\": { \"secureinput\": { \"type\": \"stdio\", \"command\": \"npx\", \"args\": [\"-y\", \"secureinput\", \"mcp\"] } } }\n```\n</details>\n\n<details>\n<summary><b>Codex CLI</b></summary>\n\n```bash\ncodex mcp add secureinput -- npx -y secureinput mcp\n```\n\nOr in `~/.codex/config.toml`:\n\n```toml\n[mcp_servers.secureinput]\ncommand = \"npx\"\nargs = [\"-y\", \"secureinput\", \"mcp\"]\n```\n</details>\n\n<details>\n<summary><b>Windsurf, Cline, or any stdio client</b></summary>\n\n```json\n{ \"mcpServers\": { \"secureinput\": { \"command\": \"npx\", \"args\": [\"-y\", \"secureinput\", \"mcp\"] } } }\n```\n</details>\n\n<details>\n<summary><b>Global install or from source</b></summary>\n\n```bash\nnpm install -g secureinput     # then use command \"secureinput\", args [\"mcp\"]\nsecureinput doctor             # checks Node, config and the daemon\n```\n\nFrom source: `git clone https://github.com/r69shabh/secureinput.git && cd secureinput && npm ci && npm run build && npm link`.\n\nGUI apps may not see a Node version manager's PATH (fnm, nvm, volta). If `npx` isn't found, use the full path from `realpath \"$(which npx)\"` as the command.\n</details>\n\n### 2. Tell your agent to use it\n\nAdd this to your agent rules (`CLAUDE.md`, `AGENTS.md`, `.cursor/rules`, …) so it reaches for the tool instead of asking you to paste:\n\n```text\nWhen you need an API key, password, or token, call the SecureInput MCP tool\nrequest_secret. Do not ask me to paste secrets into chat. Prefer delivery\nmode \"execute\" with the secret in stdin/env, or \"write_env\" for .env files.\n```\n\n## How it works\n\n```\nagent: \"I need a GitHub token to create the repo.\"\n        │  request_secret (MCP)\n        ▼\n  input inside the chat (MCP Apps hosts)  ─or─  paired page on 127.0.0.1\n        │  encrypted to the local daemon\n        ▼\n  SecureInput daemon on 127.0.0.1\n        ├── execute:   gh auth login --with-token   (token through stdin)\n        └── write_env: .env.local                   (mode 0600)\n        │\n        ▼\nagent: { \"ok\": true, \"status\": \"completed\", \"exitCode\": 0 }\n```\n\n### Feels native in every host\n\nOn hosts that support [MCP Apps](https://modelcontextprotocol.io/docs/extensions/apps), the input renders inline in the conversation and adopts the host's own theme, fonts, colours, corner radii, and focus ring from the design tokens the host provides. In Claude it looks like Claude's composer; in a dark-mode client it's dark. Hosts that send no tokens get a neutral light/dark fallback. Press Enter to submit; there is no extra SecureInput dialog.\n\nText-only clients automatically fall back to a paired page on `127.0.0.1`, which also shows the destination and a risk review before anything runs. Force it with `secureinput settings --input-mode browser`.\n\n### What stays out of the chat\n\n| | Pasting into chat | SecureInput |\n| --- | --- | --- |\n| Model context and transcript | ✗ contains the key | ✓ status only |\n| Shell history / `ps` | often | ✓ never: stdin or env, no shell |\n| `.env` permissions | whatever you typed | ✓ atomic write, `0600` |\n| Reuse of a request | n/a | ✓ one-time, expires (5 min default) |\n\nSecureInput keeps the value out of the MCP result, but the commands you approve, the host that renders the inline form, and anything that can read a written `.env` file are still trusted. Read the [security model](docs/security-model.md) before using real credentials.\n\n## Features\n\n- **One MCP tool**: `request_secret`, plus check, cancel, and status tools.\n- **Local only**: binds `127.0.0.1`; pairing token, signed session cookie, Origin and CSRF checks.\n- **No secret in results by default**: the agent sees status and exit code; stdout/stderr are withheld.\n- **Direct execution**: no shell; secrets through stdin or env, rejected in command arguments.\n- **Safe `.env` writes**: atomic replace, `0600`, comments and line endings kept, `.env` / `.env.*` only, no overwrites unless requested.\n- **Encrypted inline submission**: AES-256-GCM, key wrapped with a fresh RSA-OAEP key pair held only by the daemon.\n- **Allowed roots**: commands and `.env` files must sit inside your allowed folders (home by default).\n- **Defense in depth**: RE2-safe patterns, size caps, ANSI stripping, output redaction, audit log without values.\n- **Opt-in return mode**: for clients that truly need the value back; off by default and marked dangerous.\n\n## Inline input details\n\nThe inline form encrypts values and a one-request capability with AES-256-GCM; a fresh RSA-OAEP-SHA-256 key pair wraps the encryption key, and only the daemon holds the private key. The capability and public key travel in widget-only tool-result metadata, which the host must keep out of model context. The widget needs no network access: it submits ciphertext through the host's MCP bridge. This does **not** protect against a malicious host that alters widget code or keys; the browser mode keeps a stronger boundary from the chat host. Neither mode sandboxes an approved command.\n\nWhen you rebuild the widget, a connected server announces the new version; clients that honour tool-list change notifications pick it up without reconnecting. Server or daemon changes still need a restart. SecureInput never downloads or runs updates on its own.\n\nTry the real widget with a fake key in a reference host:\n\n```bash\nnpm run build\nnpm run demo:host\n```\n\nOpen the printed loopback URL and submit a fake key. The demo writes `.env.inline-demo` with mode `0600` in a temporary directory. The reference host is a test fixture, not proof that a particular chat product supports inline apps.\n\nUseful commands: `secureinput status`, `secureinput doctor`, `secureinput pair` (browser fallback), `secureinput stop`. The daemon listens on `http://127.0.0.1:4780`; config lives in `~/.secureinput/` with mode `0600`.\n\n## MCP tools\n\n### `request_secret`\n\nCreates a secure input request and optionally waits for the user.\n\n```json\n{\n  \"title\": \"GitHub CLI login\",\n  \"description\": \"Authenticate gh so I can create and push the repository.\",\n  \"fields\": [\n    {\n      \"id\": \"token\",\n      \"label\": \"GitHub token\",\n      \"description\": \"A fine-grained token with repo access.\",\n      \"type\": \"password\",\n      \"minLength\": 20\n    }\n  ],\n  \"delivery\": {\n    \"mode\": \"execute\",\n    \"command\": \"gh\",\n    \"args\": [\"auth\", \"login\", \"--with-token\"],\n    \"stdin\": \"{{token}}\",\n    \"timeoutSeconds\": 60\n  },\n  \"waitSeconds\": 30\n}\n```\n\nFields:\n\n- `title` — short request name, max 100 characters.\n- `description` — why the secret is needed, max 1000 characters.\n- `cwd` — optional working directory, resolved relative to the MCP client's working directory; must resolve inside an allowed root.\n- `fields` — 1–10 inputs. Each has `id`, `label`, optional `description`, `type` (`password` or `text`), `placeholder`, `minLength`, `maxLength`, `pattern` (RE2), `required`, `sensitive`, and `autocomplete`.\n- `delivery` — `execute`, `write_env`, or `return` (disabled by default).\n- `timeoutSeconds` — request lifetime, 30–900 seconds. Default 300.\n- `waitSeconds` — how long this tool call blocks, 0–240 seconds. Default 30.\n\nIf the user has not submitted within `waitSeconds`, the result is `{ \"requestId\": \"req_…\", \"status\": \"pending\", \"inputMode\": \"inline\" }` (or `\"browser\"`). Call `check_secret_request` with the returned request ID.\n\n### `check_secret_request`\n\nLong-polls an existing request. Waits up to 45 seconds.\n\n```json\n{ \"requestId\": \"req_…\", \"waitSeconds\": 30 }\n```\n\n### `cancel_secret_request`\n\nCancels a request that is still pending.\n\n```json\n{ \"requestId\": \"req_…\" }\n```\n\n### `secureinput_status`\n\nReturns input mode, return-mode state, and pending request IDs/statuses. Full browser/API policy responses are unchanged.\n\n## Delivery modes\n\n### `execute` — run a command with the secret\n\n```json\n{\n  \"mode\": \"execute\",\n  \"command\": \"gh\",\n  \"args\": [\"auth\", \"login\", \"--with-token\"],\n  \"cwd\": \".\",\n  \"stdin\": \"{{token}}\",\n  \"env\": {},\n  \"timeoutSeconds\": 120\n}\n```\n\n- The command is spawned directly with `shell: false`. There is no shell interpolation by SecureInput.\n- Secret fields can appear in `stdin` or `env` values as `{{fieldId}}`. They cannot appear in `command` or `args`. Environment variables can still be visible to other processes on some systems; prefer stdin when available.\n- Non-sensitive `text` fields may be used in `args` for things like usernames.\n- At least one secret reference must go through `stdin` or `env`.\n- stdout and stderr are withheld from the agent. The paired local browser can show size-capped, best-effort redacted output.\n- Child processes receive basic OS variables (such as PATH, HOME and temporary-directory settings), fixed terminal settings, and the explicit delivery env mapping. Other daemon environment variables are not inherited.\n- The process group is terminated on timeout.\n- Commands must be non-interactive. TTY-only login flows are not supported; use the tool's token/non-interactive mode instead.\n\n### `write_env` — write a `.env` file\n\n```json\n{\n  \"mode\": \"write_env\",\n  \"path\": \".env.local\",\n  \"mapping\": {\n    \"secret\": \"STRIPE_SECRET_KEY\"\n  },\n  \"overwrite\": false\n}\n```\n\n- Only `.env` and `.env.*` files are allowed, and names containing `example`, `sample`, `template`, `dist`, `defaults`, or `schema` are rejected. Script-like names such as `.envrc` are not accepted.\n- The parent directory must exist and resolve inside an allowed root.\n- Symlinks are rejected.\n- Existing keys require `overwrite: true`. The browser asks for overwrite confirmation; inline submission authorizes the requested overwrite without a separate confirmation.\n- Writes are atomic and the resulting file gets mode `0600`. Existing comments, `export` prefixes, quoting style, and line endings are preserved.\n- Files larger than 1 MiB are refused.\n\n### `return` — hand the secret back to the agent (dangerous)\n\nDisabled by default. Start the daemon with `--allow-return` to enable it:\n\n```bash\nsecureinput serve --allow-return\n```\n\nThe setting is persisted in the config file. Return mode puts the secret in the model context and transcript. Use it only when `execute` and `write_env` cannot solve the problem.\n\n## What the agent sees\n\nMCP responses are compact, minified status objects. Full request details are sent only in widget-only `_meta`; they are not repeated in model-visible content. The inline widget does not poll while input is pending. After submission it long-polls an app-only status tool, keeping UI updates out of model context.\n\nA completed `execute` request looks like this:\n\n```json\n{\"requestId\":\"req_…\",\"status\":\"completed\",\"exitCode\":0}\n```\n\nThe secret does not appear in the tool result, the request object, the SSE stream, the audit log, or the server log.\n\n## Security model\n\nSecureInput is built for the local-desktop threat model, not for hostile multi-user hosts.\n\nSee the [security and trust guide](docs/security-model.md) for a diagram, data locations, and a practical checklist.\n\n**Protected by design**\n\n- Loopback-only server, with a Host-header check that rejects DNS-rebinding attempts.\n- 256-bit random MCP and pairing tokens stored in a `0600` config file.\n- Pairing token exchanged for a signed, HttpOnly, `SameSite=Strict` session cookie.\n- Same-origin checks and a custom CSRF header on state-changing browser requests.\n- Strict CSP, `frame-ancestors 'none'`, no CORS, no-store on API and HTML entry points.\n- One-time request claim, per-request TTL, pending caps, and rate limits.\n- Constant-time token comparison and pairing rate limiting.\n- RE2 validation patterns, which avoid regex denial-of-service.\n- Submitted field values are not deliberately put in URLs, argv, logs, audit records, or error messages. The initial pairing URL contains a separate pairing token; keep it private.\n- Output redaction covers raw, JSON-escaped, URL-encoded, and base64 forms of each secret, plus common token formats.\n- Command execution with `shell: false`; timeout kills the process group.\n- Env writes are atomic, `0600`, symlink-safe, and confined to allowed roots.\n- Risk assessment marks shell/network/absolute-path commands, env overwrites, and return mode. The browser flow adds review checkboxes; the inline flow does not show those checkboxes and submission authorizes delivery.\n- Audit log records metadata and outcomes only: request ID, client, delivery destination, field names, exit code, duration.\n\n**Outside the boundary**\n\n- A command you approve can exfiltrate the secret. Reviewing the exact command and destination is the final control.\n- An agent with filesystem or terminal access may be able to read a `.env` file after it is written, or run commands that read it. Use `execute` with stdin when you do not want a persistent file.\n- Same-user malware, root, a compromised browser, a malicious extension, or a keylogger can read anything you type. SecureInput does not claim to stop them.\n- Environment variables can be visible to other processes running as the same user on some systems. Prefer stdin when the command supports it.\n- Redaction is best-effort for transformed secrets (hashed, split, encrypted, or re-encoded output).\n- Pending requests and secret values live only in memory. Restarting the daemon discards them.\n- `.env` files are plaintext. SecureInput sets safe permissions but does not encrypt anything.\n\n## Configuration\n\nDefault config directory: `~/.secureinput/`\n\n| File | Purpose |\n| --- | --- |\n| `config.json` | Port, tokens, allowed roots, return/audit/browser settings. Mode `0600`. |\n| `runtime.json` | Running daemon PID and port. |\n| `audit.jsonl` | Metadata-only audit trail. No secret values. |\n| `server.log` | Daemon log. No secret values. |\n\nEnvironment variables:\n\n| Variable | Meaning |\n| --- | --- |\n| `SECUREINPUT_HOME` | Override the config directory. |\n| `SECUREINPUT_NO_OPEN` | Set to `1` to prevent automatic browser opening. |\n| `SECUREINPUT_WEB_DIR` | Override the built web UI directory. |\n\nAllowed roots default to your home directory. Tighten this when starting the daemon:\n\n```bash\nsecureinput serve --root ~/projects\n```\n\nRepeat `--root` for multiple directories. Values are resolved and persisted on first run. Restart the daemon after changing them.\n\n## CLI reference\n\n```text\nsecureinput serve [--port 4780] [--root <path>]... [--allow-return]\n                  [--no-open] [--no-audit] [--home <path>] [--web-dir <path>]\nsecureinput mcp   [--home <path>] [--port <port>]\n                  [--client-name <name>] [--client-version <version>]\nsecureinput pair  [--home <path>] [--port <port>] [--no-open]\nsecureinput status\nsecureinput stop\nsecureinput doctor\n```\n\n- `serve` runs the daemon and opens the pairing page.\n- `mcp` runs the stdio bridge and auto-starts the daemon if needed.\n- `pair` starts the daemon if needed and prints a pairing URL.\n- `status` shows daemon health, policy, and pending requests.\n- `stop` stops the daemon.\n- `doctor` checks Node version, daemon reachability, and runtime PID.\n\nThe MCP bridge inherits your current working directory and reports it to the daemon. That directory must be inside an allowed root; otherwise the request is rejected instead of silently falling back.\n\n## HTTP API and HTTP MCP\n\nThe daemon also exposes a stateless Streamable HTTP MCP endpoint:\n\n```text\nPOST http://127.0.0.1:4780/mcp\nAuthorization: Bearer <mcpToken from ~/.secureinput/config.json>\nContent-Type: application/json\n```\n\nThis is useful for clients or test harnesses that prefer HTTP over stdio. The endpoint uses JSON responses rather than long-lived SSE sessions. It is still loopback-only and requires the MCP token.\n\nThe browser UI uses authenticated REST and SSE endpoints under `/api`. Those are implementation details and may change; the MCP tools are the stable interface.\n\n## Choose a delivery method\n\n| Method | Useful for | What persists |\n| --- | --- | --- |\n| `execute` with stdin | A non-interactive CLI login or command that accepts a token on stdin | The command may store credentials itself |\n| `execute` with environment variables | A command that requires an environment variable | The command may store credentials; other same-user processes may see its environment |\n| `write_env` | A local application that reads a `.env` file | A plaintext file with owner-only permissions |\n\nUse the browser input mode when you need to review the destination and risk confirmations in SecureInput itself. The compact inline form shows the input label and submits immediately; it does not show the browser's review screen. A person must be available to enter the value in either mode.\n\n## FAQ\n\n**Does the coding agent see my API key?**\nThe MCP result does not intentionally return it by default. The agent sees the request ID, status and exit code; stdout/stderr stay local. An approved command can still write or transmit values, and an agent with terminal or filesystem access may be able to read a written `.env` file. `return` mode explicitly gives the value to the agent and is disabled unless you start the daemon with `--allow-return`.\n\n**Can I enter a secret inside the agent chat?**\nYes, by default with a compatible MCP Apps host. The widget encrypts its submission before passing it through the host to the local daemon. It uses the same local command/env-file delivery implementation; Enter authorizes inline delivery, while the browser retains its review controls. The host still controls rendering and widget metadata, so use inline input only in a host you trust. See the input-mode settings below. This is not a claim of compatibility with every chat client.\n\n**Where are secrets stored?**\nSecureInput keeps pending requests and submitted values in process memory and drops references after delivery; JavaScript memory is not guaranteed to be zeroized. `write_env` intentionally writes plaintext to the `.env` file you approve. Commands may also persist credentials themselves. Completed request metadata is kept in memory for 10 minutes, and only metadata is written to the audit log.\n\n**Does it work with Claude Code?**\nYes. Run `claude mcp add secureinput -- secureinput mcp`. The same stdio bridge works with Cursor, Codex CLI, VS Code Copilot, Cline, Windsurf, and other MCP clients.\n\n**Does it work with Cursor?**\nYes. Add the `mcpServers` entry shown above to Cursor's MCP config.\n\n**Why not just use a password manager?**\nA password manager is great for humans, but it is not an MCP delivery path. SecureInput focuses on getting a secret into a specific command or env file with an auditable destination, without an agent API key.\n\n**Can the agent read my whole environment?**\nA command receives a small set of OS environment variables plus the values explicitly supplied in the delivery mapping. It still runs with your OS user's permissions and may read files or credential stores. The browser mode shows the command and adds confirmations for high-risk requests; the compact inline mode does not show those details. Use the smallest credential and the narrowest scope you can.\n\n**What happens if the daemon restarts?**\nPending requests and completed results are lost. The agent will receive a \"request not found\" error and can create a new request. This is deliberate: no secret vault, no stale credentials.\n\n**Can I use it over SSH or in a dev container?**\nThe daemon only listens on loopback. If your browser and agent run on the same machine, it works. For remote containers, forward the port over SSH and pair from that machine's browser session; do not expose port 4780 publicly. Remote/multi-user hosting is out of scope.\n\n**What about Windows?**\nThe code handles Windows process termination and paths, but CI currently runs on Linux. Treat Windows as best-effort for now.\n\n**Is the browser UI built with shadcn/ui only?**\nThe UI uses shadcn/ui components, Radix primitives, Tailwind, and lucide icons. There are no third-party UI kits or external assets.\n\n## Development\n\n```bash\nnpm ci\nnpm run typecheck\nnpm test\nnpm run build\n```\n\n- `npm run dev` builds the web UI and starts the daemon without opening a browser.\n- `npm run dev:web` runs Vite on port 5173 and proxies `/api` to the daemon.\n- `npm test` runs node:test unit and integration tests, including auth, CSRF, Origin/Host rejection, SSE, MCP over HTTP, one-time submit, expiry, redaction, env writes, and stdio bridge behavior.\n\nProject layout:\n\n```text\nsrc/shared/   schemas, validation, redaction, env parser\nsrc/server/   daemon, auth, policy, request service, delivery, audit\nsrc/mcp/      stdio bridge\nsrc/cli/      command-line entry point\nsrc/web/      React + Vite + Tailwind + shadcn/ui\ntest/         node:test unit and integration tests\n```\n\nSee [PLAN.md](PLAN.md) for the detailed design and threat model, [SECURITY.md](SECURITY.md) for vulnerability reporting, and [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines.\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n"}