Skip to content
Blog

AgentKit guidesArticle 21/21

AK Vault: let agents use secrets without ever seeing them

Goon Nguyen•AgentKit Guides••24 min read
Article 21 in the AgentKit v3 onboarding series. About a 15 minute read. This is the full user guide and goes deeper than article 6: why you need a vault, how to use it from the CLI and the desktop app, the vault.agentkit.best dashboard, multi-device sync, and the design principles you must understand so you don't end up thinking "the vault keeps blocking me, I'm turning it off" 😅

To be fair up front: a vault is not a miracle cure. It solves exactly one problem, which is the agent can use the key without holding the key. It cannot save you if you paste a key into the chat yourself, and it cannot save you from a program that received a key and sent it somewhere. Those limits are spelled out in section 9.

Every command in this article is pasted from the real --help of ak 3.0.0, run in a temporary HOME. The sync and dashboard parts come from the source code, the docs and the public vault.agentkit.best pages as of 04/20/2026.

1/ Why you need a vault: numbers, not feelings

I often hear "it's my machine, the repo is private, what's the worry". Let's look at the numbers.

Secret leaks are growing, and AI makes them grow faster. GitGuardian's State of Secrets Sprawl 2026 (2025 data):

  • 28.65 million new hardcoded secrets were pushed to public GitHub in 2025, up 34% year over year, the largest jump they have recorded.

  • Secrets for AI services (OpenAI, Anthropic, Gemini...) reached 1,275,105, up 81%.

  • Claude Code-assisted commits had a 3.2% secret leak rate, double the 1.5% baseline.

  • 24,008 secrets sat in MCP config files on public GitHub, 2,117 of them still valid.

  • Over 64% of secrets confirmed valid in 2022 were still valid when retested in January 2026. Leaked, and nobody rotated them.

  • Internal repos are about 6 times more likely to contain hardcoded secrets than public ones. So "private repo" is no shield.

Agents read .env even when told not to. The Register (01/2026) tested Claude Code v2.1.12: the .env file was in the ignore list, and the agent still read it and printed the secrets to the console. A blog post from 04/2026 describes an AGENTS.md that said plainly "do not read this file", and the agent read it anyway. On the Cursor side, a staff member confirmed on the forum that .cursorignore blocks indexing but cannot block terminal commands like cat or grep that the agent runs itself.

Attackers already know how to use your own agent. The s1ngularity attack (Nx, 08/2025): malicious Nx versions on npm called the Claude, Gemini and Q CLIs directly with flags like --dangerously-skip-permissions and --yolo to hunt for secrets on the machine. GitGuardian counted 2,349 credentials taken from 1,079 developer machines, mostly GitHub tokens, plus npm, AWS, OpenAI and Anthropic keys.

Prompt injection pulls secrets out through tools. General Analysis (07/2025) built a demo: instructions hidden in a support ticket, a Cursor agent connected to the Supabase MCP reads the ticket, queries the integration_tokens table and writes the tokens back into the ticket for the attacker. Supabase countered that no customer has actually been hit, and that the demo ran with a high-privilege setup. Both sides are right: the flaw is in the permission setup, but high-privilege setups are what developers do every day.

One line to take away: wherever an agent can read files, a plaintext secret is already half leaked. A vault won't stop you from doing something silly, but it takes the plaintext out of the agent's reach.


2/ What AK Vault is

AK Vault is the umbrella name for these layers:

Layer What it is Status
Local vault ak secrets Secrets kept in the OS keychain (or an age-encrypted file); the agent asks, you approve, every read is logged ✅ shipped in ak 3.0.0
E2E sync ak secrets sync, ak secrets devices Syncs the vault between machines; the server holds only ciphertext 🧪 preview, hidden from the top-level --help command list, not GA
Web dashboard vault.agentkit.best/app The browser is a device of the vault and decrypts inside the page 🧪 pre-release
Remote MCP vault.agentkit.best/mcp Lets cloud AI tools (ChatGPT, Claude...) find and use secrets through one of your machines 🧪 pre-release
Secrets page in the AgentKit desktop app UI for the local vault: add, approve, read the audit ✅ in the 3.0.0 desktop app

