Use
printf 'ghp_xxx' | passbox add github/token
passbox ls
passbox get github/tokenComing from pass
passbox import-pass # everything
passbox import-pass acme # one namespace
passbox import-pass acme/token # one entryEach entry is decrypted through GPG and re-encrypted to the store key. The whole body is kept, so the key: value lines many pass entries carry under the password survive. Entries already in the store are skipped unless you pass --force, and nothing in pass is changed or removed.
Giving a secret to an agent
The value goes into one child process and never into the agent.
passbox exec --env GITHUB_TOKEN=github/token -- gh api /user
passbox exec --stdin db/password -- psqlPrefer --stdin where the tool accepts it. Any process running as you can read a child's environment with ps eww.
Set PASSBOX_AGENT to name the caller in the prompt. Without it, passbox reads the process chain and uses the first ancestor that is not a shell, such as claude or codex.
PASSBOX_AGENT=claude-code passbox exec --env GH_TOKEN=github/token -- gh pr listThe prompt then reads passbox is trying to release the password for github/token to claude-code. macOS writes the opening clause from the binary name, and passbox writes the rest.
Through MCP
An agent asks through tools rather than a shell, and the tools cannot hand it a value at all.
claude mcp add passbox -- passbox mcp # Claude Code
codex mcp add passbox -- passbox mcp # CodexAny client that speaks MCP over stdio works, and the command is always passbox mcp. For one configured by file:
{
"mcpServers": {
"passbox": { "command": "passbox", "args": ["mcp"] }
}
}That file is ~/.claude.json for Claude Code, ~/.codex/config.toml for Codex in TOML form, and ~/.cursor/mcp.json for Cursor. Check it took with claude mcp list.
The server exposes two tools, and neither returns a secret.
| Tool | Returns |
|---|---|
list_secrets | names only |
run_with_secret | the command's output, with the value scrubbed out of it |
There is deliberately no get_secret. A tool result lands in the model's context, where it is logged and sent onward, so the tools run the command for the agent. If the command prints the secret anyway, passbox replaces it in the output before returning.
The agent name in the prompt comes from the MCP handshake, so it is the client's own clientInfo.name rather than a guess up the process tree.
Grants and MCP
The MCP server is a client of the broker like anything else, so a token works the same way: PASSBOX_TOKEN in the server's environment covers the secrets it was granted, and those reads stop prompting.
The server inherits the environment of whatever launched it, which for an editor is the editor itself. So the token has to be in the MCP entry rather than your shell:
{
"mcpServers": {
"passbox": {
"command": "passbox",
"args": ["mcp"],
"env": { "PASSBOX_TOKEN": "..." }
}
}
}The cost is a bearer token in a config file. Worth it for an agent that reads the same few secrets all day and otherwise prompts on every call. For an agent that reads a secret now and then, take the prompt instead.
examples/mcp speaks the protocol to passbox mcp from Python with no SDK, and holds a project .mcp.json. Models that need the whole MCP guide in one place can read llms.txt.
A token cannot widen what the tools already refuse. list_secrets still returns names only, run_with_secret still runs the command, and there is still no get_secret. A token only changes whether you are asked.
Tokens live in the broker's memory, so restarting it revokes every one of them and the MCP server goes back to prompting. It does not need restarting itself.
Approving a project once
A project declares what it needs in .passbox.toml:
secrets = ["github/token", "npm/token"]
window_secs = 3600The first time an agent working in that directory asks for one of them, passbox shows a single prompt covering the whole list, and does not ask again for the window. The default is an hour and the cap is twelve.
The file is a request, not a grant. An agent can write one itself, so nothing is approved until you put your finger on the sensor. The approval is bound to the file's SHA-256: add a secret to the list and the hash changes, so it asks again.
The project is the caller's working directory, read from the kernel rather than taken from the request, so a caller cannot point at a manifest somewhere else. Grants live in grants-<host>.age, encrypted, and never sync.
.passbox.toml and .env.passbox are different files
The names are similar and the jobs are unrelated. They work together, and a project can have both.
.passbox.toml | .env.passbox | |
|---|---|---|
| Answers | may this project ask for these | which variable gets which secret |
| Read by | the broker, when deciding | your program, at startup |
| Effect | one prompt covers the list, for a window | populates process.env or equivalent |
| Approval | yes, bound to the file's hash | none, it is a lookup table |
| Safe to commit | yes, it names secrets and grants nothing | yes, same |
The .toml is a request: it changes how often you are asked. The .env is a mapping: it says where a value lands once you have it. Neither holds a value, which is why both can be committed, and neither is a grant on its own.
If a project uses both, the .toml covers the same names the .env maps, so the one prompt covers the whole startup.
Seeing what was released
passbox audit --tail 50Every decision is logged, encrypted, one record per line.