Examples
Four everyday tasks, each with the one-time setup, the request the agent makes and the prompt you approve. They all start the same way: copy the token from the provider's web page and store it with --from-clipboard, so it never passes through the chat or your shell history.
After adding a secret
The MCP server reads the policy when it starts. After keymitt add or keymitt allow, restart the keymitt server in your AI client. In Claude Code: /mcp → keymitt → Reconnect.
Triage GitHub issues
"List the open bugs in my-org/my-app, oldest first, and suggest which ones to close."
Setup. Create a fine-grained token that can read issues, copy it, then:
keymitt add github --description "GitHub token, issues read-only" --host api.github.com --from-clipboardThe agent calls http_request:
{
"url": "https://api.github.com/repos/my-org/my-app/issues?labels=bug&state=open&direction=asc",
"headers": {
"Authorization": "Bearer {{secret:github}}",
"Accept": "application/vnd.github+json"
}
}You approve:
send github → https://api.github.com | GET https://api.github.com/repos/my-org/my-app/issues?labels=bug&state=open&direction=asc | Accept: application/vnd.github+json | Authorization: Bearer {{secret:github}}The agent gets the list of issues. It never sees the token, and it cannot send the token anywhere but api.github.com.
Deploy a site to Cloudflare Pages
"Build the docs and deploy them to the
docsproject on Cloudflare Pages."
Setup. Create an API token with the Cloudflare Pages: Edit permission, copy it, then:
keymitt add cloudflare --description "Cloudflare API token, Pages edit" --command wrangler --env CLOUDFLARE_API_TOKEN --from-clipboardkeymitt prints the program it found, following symlinks to the real file, for example wrangler → /opt/homebrew/lib/node_modules/wrangler/bin/wrangler.js. Only that program can receive the token, and only as CLOUDFLARE_API_TOKEN.
The agent builds the site on its own (no secret needed), then calls run_with_secrets:
{
"command": ["wrangler", "pages", "deploy", "dist", "--project-name", "docs"],
"env": { "CLOUDFLARE_API_TOKEN": "{{secret:cloudflare}}" }
}You approve:
use cloudflare → /opt/homebrew/lib/node_modules/wrangler/bin/wrangler.js (as wrangler) | args: pages deploy dist --project-name docs | env: CLOUDFLARE_API_TOKEN={{secret:cloudflare}} | dir: /Users/you/siteThe agent reads wrangler's output, including the deployment URL. If the token were ever printed, it would arrive as [REDACTED:cloudflare].
Ask the staging database a question
"How many users signed up on staging each day this week?"
Setup. Copy the password of a read-only database user from your password manager, then:
keymitt add pg-staging --description "staging DB, read-only user" --command psql --env PGPASSWORD --from-clipboardThe agent calls run_with_secrets:
{
"command": ["psql", "-h", "staging-db.example.internal", "-U", "readonly", "-d", "app", "-c",
"select date(created_at), count(*) from users where created_at > now() - interval '7 days' group by 1 order by 1"],
"env": { "PGPASSWORD": "{{secret:pg-staging}}" }
}You approve a prompt that contains the whole SQL statement, so you see exactly what runs:
use pg-staging → /opt/homebrew/Cellar/postgresql@18/18.6_1/bin/psql | args: -h staging-db.example.internal -U readonly -d app -c 'select date(created_at), count(*) from users …' | env: PGPASSWORD={{secret:pg-staging}} | dir: /Users/you/appUse a read-only role anyway: keymitt protects the password, not the data behind it.
Debian and Ubuntu
There, psql is a link to pg_wrapper, a script that only works when started under the name psql. Allow the real program instead, for example --command /usr/lib/postgresql/16/bin/psql (the version varies). The agent sees that path in list_secrets and calls it directly.
After an upgrade
keymitt stores the real file's path, and Homebrew's include the version. When an upgrade moves the program, run keymitt allow pg-staging --command psql to point the policy at the new file.
When the agent needs a secret you haven't stored
"Post the release notes to #releases on Slack."
The agent calls list_secrets, finds nothing for Slack, and calls request_secret. You get a message like this, never a request to paste the token into the chat:
Please store this secret yourself by running in a terminal:
keymitt add slack-bot --description 'Slack bot token for release notes' --command <program> --host <api-host>
Replace <program>/<api-host> with what it may be used for (intended use: slack.com chat.postMessage), or
drop the flags you don't need. keymitt will prompt for the value without echoing it; if you have copied
the value, add --from-clipboard to read it from the clipboard instead. …Setup. Copy the bot token from your Slack app's OAuth & Permissions page and run the command with the placeholders filled in:
keymitt add slack-bot --description "Slack bot token for release notes" --host slack.com --from-clipboardReconnect the keymitt server. The agent calls http_request:
{
"method": "POST",
"url": "https://slack.com/api/chat.postMessage",
"headers": {
"Authorization": "Bearer {{secret:slack-bot}}",
"Content-Type": "application/json; charset=utf-8"
},
"body": "{\"channel\": \"#releases\", \"text\": \"keymitt 0.1.0 is out: …\"}"
}The prompt shows the start of the message body as well, so you can check the text before it is posted.
Tips
- Name and describe secrets well. The agent chooses a secret from
list_secretsby its name and description. - Scope tokens narrowly. keymitt controls where a token goes, not what it may do there.
- Look back.
keymitt auditlists every use, with the approval and the outcome, never values.