How it works, in one loop:

the agent writes ${secret:NAME} into a command
→ the secrets-inject hook calls ak secrets resolve
→ resolve identifies the agent by hashing its binary
→ matching grant? yes: substitute the value, write an audit row
no: REFUSE, file a request in the inbox
→ you answer in your own terminal (ak secrets approve) or the desktop app

The agent only ever sees the name ${secret:NAME}. The value is substituted at run time, in memory.


3/ Using the CLI, step by step

Step 1: move .env into the vault

Importing .env into the vault

Preview the plan first, then import:

ak secrets import-env ./my-app --dry-run   # directory: finds .env and .env.*, up to 3 levels deep
ak secrets import-env ./my-app
ak secrets import-env .env.production # or a single file

A few things --help states that you should know:

  • Values are never printed, logged or put into --json; you only get imported/skipped counts.

  • Keys already in the vault are skipped unless you add --force.

  • A directory import skips .env.example, .env.template, .env.bak, symlinks, hidden directories and dependency directories. A key present in two files is a conflict, skipped everywhere; no file silently wins.

  • The environment comes from the file name (.env.production → production), the project from the directory.

  • import-env never touches the source file. After importing you must clean up .env yourself. If you don't, you haven't really done anything 😁

Step 2: add, find, check

ak secrets set OPENAI_API_KEY                       # non-echoing prompt
echo -n "$VALUE" | ak secrets set OPENAI_API_KEY --value=- # or pipe from stdin
ak secrets list
ak secrets search stripe --environment production --json
ak secrets describe OPENAI_API_KEY --json # full profile, never the value
ak secrets get OPENAI_API_KEY # value shows as [redacted]

set refuses --value=<value> on the command line, because it would land in shell history and ps.

To check whether the key in the vault is the one in your old .env without printing either:

ak secrets compare OPENAI_API_KEY --value-file ./key-to-check.txt

The answer is only match or differs. Values under 16 characters (not counting a public prefix like sk_live_) are refused, and each comparison is a row in the audit, so don't even think about looping it to guess a key.

Add metadata so it's easy to find and you get rotation reminders:

ak secrets set STRIPE_API_KEY --force --rotate-ttl 30d --provider stripe
ak secrets rotate STRIPE_API_KEY

Step 3: let the agent use it

The agent writes a reference, not a value:

curl -H "Authorization: Bearer ${secret:OPENAI_API_KEY}" https://api.openai.com/v1/models

The secrets-inject hook (shipped in the core kit) scans only Bash.command and WebFetch.url. The first time, with no grant, the command is refused and the request waits for you.

The agent can also ask ahead of use:

ak secrets request OPENAI_API_KEY --purpose "deploy billing" --json

request never reads a value and never writes a grant on its own. It just drops a letter in the inbox.

Step 4: you approve, in your own terminal

The approval flow: you approve in your own terminal
ak secrets approve                                   # walk through every waiting request
ak secrets approve OPENAI_API_KEY --state once # exactly 1 release
ak secrets approve OPENAI_API_KEY --state ttl --ttl 30d
ak secrets approve OPENAI_API_KEY --state deny

Four answers, learn them well:

Answer (--state) Meaning
deny Refuse this request, store nothing; it asks again next time
once Exactly one release
ttl (default) Allow for a while, expires on its own. Default 24h, change with --ttl 12h, 30d, 2w
persist Allow until you revoke it yourself

Step 5: run a program with a secret (exec scope)

When you want one program to receive the secret in one environment variable:

ak secrets run --env STRIPE_API_KEY=STRIPE_API_KEY -- stripe customers list
ak secrets run --env GH_TOKEN=GH_TOKEN --purpose release -- gh release list

Each VAR=REF pair needs an exec grant pinned to the program's real path (symlinks resolved) and content hash. Without one, a request is filed and you answer with ak secrets approve --env STRIPE_API_KEY. The program is started directly, never through a shell; shells and launchers are refused; the environment is rebuilt from scratch (system PATH, a few variables like HOME and LANG, plus the granted ones); output has the values struck out before it reaches you.

Step 6: see who read what, revoke when needed

