Alien Agent ID — Vault
Portable single-file encrypted vault at ${AGENT_ID_STATE_DIR:-$HOME/.agent-id}/vault.enc.
The master key is held in slots, LUKS-style — passkey (Touch ID / Face ID / security key), agent-key (auto-unlock), passphrase, mobile (phone), owner-approval (Alien app).
Two modes (chosen at init, one-way):
- user mode (default) — NO passphrase, ever. Unlock by agent-key or owner-approval/mobile (the Alien app). The agent cannot add a passphrase, and a user-mode vault cannot be converted to dev mode (the mode is bound to the master key and verified on every unlock).
- dev mode (
--dev, or providing a passphrase at init) — for developers / power users: passphrase slots are allowed, plus all the user-mode methods.
Passphrase is the exceptional, opt-in path — never something the agent enables.
Pairs with the agent-id-proxy plugin — the proxy unlocks the vault and injects values into outbound HTTP requests. The agent itself does not retrieve plaintext during normal operation; show exists only for manual export.
Resolve the CLI
bin/cli.mjs lives in this plugin's directory. In the examples below, CLI is ${CLAUDE_PLUGIN_ROOT}/bin/cli.mjs — the ${CLAUDE_PLUGIN_ROOT} path is filled in for you when the skill loads.
Initialize the vault — pick how it unlocks
init --unlock <method> chooses the unlock. The passkey and passphrase
methods are hard boundaries — the agent does not hold them, so it can't
self-unlock; both default to no agent-key slot.
# Passkey (recommended): Touch ID / Face ID / security key. Opens a secure form;
# you verify with biometrics. The agent can never unlock the vault itself.
# (On macOS the ceremony opens in Safari — only Safari exposes the Touch ID PRF.)
node CLI init --unlock passkey
# Passphrase: typed into the secure form (dev mode).
node CLI init --unlock passphrase
# Agent-key (default if --unlock is omitted): auto, unattended — but the agent
# CAN unlock it (convenience / low-stakes; needs `agent-id-core bootstrap`).
node CLI init # == --unlock agent-key
--agent-key keeps an agent-key slot alongside a passkey/passphrase (convenience +
hard method). --no-agent-key removes it. Add more unlock methods later with
rekey add-passkey / add-passphrase (dev only) / add-mobile / add-owner-approval.
A user-mode vault never gains a passphrase and cannot be converted to dev mode.
Add a credential
Every credential needs a name, type, and domain allowlist (default-deny — the proxy refuses to inject for any host not on the list).
# Bearer token (GitHub PATs, OpenAI keys, Slack bot tokens, …):
node CLI add --name github-pat --type bearer \
--domains '*.github.com,api.github.com' --value-file /tmp/tok
# Custom header (X-Api-Key style):
node CLI add --name openai --type header --header-name X-Api-Key \
--domains api.openai.com --value-file /tmp/key
# Basic auth:
node CLI add --name old-svc --type basic --domains svc.example.com \
--username admin --password-file /tmp/pw
# Query param (?api_key=... style):
node CLI add --name geocoder --type query --param-name api_key \
--domains api.example.com --value-env GEO_KEY
# Cookie:
node CLI add --name session --type cookie --cookie-name sid \
--domains app.example.com --value-file /tmp/sid
# TOTP seed (proxy generates the code at request time):
node CLI add --name github-totp --type totp --domains '*.github.com' \
--secret-file /tmp/seed.b32
# Full cookie jar for one origin (JSON object):
echo '{"sid":"abc","csrf":"xyz"}' | node CLI add --name gmail \
--type cookie-jar --domains mail.google.com
# Arbitrary secret — SSH/RSA private key, PEM, service-account JSON, any blob.
# Not host-scoped (use it via `exec --file`/`--env`, not the HTTP proxy):
node CLI add --name deploy-key --type secret --form # paste into the form
node CLI add --name deploy-key --type secret --value-file ~/.ssh/id_ed25519
Login/password pairs use --type basic (--username + a password input). For
SSH/RSA keys and other key material use --type secret.
Value-input channels — pick the one with the smallest attack surface:
--form— out-of-band browser form (recommended when a human is present). You supply--name/--type/--domains(and any metadata like--header-name); a one-shot127.0.0.1form opens for the human to type the secret, which then goes straight to the vault. It never enters your stdin, transcript, or process args. Multi-field types (basic,oauth2,cookie-jar) collect all their secrets in one form. The command prints the URL to stderr and waits up to 5 min.--<field>-file <path>— read from a file--<field>-env <VAR>— read from an environment variable- stdin (piped) —
echo "secret" | node CLI add ... --<field> <value>— visible in process list; avoid
# The human types the secret into a browser form — the agent never sees it:
node CLI add --name github-pat --type bearer --domains api.github.com --form
Never paste a secret into chat. The agent transcript persists.
Access levels — grant "read", not "everything"
Any credential can carry --access ro (default is rw, unrestricted). A
read-only credential can be used but not used to change anything:
- The proxy allows read-shaped requests only:
GET/HEAD/OPTIONS, plus POST-tunneled reads it can classify (GraphQLquery, JMAP*/get/*/query, JSON-RPC calls that don't submit transactions). Everything else → structured403 access_denied. - The sealed browser (agent-id-browser) enforces the same classification on
every request a
--access rosession makes, at the network layer. - Plaintext egress is sealed:
showredacts andexecrefuses a ro credential's secret fields — otherwise the enforcement would be theater.
# "The agent may READ my mailbox, never send/delete":
node CLI add --name fastmail --type bearer --access ro \
--domains api.fastmail.com --form
# Fine-grained overrides (first match wins), e.g. allow one search POST:
node CLI set-access --name fastmail \
--rules '[{"effect":"allow","methods":["POST"],"path":"/search"}]'
Changing the level later (set-access) is asymmetric by design:
node CLI set-access --name fastmail --access ro # tighten: applies at once
node CLI set-access --name fastmail --access rw # WIDEN: the owner must
# confirm via the secure form
Widening (ro→rw, adding an allow rule, dropping a deny rule) opens the
secure prompt and asks the owner to type the credential name — the agent
cannot approve it itself, and add/generate refuse to overwrite a record
with a wider level. When a human types the secret via --form, the form shows
the access level being granted.
Any credential the proxy restricts (level ro, or rw with rules) is also
sealed against show/exec — its plaintext can't be extracted to sidestep the
gate.
Where ro works — the operation must be legible in the request
Read/write can only be told apart when the request reveals the operation. It fails safe: when it can't tell, it denies (never silently allows a write).
| API shape | Under ro |
|---|---|
| REST with real verbs (Gmail API, Stripe, GitHub REST) | reads pass, writes blocked — best case |
| GraphQL with the query text inline in the body | query passes, mutation/subscription blocked |
| JMAP (Fastmail) | */get,*/query pass; */set blocked |
| JSON-RPC with recognized read methods | reads pass; unknown methods blocked |
| GraphQL persisted queries (hash only, no query text — Reddit/X web) | over-blocks: reads AND writes denied |
| Opaque binary RPC (gRPC-web, Connect, Twirp) | over-blocks: denied |
Two things ro deliberately does not cover: a server that mutates on a
GET (per HTTP safe-method convention GET/HEAD/OPTIONS are reads —
constrain such an endpoint with an explicit deny rule), and raw IMAP/SMTP
(no HTTP method to gate — use an API or a web session).
For an over-blocked endpoint you have two levers: a scoped allow rule for a
read path (but if reads and writes share one URL, e.g. a single /graphql, that
also reopens writes there); or, for a token API, prefer a server-side
read-only scope at onboarding (e.g. Gmail gmail.readonly) — the only
enforcement even a client-side bug can't defeat.
The policy is enforced by the proxy / browser-session processes. With an agent-key vault the agent could in principle open the vault directly, so for a hard guarantee init the vault with
--unlock passkey(no agent-key slot) and let only the proxy hold it unlocked.
Generate a wallet keypair (the key never leaves the vault)
For blockchain wallets the vault can create the private key itself — the
key material is generated inside the vault process and sealed
(exportable: false): show redacts it, add refuses the type, and the only
way to exercise it is transaction signing inside the proxy. The command prints
only the public address.
# Solana wallet (ed25519). Domains = RPC hosts the proxy may sign for:
node CLI generate --name sol-hot --type solana-keypair \
--domains api.mainnet-beta.solana.com
# EVM wallet (secp256k1) — Ethereum, Polygon, Base, any EIP-1559 chain:
node CLI generate --name polygon-hot --type evm-keypair \
--domains polygon-bor-rpc.publicnode.com,ethereum-rpc.publicnode.com
Output (and list) carries the address (publicKey for Solana, address for
EVM) — that is all an agent ever needs: fund the address, build unsigned
transactions against it, and submit them through the proxy, which signs them
in-process (see the proxy skill, "Wallet credentials").
List, show, remove
node CLI list # metadata only; never plaintext
node CLI show --name github-pat # plaintext export; prefer the proxy for runtime use
# (sealed/generated records stay redacted)
node CLI remove --name github-pat
Run a command with credentials injected (env var or key file)
For tools that authenticate from environment variables or a key file
rather than HTTP — where the proxy can't help — exec materializes selected
credential fields and runs the command. The agent never sees the value; only the
variable names + their sources are logged (to stderr).
# Into the environment (CLIs/SDKs that read env vars):
node CLI exec \
--env MODAL_TOKEN_ID=modal-token.username \
--env MODAL_TOKEN_SECRET=modal-token.password \
-- modal run gpu_job.py
# Into a temp 0600 file (tools that want a key FILE — ssh, RSA PEMs):
node CLI exec --file GIT_SSH_KEY=deploy-key.value \
-- sh -c 'GIT_SSH_COMMAND="ssh -i $GIT_SSH_KEY -o IdentitiesOnly=yes" git fetch'
--file writes the field to a temporary 0600 file, sets VAR to its path,
runs the command, then shreds and removes the file on exit. The agent gets the
path, never the contents — ideal for the secret type (SSH/RSA keys, PEMs).
execis a weaker boundary than the proxy — prefer the proxy when you can. The child runs with inherited stdio, so if the command you run prints its own environment, the secret comes straight back to your transcript. Treat the value as exposed to the subprocess: onlyexecinto a trusted tool that authenticates from the env, never into something that echoes it (env,printenv,sh -c 'echo $VAR'). The proxy, by contrast, never hands the agent the value at all — reach forexeconly for env-var-auth tools the proxy genuinely can't reach.
--env/--fileare repeatable and mixable.fieldis the record field to read:basic→username/password;bearer/header/query/cookie→value;totp→secret;oauth2→refreshToken;secret→value. (credmay contain dots — the split is on the last dot.)- Everything after
--is the command; it runs with inherited stdio, so interactive commands keep their TTY (e.g.-- modal shell --gpu a10g). - Sealed in-vault-generated keys (
solana-keypair/evm-keypair) refuse to leave the vault — use the proxy to exercise those. - Unlock +
--state-dirflags work as for any other subcommand.
Slot management, portability & migration
Less-common admin operations — adding/removing unlock slots (rekey), moving the vault between machines (export/import), and migrating a legacy v4 vault (migrate) — are documented in reference.md.
Common flag
--state-dir <path> — defaults to $AGENT_ID_STATE_DIR then ~/.agent-id.