# End-to-end walkthrough
A complete run-through of Svault's AI-aware flow: create a vault, classify secrets
with descriptions, define callers, turn on the AI judge, and watch a real model
grant or deny agent requests. The judge outputs below are **actual responses** from
`google/gemini-2.5-flash` via OpenRouter.
> **Vaults are addressed by name.** You can keep several (local today, remote on the
> [roadmap](roadmap.md)). The `secret`, `get`, and `settings` commands take
> `-v <vault>`; `unlock` and `lock` take the name as a positional argument. Omit the
> name only when there's a single vault.
## 1. Build
```bash
git clone https://github.com/nim444/Svault.git
cd Svault && cargo build --release
alias svault="$PWD/target/release/svault"
```
## 2. Create a vault (with a description)
```bash
svault create
# Name: billing-api
# Description: production billing service ← the judge sees this
# Allow agents: list → claude-code, billing-worker
# Rate limit: 20/hour
# Default tier: medium
# Use AI judge: yes
# Master passphrase: ******** ← first run only; one secret unlocks every vault
# → prints a one-time RECOVERY CODE; save it.
```
The vault description is the overall context the judge sees. Storage is `local`
today. There is no per-vault passphrase — the first `create` sets a **master
passphrase** that unlocks every vault; later vaults reuse it.
## 3. Add secrets — classify them and say what they're for
`--description` records the secret's purpose. The judge weighs each request's
*reason* against it, so an off-purpose request is denied even when the caller and
scope check out.
```bash
svault secret add DATABASE_URL -v billing-api --scope database --tier medium \
--description "production Postgres connection string"
svault secret add STRIPE_KEY -v billing-api --scope payments --tier high --require-reason \
--description "production Stripe charge key — only for billing/charge flows"
```
Classification lives **AES-256-GCM encrypted inside `vault.enc`**. Without the vault
key, a same-UID process can neither read a tier, scope, or purpose at rest to plan a
passing request nor downgrade one. In the TUI, `a` in the secret browser opens the
same form (name / value / scope / description / tier / require-reason).
## 4. Define who may ask (callers)
Caller rules live encrypted in the vault as well, so there is no committable
`svault.policy.yaml`. Seed the defaults, then edit them in `svault settings`:
```bash
svault policy init # seed default callers into the vault's encrypted policy
svault policy check claude-code # what it can reach + recent activity (unlocks the vault)
```
Conceptually a vault's caller rules look like:
```yaml
callers:
claude-code:
scopes: [database, api]
rate_limit: 20/hour
billing-worker:
scopes: [payments]
rate_limit: 60/hour
default:
scopes: []
rate_limit: 5/hour
```
## 5. Turn on the AI judge
There is no plaintext config and no key file. Every judge — its model, thresholds,
free-text **criteria**, and its own API key — lives AES-256-GCM **encrypted** in the
keyring at `.svault/keyring.enc`, opened by your **master passphrase** (no separate
keyring passphrase). Create the keyring, add a named judge (with criteria and key),
enable the judge globally, then unlock once:
```bash
svault keyring init # create the keyring under your master (one-time)
svault judge add strict # prompts: model, thresholds, criteria, then the API key (hidden)
# Model: google/gemini-2.5-flash
# Allow threshold: 60
# High threshold: 80
# Criteria: Only allow billing-related reasons that name an invoice or charge.
# OpenRouter API key: sk-or-… (blank = fall back to $SVAULT_OPENROUTER_KEY)
svault judge enable # flip the global on/off switch (on)
svault keyring unlock # caches a 0600 session key so the judge is live this session
svault judge status
# keyring: unlocked
# judge (global): on default: strict
# strict google/gemini-2.5-flash allow 60 high 80 KEY set
```
The first judge you add becomes the keyring's **default**, which vaults with no
explicit assignment use. A vault opts in to the judge via its per-vault toggle (set
at `svault create` or in settings); the keyring's global switch and each judge's
criteria are managed here. Until the keyring is unlocked the judge stays off and the
static tier rules apply (high = human-only).
## 6. Dry-run the model — `svault judge test`
This builds a sample request and asks the live model; nothing is read or written.
Pass a realistic `--vault` (the model sees it) and the descriptions. The outputs
below are real model responses:
```bash
# Plausible reason that matches the vault → ALLOW
svault judge test --vault billing-api --vault-description "production billing service" \
--tier medium --scope database --secret DATABASE_URL \
--reason "run the nightly billing migration against the production database" --caller claude-code
# ALLOW score 95 — Nightly billing migration is a plausible and specific reason for
# accessing the production database URL, matching the vault's purpose.
# Vague reason → DENY
svault judge test --vault billing-api --tier medium --scope database --secret DATABASE_URL \
--reason "asdf" --caller claude-code
# DENY score 10 — Stated reason 'asdf' is vague and unspecific.
# Reason matches the secret's description → ALLOW
svault judge test --vault billing-api --tier high --scope payments --secret STRIPE_KEY \
--description "production Stripe charge key — only for billing/charge flows" \
--reason "charge a customer invoice for the monthly subscription" --caller billing-worker
# ALLOW score 95 — Caller is a billing worker, requesting a Stripe key for a billing-api
# vault to charge a customer invoice, which aligns perfectly with the
# secret's purpose.
# Reason contradicts the secret's description → DENY
svault judge test --vault billing-api --tier high --scope payments --secret STRIPE_KEY \
--description "production Stripe charge key — only for billing/charge flows" \
--reason "export the full customer email list for a marketing campaign" --caller billing-worker
# DENY score 90 — Stated reason does not match secret's purpose. STRIPE_KEY is for
# billing/charge flows, not exporting customer email lists.
```
The last two are the headline behavior: same secret, same caller, same tier — the
**reason versus the documented purpose** is what flips the decision.
## 7. The agent request — the gate from a shell
The canonical agent door is the **MCP server** (section 10). `svault get` runs the
identical gate from a shell and is shown here because it's the quickest way to see
the gate end-to-end — but it's **deprecated** (it prints a deprecation note to
stderr and will be removed), so new integrations should wire up MCP instead.
Unlock once (or run the [daemon](daemon.md) so the key stays in memory), then the
agent path runs the full gate — policy, then tier, then judge, then audit —
**enforced inside the daemon**.
```bash
svault daemon start
svault unlock billing-api
# Granted — reason fits the secret's purpose
svault get STRIPE_KEY -v billing-api --scope payments \
--reason "charge a customer invoice for the monthly subscription" --caller billing-worker
# → sk_live_... (value to stdout; a one-line granted: status to stderr)
# Denied — reason doesn't fit
svault get STRIPE_KEY -v billing-api --scope payments \
--reason "export the customer email list for marketing" --caller billing-worker
# → denied: request not authorized for this secret
# (exits non-zero; no value printed)
```
On allow, only the value goes to stdout, so an agent capturing stdout never sees the
rationale. On deny the caller gets a single **generic** message; the real reason
(judge score plus rationale, scope or caller mismatch, rate limit) is recorded only
in the audit log, so the agent can't hill-climb a denied request into a passing one.
## 8. Tiers and fail modes
| `low` | skipped (unless `require_reason`) | allow |
| `medium` | allow if score ≥ `allow_threshold` (60) | **fail-open**, audit-flagged `judge-unavailable` |
| `high` | allow if score ≥ `high_threshold` (80) | **fail-closed** (deny) |
With the judge **off** (keyring locked, global switch off, no resolved key, or the
vault opted out), the gate falls back to the static rule (high = human-only), so
nothing regresses. To verify fail-closed, edit a judge (`svault judge edit strict`)
to point its base URL at an unreachable host, then request a high-tier secret — it
denies.
## 9. Review the audit trail
Every decision is appended to `.svault/billing-api/audit.log` (gitignored, `0600`),
stamped with the **peer UID** of the connecting process (unforgeable, unlike the
self-asserted `--caller`) and the judge's score and rationale — never the secret
value.
```bash
svault policy check billing-worker # scopes, reachable secrets, recent allows/denials
```
## 10. Hand it to an agent over MCP
MCP is the canonical agent door. The same gate that the deprecated `svault get`
runs from a shell is exposed over the [Model Context Protocol](mcp.md), so an
MCP-aware agent (Claude Code, Cursor) requests secrets directly instead of reading
`.env` files.
The human unlocks once, then wires the server (or press `m`, then `w` in the TUI):
```bash
svault daemon start # keys live in memory (optional but recommended)
svault unlock billing-api # one master passphrase opens every vault
# .mcp.json (Claude Code / Cursor) — points the agent at the gated server
# {
# "mcpServers": {
# "svault": { "command": "svault", "args": ["mcp"],
# "env": { "SVAULT_CALLER": "claude-code" } }
# }
# }
```
Now the agent calls the `svault_get_secret` tool with `name` / `scope` / `reason`
— the request runs through the *same* policy + judge gate as section 7, audited
with `source = mcp`. A weak reason is denied with the generic message; a locked
vault tells the agent a human must run `svault unlock`; high-tier stays human-only.
The agent never sees the master passphrase. See [mcp.md](mcp.md) for the tools, the
security model, and a raw transcript.
## Notes
- **Key safety.** Each judge's OpenRouter key is stored AES-256-GCM encrypted in
`.svault/keyring.enc`, never in a plaintext file. A judge with no stored key falls
back to the opt-in `$SVAULT_OPENROUTER_KEY` environment variable. Rotate a key with
`svault judge set-key <name>` if it is ever exposed (for example, pasted into a chat
or a log).
- **What the judge sees.** Secret name, scope, tier, caller, reason, recent activity,
and any vault or secret descriptions — **never the secret value**. Keep descriptions
free of sensitive data, since they are sent to the model. See
[security.md](security.md#ai-judge).
- **Honest boundary.** Svault enforces the gate for cooperative or semi-trusted agents
and keeps a tamper-resistant audit trail. It is *not* a sandbox against a hostile
same-UID process, which can read the unlocked daemon's memory directly.