ak secrets grants
ak secrets audit --secret OPENAI_API_KEY
ak secrets audit --decision deny --json
ak secrets revoke OPENAI_API_KEY --agent sha256:<fingerprint>
ak secrets revoke --agent sha256:<fingerprint> --all # panic button
ak secrets trust claude # see which binary is trusted
ak secrets trust claude --refresh # re-pin after updating the agent

revoke takes away permission, not the copy the agent already read. --help says it plainly: revoke is step one, ak secrets rotate <ref> is step two.

Step 7 (optional): one variable name, several accounts

ak secrets alias set CLAUDE_API_KEY CLAUDE_API_KEY_1   # use account 1
ak secrets alias set CLAUDE_API_KEY CLAUDE_API_KEY_2 # switch to account 2
ak secrets alias list --json

An alias grants nothing. Grants go on the target secret (CLAUDE_API_KEY_1), and aliases do not sync between machines.

Let the agent do it right: the ak-vault skill

In Claude Code or Codex:

/ak-vault import this project's .env files into the vault, then switch the config to ${secret:...}

The skill first reads the choice you recorded (ak config prefs resolve --json, key secretStore.choice). If you chose to keep .env files or use 1Password, it follows that and does not push the vault on you. It never prints a value, asks at most once, and for commands that are blocked under an agent it hands you the command to run instead of looking for a way around.


4/ Using the desktop app

Open the app with ak gui (or download the desktop app at agentkit.best/agentkit-app). The Secrets page sits in the System group and exists only in the desktop app; the browser dashboard started with ak config start does not have it.

What you can do:

  • Add secret, Rotate, Delete, Import .env. Import adds only new names by default; with Replace names that already exist on, you must preview first.

  • Reveal makes you type a reason, and the reason goes into the audit. If you copy, the clipboard is cleared when the dialog closes.

  • Export does not export from the window. It only shows the ak secrets export commands to run in a terminal.

  • The Grants panel lists which agent may read which name, with its Exec scope (variable, path, short hash). Granting and revoking by hand stay in the terminal.

  • The Waiting for you panel: answer requests agents left waiting.

The approval dialog has 4 buttons: Deny, Allow once, Allow for a while (15 minutes, 1 hour by default, or 8 hours), Always allow. The D, O, S, A shortcuts only "arm" after about 0.6 seconds, so a keystroke meant for another window can't answer a request you haven't read. Deny has focus by default. Decide later or Esc leaves the request waiting.

Watch out: "for a while" defaults to 1 hour on desktop, while ak secrets approve in the terminal defaults to 24 hours. Same name, different number. For long jobs, choose in the terminal with --ttl to be sure.

A few more things worth having:

  • Every time a secret is released to an agent, the app shows an in-app notice with a Revoke this agent's grants button. When the window isn't focused there is an OS notification Secret released (never with the value or the purpose).

  • Audit log filters by name, decision (Allowed/Blocked/Denied) and source (Agent/Terminal/Desktop). Same ledger as ak secrets audit.

  • The Vault sync panel is read-only: sync state, conflicts, devices. Every sync action is still a terminal command; the panel gives you a copy button for each.

  • If an agent launched the desktop app itself, the app refuses the answer and records it as a deny.


5/ Common use cases

Situation What to do
Old repo full of .env files ak secrets import-env ./ --dry-run → import → clean up .env → switch config to ${secret:NAME}
Agent calls an API with curl Write ${secret:NAME} in the command, approve the first time with approve --state ttl
Deploy with a provider CLI (stripe, gh, wrangler...) ak secrets run --env VAR=REF -- <program>, approve the exec scope
Several Claude/OpenAI accounts ak secrets alias set to switch
You suspect the agent reads too much ak secrets audit --since 24h, then revoke --all + rotate
Everything refused after updating Claude Code Binary hash changed; run ak secrets trust claude --refresh in your own terminal
Check whether a key in an old .env still matches ak secrets compare REF --value-file <file>
Server/container without a keychain --backend=file (age-encrypted vault), see ak secrets recovery
Want secrets on a second machine Sync 🧪 in section 7, dev/test keys only

6/ The vault.agentkit.best dashboard 🧪

