@sethwebster/agent-password-manager
v0.4.0
Published
Local-first encrypted password manager for agents and operators
Maintainers
Readme
Agent Password Manager
A local-first password manager for agents and operators.
Now packaged for npm as @sethwebster/agent-password-manager, while keeping the same conservative vault, policy, audit, CLI, and MCP behavior intact.
Why
Because agents and operators still need secrets, but the usual options are full of nonsense:
- shell history leaks
- plaintext
.envsprawl - shared browser password stores
- overbuilt SaaS vaults when local control is enough
- no clear audit trail for who revealed what and why
This repo exists to provide a boring, local-first secret layer for agent workflows:
- encrypted at rest
- masked reads by default
- explicit about purpose when revealing secrets
- policy-gated for sensitive reads/exports
- usable from both CLI and MCP integrations
What it does
- encrypted local vault
- masked reads by default
- explicit-purpose secret reveal
- policy-gated approvals for reveal/export
- append-only local audit log with hash chaining
- MCP tool surface for local integrations
Install / run
One-shot via bunx
After publish:
export APM_MASTER_PASSPHRASE='your-passphrase'
bunx @sethwebster/agent-password-manager doctor
bunx @sethwebster/agent-password-manager initOne-shot via npx
After publish:
export APM_MASTER_PASSPHRASE='your-passphrase'
npx @sethwebster/agent-password-manager doctor
npx @sethwebster/agent-password-manager initInstall globally
npm install -g @sethwebster/agent-password-manager
apm doctorRun from this repo during development
cd /Users/admin/.openclaw/workspace/repos/agent-password-manager
export APM_MASTER_PASSPHRASE='your-passphrase'
bun run build
node dist/apm.js doctorCLI entrypoints after publish
Published package bins:
agent-password-manager— package-name-matched bin fornpx/bunx @sethwebster/agent-password-manager ...apm— short CLI alias after installapm-mcp— MCP stdio server after install
Examples:
npx @sethwebster/agent-password-manager doctor
bunx @sethwebster/agent-password-manager get github-dougbot
apm doctor
apm-mcpMCP server
Run directly from the published package
export APM_MASTER_PASSPHRASE='your-passphrase'
npx apm-mcpIf you do not want a global install, npx can invoke the named bin from the scoped package explicitly:
npx --package @sethwebster/agent-password-manager apm-mcpEquivalent with Bun:
bunx --package @sethwebster/agent-password-manager apm-mcpRun from this repo
export APM_MASTER_PASSPHRASE='your-passphrase'
bun run build
node dist/mcp-server.jsOr through the package script:
bun run mcpOr through the CLI shim:
node dist/apm.js mcp-serverVault path resolution
Default behavior is intentionally repo-local / working-directory-local:
- CLI and MCP both use
./vaultrelative to the current working directory. - If you launch them from the same repo or project directory, they will use the same vault.
- To pin the vault somewhere else explicitly, set
APM_VAULT_DIRto an absolute or relative path.
Examples:
# default: use ./vault under the current directory
apm doctor
# explicit override: force a shared vault path
APM_VAULT_DIR=~/.apm/main-vault apm doctor
APM_VAULT_DIR=~/.apm/main-vault apm-mcpDirectory layout
vault/vault.enc.json— encrypted vaultvault/audit.log— append-only audit log with integrity hashesvault/policy.json— local policy file for reveal/export/read rulesvault/sync-state.json— peer sync config and per-peer sync baselinessrc/core.ts— shared vault, policy, and audit logicsrc/apm.ts— CLI sourcesrc/mcp-server.ts— stdio MCP server sourcedist/apm.js— compiled CLI entrypointdist/mcp-server.js— compiled MCP entrypointspec.md— product specthreat-model.md— security assumptions and risks
CLI usage
Doctor
npx @sethwebster/agent-password-manager doctorAdd an entry safely
Preferred order:
--password-stdin- interactive secret prompt on a TTY
--passwordonly if you knowingly accept shell history / process list exposure
printf 'super-secret\n' | npx @sethwebster/agent-password-manager add github-dougbot \
--username '[email protected]' \
--password-stdin \
--url 'https://github.com' \
--tag github --tag doug \
--ownerType sharedGet an entry
Masked by default:
npx @sethwebster/agent-password-manager get github-dougbotReveal only with explicit purpose, and with approval when policy requires it:
npx @sethwebster/agent-password-manager get github-dougbot --reveal --purpose 'log into GitHub CLI'
npx @sethwebster/agent-password-manager get github-dougbot --reveal --approve --purpose 'break-glass production fix'Edit an entry
printf 'rotated-secret\n' | npx @sethwebster/agent-password-manager edit github-dougbot \
--password-stdin \
--purpose 'rotate credential'Search / list
npx @sethwebster/agent-password-manager list
npx @sethwebster/agent-password-manager list --tag github
npx @sethwebster/agent-password-manager search --query dougExport / import
Plaintext export is intentionally annoying:
npx @sethwebster/agent-password-manager export \
--output ./vault-export.json \
--allow-plaintext-export \
--approve \
--purpose 'offline encrypted backup prep'
npx @sethwebster/agent-password-manager import --input ./vault-export.json --mode mergeAudit log
npx @sethwebster/agent-password-manager audit --limit 20
npx @sethwebster/agent-password-manager audit --verifySecure peer sync
There are now two sync modes:
- Network start/connect for an actual live machine-to-machine exchange.
- Package push/pull for removable media / shared-folder workflows.
Both use the same conservative merge rules:
- vault passphrases stay local
- sync uses a separate shared secret/key
- deletes replicate as tombstones
- if both sides changed the same id since the last baseline, sync aborts instead of guessing
- sync actions are written to the audit log
Live network sync
Intended UX:
# source machine
apm sync start --secret 'something'
# safer equivalents
printf 'something\n' | apm sync start --secret-stdin
APM_SYNC_SECRET='something' apm sync start
# client machine
apm sync connect http://source-host:8787 --secret 'something'
# safer equivalents
printf 'something\n' | apm sync connect http://source-host:8787 --secret-stdin
APM_SYNC_SECRET='something' apm sync connect http://source-host:8787What sync connect does:
- sends the client's current encrypted sync snapshot to the source server
- the source applies only fast-forwardable changes from the client
- the source returns its current encrypted snapshot
- the client applies only fast-forwardable changes from the source
- if either side detects a same-record/same-baseline divergence, the sync fails with the conflicting ids
The server is intentionally simple: an HTTP process you start when you want it, not a permanent secret daemon.
Legacy/local-first package sync
File-package sync still exists for conservative offline workflows:
# machine A: connect peer B to a package path
printf 'shared-sync-key\n' | npx @sethwebster/agent-password-manager sync connect laptop-b \
--package ./laptop-b.apmsync \
--sync-key-stdin
# write my current state for the peer to consume
printf 'shared-sync-key\n' | npx @sethwebster/agent-password-manager sync push laptop-b --sync-key-stdin
# read the peer package and apply only non-conflicting changes
printf 'shared-sync-key\n' | npx @sethwebster/agent-password-manager sync pull laptop-b --sync-key-stdinSafety semantics:
- vault passphrases are never transmitted by either sync mode
- network sync authenticates with a shared secret and encrypts the exchanged sync body with that same secret-derived key material
- package sync encrypts the sync package with the shared sync key
- package contents and network payloads contain vault records/tombstones, not the master vault passphrase
- conflict handling stays conservative on purpose; you reconcile locally, then retry
MCP tool surface
Minimal and conservative on purpose:
apm_doctorapm_listapm_searchapm_getapm_addapm_editapm_policy_showapm_audit_tail
MCP safety model
apm_getreturns masked passwords by default.- Reveals require
reveal=trueandpurpose="...". - If policy says approval is required, also pass
approve=true. apm_addandapm_editdo not take inline plaintext passwords.- Secret writes must use one of:
passwordEnvVar: environment variable name containing the passwordpasswordFile: local file path containing the password
- MCP calls reuse the same policy checks and audit log as the CLI.
- MCP audit records are tagged with
transport: "mcp".
Example MCP config
Example for a local MCP client that supports stdio servers:
{
"mcpServers": {
"agent-password-manager": {
"command": "npx",
"args": [
"--package",
"@sethwebster/agent-password-manager",
"apm-mcp"
],
"env": {
"APM_MASTER_PASSPHRASE": "your-passphrase"
}
}
}
}If your client supports inherited environment instead of literal secrets in config, use that. Better than hardcoding the passphrase in JSON.
Example MCP tool calls
Masked read:
{
"name": "apm_get",
"arguments": {
"id": "github-dougbot"
}
}Reveal with explicit purpose:
{
"name": "apm_get",
"arguments": {
"id": "github-dougbot",
"reveal": true,
"purpose": "log into GitHub CLI"
}
}Add using an environment variable instead of inline plaintext:
{
"name": "apm_add",
"arguments": {
"id": "github-dougbot",
"username": "[email protected]",
"ownerType": "shared",
"tags": ["github", "doug"],
"passwordEnvVar": "GITHUB_DOUGBOT_PASSWORD",
"purpose": "initial vault import"
}
}Policy
Default local policy file:
{
"version": 1,
"defaults": {
"requireApprovalForReveal": false,
"requireApprovalForMaskedRead": false,
"requireApprovalForExport": true,
"denyReveal": false,
"allowedOwnerTypes": ["agent", "operator", "shared", "service"]
},
"rules": [
{
"name": "prod secrets",
"tag": "prod",
"requireApprovalForReveal": true,
"denyReveal": false,
"allowedOwnerTypes": ["service", "operator"]
}
]
}Scripts
bun run build— compile TypeScript entrypoints todist/for Node executionbun run build:clean— rebuild from scratchbun run prepack— rebuild before publishing/packingbun run pack:check— rebuild and inspect the npm tarball contentsbun test— run all Bun-native testsbun run test— rebuild, then run all testsbun run test:noninteractive— rebuild, then run CLI tests onlybun run test:mcp— rebuild, then run MCP tests onlybun run test:smoke— rebuild, then run the doctor command under Nodebun run mcp— rebuild and launch the MCP server under Nodebun run cli -- <args>— rebuild and run the CLI with arguments under Node
Testing
bun run test
bun run test:mcp
bun run test:noninteractive
bun run pack:checkTradeoffs
- This stays local-first and simple.
- Approval is local friction, not a remote workflow system.
- The MCP server is intentionally stdio-only and short-lived by default.
--passwordstill exists in the CLI for compatibility, but MCP blocks inline plaintext secret writes on purpose.- Policy is a local guardrail, not a defense against a hostile machine owner.
