credential-usage · diff
git:20260917.9f1f407 to git:20260917.f6485e4
85 added, 73 removed. Audit A to A.
---
name: credential-usage
- description: Use when a task needs an API key, token, password, database URL, or other credential, or when the user pastes a credential into chat. macOS with the tsm vault installed.
+ description: Use whenever a task needs an API key, token, password, database URL, certificate, or any other credential. That includes configuring an MCP server, calling an authenticated API with curl, running a CLI like gh/aws/openai, running psql or docker compose against a real service, filling in a .env, or when the user pastes a secret into chat or asks where to keep one. Consult it even when the user never says "secret" or "tsm". macOS with the tsm vault installed.
---
# Using credentials from the tsm vault
- When a task needs an API key, token, password, database URL, or other credential, use the local `tsm` vault before asking the user for it. The vault is biometric-gated (Touch ID) and the user has already approved the patterns below by installing this plugin.
+ `tsm` is a Touch ID-gated secrets vault on this Mac. The user installed this plugin so you pull credentials from it instead of asking, and so you handle them the way this skill describes. `tsm list`, `tsm get`, and `tsm run` are allowlisted for you. Every other `tsm` subcommand (`add`, `edit`, `remove`, `reset`, `init`, `config set`) changes the vault and is the user's to run.
- ## 1. Discover first
+ The first vault access in a session pops a system Touch ID dialog, and the command blocks until the user responds. That is normal. Do not kill or retry it. Later accesses inside the unlock window do not prompt.
- Run `tsm list --json` before assuming a credential is missing. Look for a name, description, or tag that matches what you need.
+ ## Workflow
- ```bash
- tsm list --json
- # [{"name":"gh-pat","display_name":"GitHub PAT","description":"...","confirm":false,"tags":["github","git"]}, ...]
- ```
+ 1. **Look before asking.** `tsm list --json` returns names, display names, descriptions, tags, and the `confirm` flag. Never values. Match on any of those fields.
+ ```bash
+ tsm list --json
+ # [{"name":"gh-pat","display_name":"GitHub PAT","description":"...","confirm":false,"tags":["github","git"]}]
+ ```
+ One match: use it. Several plausible matches: ask which. None: tell the user, suggest a name, and stop. Do not ask for the value unless nothing matches.
+ 2. **Pick the delivery pattern** for the tool (below). Prefer `tsm run` whenever the tool reads an environment variable.
+ 3. **If the entry has `"confirm": true`, warn before using it** (see "Confirm-gated secrets").
- If exactly one entry matches, use it via the patterns below. If several plausibly match, ask the user which one. Only ask for the value itself when no matching secret exists.
+ ## The one rule: the value never lands in an argument list
- ## 2. Pattern by tool category
+ Anything in a process's argv is visible in `ps` to every user on the machine and is written to shell history. So the value has to travel by environment variable, file descriptor, or a 0600 temp file, never as part of a command line. `$(tsm get x)` expanded inside a flag value breaks the rule even though it looks tidy.
- **The rule behind every pattern:** the secret value must never appear in any process's argument list. Anything in argv is visible in `ps` and lands in shell history. `tsm run` (env var), process substitution `<(...)` (the tool sees a `/dev/fd/N` path), and a `mktemp` file keep it out. `$(tsm get ...)` expanded inside an argument does not.
+ Three carriers that respect it:
- ### MCP server credentials
+ - **`tsm run --env VAR=name -- cmd`** sets VAR in the child process only. The parent shell is untouched and the variable is gone when the child exits.
+ - **`<(tsm get name)`** process substitution. The tool receives a `/dev/fd/N` path and the value never touches disk. Works when the tool reads the path once. Your Bash tool runs bash, so this syntax is available to you.
+ - **`mktemp`** gives a 0600 file under the per-user `$TMPDIR`. Redirect into it and `rm` it when done. There is no `/dev/shm` on macOS.
- MCP server configs in `.mcp.json` accept `command`/`args`. Wrap the server in `tsm run`:
+ ## Patterns by tool type
+ ### Anything that reads an env var (gh, aws, openai, anthropic, PGPASSWORD, most SDKs)
+
+ ```bash
+ tsm run --env GITHUB_TOKEN=gh-pat -- gh pr list
+ tsm run --env PGPASSWORD=pg-prod-password -- psql -h db.example.com -U app mydb -f migrate.sql
+ tsm run --env A=key-a --env B=key-b -- ./deploy.sh prod
+ ```
+
+ ### MCP servers in `.mcp.json`
+
+ Wrap the server command so it inherits the credential at startup:
+
```json
- {
- "github": {
- "command": "tsm",
- "args": ["run", "--env", "GITHUB_TOKEN=gh-pat", "--", "github-mcp-server"]
- }
- }
+ { "github": { "command": "tsm", "args": ["run", "--env", "GITHUB_TOKEN=gh-pat", "--", "github-mcp-server"] } }
```
- ### Env-var CLI tools (gh, openai, anthropic, aws, etc.)
+ ### docker compose and docker run
+ A bare key under `environment:` passes the variable through from the parent process, so `tsm run` covers compose with no `env_file:` at all:
+
+ ```yaml
+ services:
+ worker:
+ environment: [SENTRY_DSN]
+ ```
```bash
- tsm run --env GITHUB_TOKEN=gh-pat -- gh pr list
- tsm run --env OPENAI_API_KEY=openai-key -- openai api models.list
+ tsm run --env SENTRY_DSN=sentry-dsn -- docker compose up worker
```
- For `docker compose`, a bare `environment: [SENTRY_DSN]` entry passes the variable through from `tsm run`'s environment, which avoids `env_file:` entirely:
+ Plain `docker run` has no pass-through, so generate an `--env-file` in a temp file:
```bash
- tsm run --env SENTRY_DSN=sentry-dsn -- docker compose up worker
+ F=$(mktemp) && tsm get gh-pat --format "env GITHUB_TOKEN" > "$F" && docker run --env-file "$F" some-image; rm -f "$F"
```
- ### HTTP calls with curl
+ ### curl and other HTTP clients
- curl reads extra headers from a file with `-H @file`. In bash (the shell your Bash tool runs), `printf` is a builtin running inside the process substitution, so the token reaches curl only through a file descriptor:
+ curl reads extra headers from a file with `-H @file`. `printf` is a bash builtin, so the header line is assembled inside the shell and reaches curl only through a file descriptor:
```bash
curl -H @<(printf 'Authorization: Bearer %s\n' "$(tsm get gh-pat)") https://api.github.com/user
```
- Do not write `curl -H "Authorization: Bearer $(tsm get gh-pat)"`. That expands the token into curl's argv.
+ `curl -H "Authorization: Bearer $(tsm get gh-pat)"` is the tempting version, and it puts the token in curl's argv.
- ### File-flag tools (curl --cacert, gcloud --key-file, psql PGPASSFILE)
+ ### Tools that take a file path (`--cacert`, `--key-file`, `PGPASSFILE`)
- If the tool reads the path once, process substitution keeps the secret off disk:
+ If the tool reads the path once, process substitution keeps the value off disk:
```bash
curl --cacert <(tsm get ca-cert) https://internal.example.com
```
- If the tool re-reads the path or requires a regular file, write a `mktemp` file and delete it afterward. libpq is the common case: it rejects a `PGPASSFILE` that is not a plain 0600 file, so `<(...)` does not work for pgpass. `mktemp` creates the file with mode 0600 under the per-user `$TMPDIR`; a redirect into it, or `tsm get --to-file` for a raw value, keeps that mode:
+ If the tool insists on a regular file, or re-reads it, use a temp file. libpq is the usual case: it ignores a `PGPASSFILE` that is not a plain 0600 file, so `<(...)` does not work for pgpass.
```bash
- PGPASSFILE=$(mktemp) && tsm get pg-prod --format pgpass > "$PGPASSFILE" && \
- PGPASSFILE="$PGPASSFILE" psql --no-password "service=mydb" -f migrate.sql ; rm -f "$PGPASSFILE"
+ F=$(mktemp) && tsm get pg-prod --format pgpass > "$F" && PGPASSFILE="$F" psql --no-password "service=mydb" -f migrate.sql; rm -f "$F"
```
- There is no `/dev/shm` on macOS.
+ The `pgpass` formatter expects the stored value to already be a `host:port:db:user:password` row. If the vault holds only the password, skip the file and use `PGPASSWORD` with `tsm run` as shown above.
- ### Wire-format-specific tools
+ ### Wire formats: `tsm get --format`
- For tools that demand a specific wire format, use `tsm get --format` and redirect into a `mktemp` file. `--format` cannot be combined with `--to-file`; the redirect keeps the 0600 mode `mktemp` set:
+ Built-in formatters: `env VAR`, `pgpass`, `aws-credential-process`. `--format` refuses to write to a TTY and cannot be combined with `--to-file`, so redirect into a `mktemp` file; the redirect keeps the file's 0600 mode. AWS is the exception: the CLI runs `credential_process` itself and reads stdout, so the command goes in `~/.aws/config` with no redirect:
- ```bash
- tsm get aws-prod --format aws-credential-process # AWS credential_process JSON
- tsm get pg-prod --format pgpass # validates the value as a pgpass row
- ENVFILE=$(mktemp) && tsm get gh-pat --format "env GITHUB_TOKEN" > "$ENVFILE" && \
- docker run --env-file "$ENVFILE" some-image ; rm -f "$ENVFILE"
+ ```ini
+ [profile prod]
+ credential_process = tsm get aws-prod --format aws-credential-process
```
- `tsm get --format` refuses to write to a TTY; always redirect the output.
-
- ### Tools that write their own env or config file
-
- Some tools persist their environment to a fixed project-local path on startup (a test harness that dumps `.env.test`, a script that writes `.env` from `process.env`). If the tool you are launching does this, tell the user before launching it, and delete that file when the process exits.
+ ### Tools that write their own env file
- ## 3. Confirm-gated secrets
+ Some tools dump their environment to a fixed project path on startup: a test harness that writes `.env.test`, a script that materializes `.env` from `process.env`. If the tool you are about to launch does this, say so before launching, and delete that file when the process exits. Otherwise the vault's protection ends the moment the tool starts.
- Secrets flagged `"confirm": true` in `tsm list --json` trigger a fresh Touch ID prompt on every access, even when the vault is already unlocked. **Check this flag during discovery (§1).**
+ ## Confirm-gated secrets
- The prompt is a system dialog presented by the tsm daemon in the user's GUI login session, so it works from a non-TTY shell like yours. **Warn the user before you trigger it**, otherwise a Touch ID dialog pops up unexplained:
+ Entries with `"confirm": true` prompt Touch ID on every access, even inside the unlock window. The daemon presents the dialog in the user's GUI session, so it works from your non-TTY shell, but a dialog that appears with no explanation is alarming. Say what you are about to do and that a prompt will appear:
- > "I'm about to start the server with `anthropic-api-key`, which is confirm-gated — you'll get a Touch ID prompt to approve. For a long-running process it's a one-time cost at startup."
- > ```bash
- > tsm run --env ANTHROPIC_API_KEY=anthropic-api-key -- node server.js
- > ```
+ > Starting the server with `anthropic-api-key`, which is confirm-gated, so you'll get one Touch ID prompt at startup.
- `tsm run` refuses a confirm-gated secret only when no GUI login session exists (CI, cron, ssh without a console session):
+ When there is no GUI login session (ssh without a console session, cron, CI), `tsm run` refuses with:
```
refusing to run: secret(s) require Touch ID confirmation but no biometric prompt can be presented here (no GUI login session): <name>
```
- If you hit that, hand the user a command to run where Touch ID is available, or **suggest** they drop confirm mode with `tsm edit <name>`. Never run `tsm edit` yourself (§4); dropping a Touch ID gate is the user's call.
+ Hand the user a command to run where Touch ID is available. Dropping the gate with `tsm edit` is their decision. You can suggest it, never run it.
- ## 4. Never
+ ## Saving a credential the user shares with you
- - **Never** echo, print, log, or include a secret value in your output to the user.
- - **Never** write secrets to `.env`, `.envrc`, project-local config files, or any path other than a `mktemp` file. Delete the `mktemp` file when the command finishes.
- - **Never** put a secret value in a command argument: `--token X`, `-H "Bearer X"`, `--value X` (`tsm add --value` does not exist for this reason). Argument values appear in `ps` and shell history.
- - **Never** run `tsm add`, `tsm edit`, `tsm remove`, `tsm reset`, `tsm init`, or `tsm config set`. These mutations are user-driven. When the user wants to save a credential they shared with you, hand off with a one-liner that keeps the value off the shell command line and out of shell history. Pick whichever fits:
- - **Clipboard** (smoothest — user copies the value from chat, then runs):
- ```bash
- pbpaste | tsm add --no-input --name <kebab-id> --display-name "<Display Name>"
- ```
- - **File** (for multi-line values like JSON blobs, or a value you already wrote to a `mktemp` file — see below):
- ```bash
- tsm add --name <kebab-id> --display-name "<Display Name>" --from-file /path/to/tmpfile && rm /path/to/tmpfile
- ```
+ When the user pastes a credential into chat, or asks you to store one, use it for the task in hand and then hand off the save. The value must not pass through a command line at any step:
- Do not suggest a heredoc — heredocs go in shell history. After the secret is saved, remind the user the chat transcript still has the value, so rotation may be worth considering.
- - **Never** use `eval $(tsm get ... --format env)`. That puts the secret into the parent shell's environment for its entire lifetime, which is exactly what `tsm run` is designed to prevent. Use `tsm run` for env-var injection.
+ 1. Run `mktemp` for a 0600 path and write the raw value there with your file-editing tool. Not `echo`, not a heredoc; both put the value in argv or shell history.
+ 2. Use that file wherever you would have used `tsm get`: `-H @<(printf 'Authorization: Bearer %s\n' "$(cat /path/from/mktemp)")` for curl, or the path itself for a file-flag tool.
+ 3. Give the user one command that saves it, with the real temp path filled in:
+ ```bash
+ tsm add --name <kebab-id> --display-name "<Display Name>" --from-file /path/from/mktemp && rm /path/from/mktemp
+ ```
+ If they would rather copy the value from chat than trust your file:
+ ```bash
+ pbpaste | tsm add --no-input --name <kebab-id> --display-name "<Display Name>"
+ ```
+ 4. Delete the temp file once it is saved or no longer needed, and mention that the chat transcript still holds the value, so rotating it is worth considering.
- ## When tsm doesn't apply
+ ## Never
- - The user pastes a credential inline in chat — do not paste it into a shell command. Run `mktemp` to get a 0600 path and write the raw value there with your file-editing tool (not `echo` or a heredoc). Use that file in place of `tsm get`, for example `curl -H @<(printf 'Authorization: Bearer %s\n' "$(cat "$F")") …`, or pass it directly to a file-flag tool. Then offer the `--from-file` handoff in §4 on that same path so the user doesn't retype it. Delete the file once it is saved or no longer needed.
- - The tool uses local OAuth that owns its own token lifecycle (gcloud user-OAuth, GitHub CLI's `gh auth login` flow). Use the tool's native auth; tsm doesn't help here.
- - The vault is empty or no relevant secret exists — tell the user, suggest a name and `tsm add`, and stop there.
+ - Print, log, or quote a secret value in your reply. Not even a prefix.
+ - Write a value into `.env`, `.envrc`, a project config file, or any path that is not a `mktemp` file.
+ - `eval "$(tsm get x --format 'env X')"`. That plants the secret in the parent shell for its whole lifetime, which is exactly what `tsm run` exists to avoid.
+
+ ## When tsm is not the answer
+
+ - The tool owns its own OAuth flow (`gcloud auth login`, `gh auth login`). Use that; the vault adds nothing.
+ - No entry matches. Say so, propose a kebab-case name, and let the user run `tsm add`. Do not guess at a value.