vault.agentkit.best

The vault.agentkit.best home page is a landing page with a guide, how-it-works, security and FAQ. It states its own status: protocol v1 has been reviewed by the owner, not independently, and cloud sync is pre-release.

The /app dashboard (pre-release, not for production credentials):

  • The browser is a device of the vault, like a second laptop. It creates device keys inside the browser, keeps them in IndexedDB as non-extractable keys, and decrypts inside the page. The server still receives only ciphertext.

  • No passphrase: every visit is a fresh sign-in. Lock, or leaving the page, drops the session and the decrypted view from memory.

  • On first sign-in the page shows a join code starting with akj1-. You run ak secrets devices approve <join code> on a machine that already owns the vault, compare the 6-digit code on both screens, and only press y if they match.

  • It can: list, filter, find a name, set a value, delete, view signed revision history, restore an old value as a new revision, resolve conflicts. The Members and access tab is read-only.

  • It never shows, copies or stores a value: the value field is write-only and cleared after saving.

  • Only a vault owner can add a browser; an editor of a shared vault cannot.

  • There is a remote approval panel: Allow once, Allow for 15 minutes / 1 hour / 8 hours, or Deny. The answer is signed by the browser, and the requesting machine checks the signature before using it.

Remote MCP /mcp (pre-release): add https://vault.agentkit.best/mcp in your AI tool's connector settings, sign in to AgentKit and choose scopes:

  • vault-mcp:read: see names and descriptions, never values;

  • vault-mcp:write: add, replace, delete; can write but never read back;

  • vault-mcp:use: ask to use a secret and run commands you approved on your machine.

Reading and using require one of your machines running ak secrets sync now --watch --relay. With no machine online the answer is no_device_online. The --relay command is refused when an agent runs it.

⚠️ Remote MCP is not E2E. During a call, plaintext passes through the server's memory after TLS ends (not stored, not logged, per ADR 0002). If you need "the server never sees plaintext", don't use remote MCP. And never paste a secret into the chat for an AI tool: use the secrets_prepare_write flow and type the value yourself on the dashboard.

To disconnect: in your AgentKit account, Connected apps, disconnect AgentKit Vault (AI tools).


7/ Multiple machines and cloud sync 🧪

Pairing a second device

Sync is opt-in. With sync off, ak secrets makes no network requests. resolve always reads the local store and never waits for the network.

First machine:

ak login
ak secrets sync enable --recovery-kit-out ./vault-recovery-kit.txt
ak secrets sync status

You choose a vault password and receive a recovery key. Keep both off the machine (paper, a password manager). Lose every device, the password and the recovery key, and the vault is gone; the server cannot rescue it, because that is exactly what zero-knowledge means.

Second machine (3 steps, and it has to be you at a terminal):

# on the new machine
ak secrets devices add --label laptop # prints a join code
# on a machine that already syncs
ak secrets devices approve <join code> # prints an offer code
# back on the new machine
ak secrets devices add --offer <offer code> # shows a 6-digit code, compare with the other machine

Answer y only when both machines show the same 6 digits. Join and offer codes are not secret, but approving has to be you: devices approve and devices add --offer refuse to run under an agent (exit 6).

Daily sync:

ak secrets sync now
ak secrets sync now --watch # keep syncing
ak secrets devices list

Lost a machine:

ak secrets devices remove <device-prefix> --reason lost

Removing a device rotates the vault key so that machine cannot read later changes. But it cannot take back what the machine already decrypted. Rotate the key at the provider, then ak secrets rotate. ak secrets list flags refs that a removed device could read.

There are also sync recover, sync rotate-recovery, sync settle (resolve conflicts), sync disable and devices rotate.

Warning you must read (from the ak-vault README): sync is not GA; don't put production credentials in it. The production sync API was turned on as a preview by owner decision, and the review is the owner's, with no third party yet. I try it with dev/test keys; production keys wait until the release gates are met.


8/ Design principles you must know

This is the part that annoys people most, because the vault is "strict". But the strictness has reasons.

8.  1 The agent never sees a value

