English | 简体中文
# dsh-tui-secret > Secure secret entry for [dsh-TUI](https://github.com/ccch1mneyyy/dsh-TUI): `/secrets` > opens a full-screen scene with **masked input → reference list → delete confirmation**. > Values only ever reach DSH's own credential seam; the plugin never echoes one, never > logs one, and never puts one into the conversation. > > The agent can ask for one: `request_credential` puts a masked card in front of you and the > value goes straight into the DSH credential store — the tool only gets a status back. > `use_credential` then runs one command with that value in **its own child process**, and the > returned output has every occurrence of the value replaced with `[credential]`. ## Highlights - **Masked entry inside the scene** — `/secrets` opens a full-screen scene: `a` starts a reference, the value is typed with per-keystroke masking, `Enter` writes it. - **Per-character echo** — the value line shows one `•` per character, like a password field. Set `echo: none` to draw nothing at all (not even the length). - **The agent asks, you decide** — `request_credential(ref, purpose)` shows a card with the name, the purpose and four choices: not allowed / once / this session / until a time. - **Stored, never handed over** — the value reaches `ctx.credentials` and nothing else; the tool's result carries a status, and the value never enters the conversation. - **Use without handing it over** — `use_credential(ref, argv)` runs one command with the value in that child process only and replaces it with `[credential]` in the returned output. - **List, delete and revoke, values never shown** — the list reports configured state plus the agent permission: `d` deletes (explicit `y`), `v` revokes one permission, `R` revokes all. - **The declaration is gated** — `npm test` runs `dsh-plugin.json` through the host's own parser and the TUI profile validator, so a drifted manifest fails locally. ## Quick Start Supported target: **`@deepseek-harness-tui/dsh-tui`** 0.16.x — other TUI implementations are out of scope. From the shell: ```sh dsh plugin --profile dsh-tui add github:zerodegress/dsh-tui-secret ``` Or from inside the TUI: ``` /plugin install https://github.com/zerodegress/dsh-tui-secret ``` You can also add one row to the profile's `cordis.patch.yml` (this package's own `cordis.patch.yml` is exactly that row, applied by the bundle mechanism on install): ```yaml - insert: - id: dsh-tui-secret name: dsh-tui-secret config: entryCommand: secrets # any other command name works too echo: dots # one • per character; 'none' hides even the length ``` Restart the TUI and type `/secrets`. ### Agent tools Two tools are registered at runtime (the host has no manifest contract for tools): | Tool | Arguments | Returns | | --- | --- | --- | | `request_credential` | `ref`, `purpose` | `stored` / `cancelled` / `refused` | | `use_credential` | `ref`, `argv[]`, `timeoutMs?` | `exitCode`, `stdout`, `stderr`, `redacted`, `timedOut`, `aborted`, `injectedAs` | `use_credential` runs exactly one command. The value goes into that child process's environment under the name reported back as `injectedAs`; it never enters the shell you or the agent sit in, and the executor's sandbox policy still applies. `argv` reaches the plugin as an array and each element is quoted by the plugin, so the model cannot feed the shell raw text: to use a pipe or a variable, it writes `["sh","-c","…"]` itself and that inner shell expands it. The value is entered by **you**, on the card. Neither tool accepts a value from the model, and neither tool ever returns one. ## Keybindings | Key | Action | | --- | --- | | `a` | New reference: name first, then value | | `d` | Delete the selected reference (`y` confirms) | | `v` | Revoke the agent permission on the selected reference | | `R` | Revoke every agent permission (`y` confirms; the stored values stay) | | `↑` `↓` | Move the selection | | `Enter` | Next step / write | | `Esc` | Back one step; on a card, cancel; on the list, close the scene | On a card the agent asked for, everything is on one screen: type the value (masked), `↑`/`↓` picks the permission, `Enter` stores it and closes straight back to the chat, `Esc` cancels. Digits are part of the value, so the permission is arrows only. Names follow DSH rules: start with an uppercase letter, `A-Z 0-9 _` only, at most 64 characters. `DEEPSEEK_` / `DSH_` prefixes belong to the host — the plugin refuses them on the spot with a readable reason, instead of letting dsh-TUI's `credentialRefGuard` silently drop the ref later. ## How It Works - **Values only go through `ctx.credentials.set(ref, value)`**: no second file, no writes to `.credentials.yaml` itself, no copies. An empty value is refused (in DSH semantics an empty value means unconfigured). - **No value in React state**: it lives in a `useRef` and is dropped locally before the write, so a failure leaves nothing behind in state. - **The default echo reveals the value's length**: one `•` per character is the conventional look, but the count is visible on screen and in scrollback. Use `echo: none` when the length itself must not be shown (a shared screen, a recording). - **No shellEnv contributor is registered**: `$NAME` never enters the environment the model executes in. The only environment that ever holds the value is the one child process `use_credential` spawns. - **The two tools are the whole model-facing surface** (nothing else is registered on `tools/execute` or `llm/stream`): `request_credential` answers with a status, `use_credential` with exit code and redacted output. - **Logs and errors carry the name and an error `code` only**: raw error text may embed the value, so it is never printed or persisted. - **The list shows configured state, source and writability only**, straight from `credentials.describe()`, which is designed not to return a value. These statements are not source-reading claims: `npm test` actually drives the scene frame by frame — see Development. - **Seams** — `ctx.tuiScenes` (full-screen scene), `ctx.commands` (command) and `ctx.credentials` (the DSH core credential service). A missing scene seam degrades silently into "nothing happens" and never breaks TUI startup. ## Storage | Content | Location | Mode | | --- | --- | --- | | Values (owned by DSH) | `` (the registration step threw; the same line is in the log, with the code/name only, never
the raw error text) or `open failed: `. When a card always comes back `refused`, start the
TUI with `DSH_TUI_SECRET_DEBUG=1`: the note on `request_credential` then names the concrete reason
(`the host did not open the scene`, `the card was answered with refuse`, `the tool call was aborted`, …).
A reason never carries a value, and by default a human refusal and a card that cannot be shown stay
indistinguishable — so the failure reason cannot become a probe.
- **The scene half** builds a minimal React hook runtime and a ui kit stand-in, drives the
scene with real key order (`a` → name → `Enter` → value → `Enter`), records every
rendered frame, every log line and every outbound call, then greps for a sentinel value.
Step `⑫` swaps in the host's own React 19 and ink and renders into a non-TTY stream,
frame by frame — element symbols, hook dispatcher and render path are all real.
- **The tools** are driven through a fake host (the same way the scene is driven through a
fake React), and the quoting checks run the built command through a real `/bin/sh`: hostile
arguments must arrive as data, and only a script the model writes itself may expand.
Toolchain: Node `^22.19 || >=24`, ESM, no runtime dependencies;
`@deepseek-ai/cordis` is an optional peer. The three devDependencies are for the build and the
types only — a fresh clone needs `pnpm install` (network) before `npm test`.
Open contract questions are tracked in [docs/plugin-design.md](docs/plugin-design.md).
## Documentation
- **Design** — [plugin-design.md](docs/plugin-design.md) · [documentation index](docs/README.md)
· [dsh-TUI plugin guide](https://github.com/ccch1mneyyy/dsh-TUI/blob/main/tui-profile/docs/plugin-admission-and-development.md)
## License
[MIT](LICENSE)