list, search, describe and get return only metadata and a hint (public prefix + last 4 characters). get --reveal needs an interactive terminal on both stdin and stdout, a y/N confirmation, and never works with --json or when piped. The agent checks with exit codes, hints and compare.

8.  2 Fail-closed: no grant means refuse, not ask

A hook has no terminal to ask you on. The approve --help explains: a hook that stalled on a prompt would be ignored rather than obeyed, so it files the request in the inbox and refuses (exit 8). That means the first time an agent touches a new secret, the command will fail. That is by design, not a bug.

8.  3 Approval has to be you, in your own terminal

approve and grant refuse to run when an agent sits above them in the process tree, and they record the attempt in the audit. The reason, in the words of --help: an agent answering its own request would make the whole inbox theatre.

A true story from today. I was too lazy to open a terminal, so I typed !ak secrets approve ... right inside Claude Code (the ! prefix runs a shell command inside the session). Result: refused, with a message along the lines of an agent may not answer its own request. Fair enough: a ! command runs under the Claude process, so to the vault it looks exactly like the agent approving itself. Lesson: open a separate terminal tab (or a desktop app you launched yourself) and approve there.

--help also admits this is "a speed bump, not a guaranteed boundary": it relies on recognizing an agent in the parent-process chain, and a detached process or a redirected AGENTKIT_HOME can look like your own terminal. Don't treat it as your only shield.

8.  4 Allow once vs allow 24h vs until revoked

A second true story, also from today: I picked allow once for a multi-step job. Step one ran fine, the single release was spent, step two was refused, and the job stopped halfway. Exactly what --help says: once means "exactly once".

Choose like this to save yourself the pain:

Job Pick
One single command, sensitive key (production, money) once
Multi-step job, runs during the day or overnight (/ak-atoz, /ak-orchestrate) ttl 24h (the CLI default) or a --ttl that is just long enough
Dev/test key used daily by exactly one agent persist, but remember to revoke when you're done
Not sure deny, and let it ask again

One note: if a synced vault has a policy requiring fresh approval, it only accepts allow once, used within 10 minutes.

8.  5 A grant is tied to 4 things, and to the binary

A grant is tied to four things

A grant matches when all 4 fields match: secret, project, agent binary fingerprint, purpose. An empty purpose is a literal value, not a wildcard. Two git worktrees of the same repo are two projects; grants don't carry between them. Updating the agent changes its hash, so you need trust --refresh. A block always wins over any allow on the same scope.

And the grant --help says it bluntly: granting means trusting that agent's model with the plaintext, because the model can run the resolve binary itself. Grant only what that agent should hold.

8.  6 An exec scope does not limit what the program does

Exec scope and blast radius

When you approve an exec scope, both the CLI and the desktop app warn you:

  • Arguments are not part of the scope. Whatever the program is told to run on its command line also gets the value.

  • For an interpreter like node or python3, the scope covers every script it is asked to run. This warning comes first.

  • curl, wget and gh can be told by their arguments to send a variable anywhere.

  • Processes of the same user can read the program's environment while it runs.

  • Output redaction is best effort: an encoded or split value is not recognized.

The literal advice from --help: grant exec scopes only to programs you would hand the value to yourself. Approving node for a key approves it for every script node runs, so prefer a dedicated CLI (stripe, gh) over an interpreter whenever you can.

8.  7 Commands the agent may not run

Per --help: approve, grant, devices approve, devices add --offer, sync enable --accept-sealed and sync now --relay refuse to run under an agent. trust --refresh requires an interactive terminal and an in-person confirmation. The ak-vault skill is told to hand you the command, not to find a detour. If your agent tries to get around these commands, that's a sign to stop and reread the prompt.


9/ Do and don't

Do ✅

  • Import .env with --dry-run first, then delete the source file.

  • Use findable ref names: <PROJECT>_<NAME>, or the provider's conventional name like CLOUDFLARE_API_TOKEN for a shared key.

  • Add --environment, --provider, --rotate-ttl when you set.

  • Approve in a terminal or desktop app you opened yourself.

  • Use ttl for multi-step jobs, once for single sensitive commands.

  • Check ak secrets audit every week, especially --decision deny.

  • After an incident: revoke --all, then rotate at the provider.

  • Keep the recovery key and vault password off the machine.

Don't ❌

  • Paste a secret into the chat with an agent, even "just so it can set it for me".

  • Run approve through ! in Claude Code or ask the agent to run it (it will be refused anyway).

  • Grant exec scopes to node, python3 or bash when a dedicated CLI exists.

  • Choose persist for production keys.

  • Put production credentials into sync, the dashboard or remote MCP while they are 🧪.

  • Type ${secret:...} into a commit message or heredoc: it may get substituted somewhere you didn't intend.

  • Treat .claudeignore, .cursorignore or a "don't read .env" line in AGENTS.md as security.


10/ Troubleshooting

Symptom Cause Fix
The agent's command fails the first time it touches a secret Fail-closed, no grant yet (exit 8) ak secrets approve in your own terminal
approve says an agent is running above it (exit 6) You ran it from inside the agent session (!, Bash tool) Open a separate terminal
Step 1 ok, step 2 refused The allow once was spent Approve again with --state ttl --ttl 24h
Just updated Claude/Codex, everything is refused Binary hash changed ak secrets trust claude --refresh
Switching worktrees means approving again Two worktrees are two projects Normal; approve again for that project
Agent blocked after asking repeatedly (exit 9) Per-fingerprint rate limit, in cool-down Wait, then grant sensibly instead of letting it keep asking
secrets run refuses the program Shells/launchers are banned, or the program's hash changed Run the real CLI, not wrapped in a shell; approve the exec scope again
A program under run can't find its helper The environment is rebuilt, PATH has only system directories Call the helper by its full path
compare exits 2 Value under 16 characters Normal; compare with the hint
A grant with --allow-host refuses a Bash command A Bash command has no destination to check (exit 6) Use WebFetch.url, or a grant without a host restriction
On Codex, a command with ${secret:...} is refused Codex has no permission prompt for hooks Read the AGENTKIT_SECRETS_INJECT_AUTO_ALLOW section of the secrets guide before turning it on
Sync reports an integrity alarm (exit 13) The vault failed verification Stop, read the guide; don't force the sync
Remote MCP answers no_device_online No machine is running the relay ak secrets sync now --watch --relay on one of your machines

For any error: ak secrets <command> --help has an exit status table. Read it before guessing.


Summary

1/ Secret leaks grew 34% in 2025, AI secrets grew 81%, and agents read .env even when told not to. Plaintext within an agent's reach is already half leaked.

2/ Local vault ak secrets ✅: import .env, the agent uses ${secret:NAME}, no grant means refuse, you approve, every read is audited.

3/ Approve in a terminal or desktop app you opened yourself. ! in Claude Code counts as the agent.

4/ once means exactly once. Multi-step jobs need ttl. Desktop defaults to 1 hour, the CLI to 24 hours.

5/ An exec scope does not limit arguments; node/python3 covers every script.

6/ Sync, the dashboard and remote MCP are still 🧪. Try them with dev/test keys and keep the recovery key off the machine.


Previous: Personal kit and /ak-dogfood ←

Sources

From AgentKit (read 04/11/2026):

  • ak secrets --help, and the --help of approve, request, grant, grants, revoke, audit, trust, run, set, get, list, import-env, compare, alias, sync, sync enable, sync now, sync status, devices, devices add, devices approve, ak gui (ak 3.0.0, temporary HOME).

  • Secrets guide: agentkit/docs/guides/secrets.md; the an agent may not answer its own request error: apps/cli/internal/commands/secretscmd/approve.go.

  • Desktop approval dialog: packages/ui/src/components/secrets/approval-modal.tsx, packages/ui/src/i18n/catalogs/en/secrets.ts; docs agentkit-docs/content/docs/beta/desktop-app/secrets.en.mdx.

  • ak-vault skill v1.0.0 (SKILL.md).

  • ak-vault repo (MIT): https://github.com/bestagentkits/ak-vault (README, ADR 0001, ADR 0002, release gates).

  • https://vault.agentkit.best/guide/ and https://vault.agentkit.best/faq/

External evidence:

Go deeper in the docs

Reference pages for every command, skill, hook and setting mentioned here.

Read the docs →

Share this article