Private/public profiles¶
Ghostvault can split your accounts into two tiers: public profiles that are always accessible to the agent, and private profiles that are hidden behind a password until you explicitly unlock them. This is an app-level visibility gate — a "drawer lock" for sensitive accounts.
Why this matters¶
Imagine you keep a Work account, a Personal account, and a Banking-SSO account in Ghostvault, all signed in. Now someone sits down at your unlocked laptop and tells the agent: "Read my latest emails." Without a gate, the agent will happily drive any of those accounts — including the banking one.
The private/public gate solves exactly this scenario. Mark the sensitive profiles private, set a password, and:
- When locked,
gv_list_accountsdoesn't show private profiles at all. Browser tools refuse to open them. The agent literally cannot see they exist. - When unlocked (you typed the password this session), everything works normally.
- After 5 wrong password attempts, all private profiles are permanently deleted and the password resets. No brute-force escape hatch.
This is access control, not encryption
The private/public gate protects against someone using the agent on your unlocked machine. It does not protect the profile files on disk — for that you need encryption at rest. The two features solve different threats and compose cleanly. See Difference from encryption at rest below.
The concept¶
There is one password for the whole Ghostvault instance. It gates visibility of any profile you mark private.
┌──────────────────────────────────┐
│ Ghostvault instance │
│ │
LOCKED state: │ public profiles → visible │
(default) │ private profiles → HIDDEN │ ← agent can't see or open these
│ │
UNLOCKED state:│ public profiles → visible │
(after gv_unlock) private profiles → visible │ ← stays unlocked until gv_logout or restart
└──────────────────────────────────┘
The unlock is per server session — it persists until you call gv_logout, or until the Ghostvault server restarts. There is no timeout; if you unlock and walk away, it stays unlocked until you explicitly re-lock.
The tools¶
| Tool | Role |
|---|---|
gv_setup_password |
Set the gate password (opens a browser window for entry). One-time setup. |
gv_make_private |
Mark a profile as private (hidden when locked). |
gv_make_public |
Mark a profile as public (always accessible). |
gv_unlock |
Unlock the gate (password or browser window). Reveals private profiles for this session. |
gv_logout |
Re-lock the gate, close private browser contexts, switch active to a public account. |
gv_get_auth_status |
Check whether the gate is active and currently unlocked. |
gv_list_accounts |
Returns public profiles always; private profiles only when unlocked. |
Recommended workflow¶
This is the full lifecycle — the same flow demonstrated in examples/scripts/sdk_private_public.py:
1. Check current status¶
2. Set the password¶
gv_setup_password opens a browser window where you type and confirm the password. Ghostvault never sees the plaintext — it stores only a salted hash in ~/.ghostvault/auth.json.
Pick a password you'll remember
There is no recovery path. If you forget the gate password, you can reset it (delete ~/.ghostvault/auth.json) but you will lose access to any profiles currently marked private — they stay hidden until the password is verified, and 5 wrong guesses wipes them. Use a password manager.
3. Create / mark sensitive accounts as private¶
# Create a new account
result = await gv.create_account(name="Banking-SSO", os="macos")
account_id = result["id"]
# Mark it private
await gv.make_private(account_id)
Or mark an existing account private — the call is the same:
4. Verify it's hidden when locked¶
# While LOCKED (the default after setup):
await gv.list_accounts()
# Returns only your public profiles. "Banking-SSO" is invisible.
# Try to open it directly — fails:
await gv.open_account("acc_banking123")
# → ToolError: account is private and the gate is locked
5. Unlock when you need access¶
await gv.unlock("your-password")
# {"unlocked": true}
await gv.list_accounts()
# Now "Banking-SSO" appears. Browser tools work on it normally.
gv_unlock also supports a browser-window mode (no plaintext password over the MCP wire) — call it without an argument and a window opens for entry.
6. Re-lock when you're done¶
await gv.logout()
# Closes any open private browser contexts, re-locks the gate,
# and switches the active account to a public one (if any).
Make this a habit. The unlock persists for the whole server session — if you walk away from your machine after unlocking, private profiles stay reachable until you explicitly gv_logout or restart Ghostvault.
Brute-force protection¶
The gate is designed to fail closed under attack:
| Wrong attempts | What happens |
|---|---|
| 1–2 | Incremented counter; attempt continues. |
| 3 | Warning returned with remaining attempts (e.g. "2 attempts remaining before private profiles are wiped"). |
| 5 | All private profiles permanently deleted. Password resets. Gate turns off. |
5 wrong guesses = permanent deletion
This is by design. The threat model is "attacker sits at your unlocked machine and brute-forces the gate." The only safe response to brute force is to destroy the protected data faster than the attacker can guess. Public profiles are never affected — they survive a wipe. If you have accounts you cannot afford to lose under any circumstances, leave them public (or back up their sessions separately).
If you forget the password and don't want to risk the wipe: stop guessing, delete ~/.ghostvault/auth.json manually, and accept that the private profiles are inaccessible.
This trade-off (data destruction over data leakage) is the whole point. If it makes you nervous, that's correct — store only profiles you're willing to lose behind the gate, or keep backups elsewhere.
Difference from encryption at rest¶
These two features are complementary, not redundant. They protect against different threats:
| Private/public gate | Encryption at rest | |
|---|---|---|
| Threat | Someone uses the agent on your unlocked machine | Someone steals the disk (laptop theft, backup leak) |
| What it protects | Visibility / accessibility via the agent | The profile files on disk |
| What it does | Hides private profiles from gv_list_accounts, refuses to open them |
Stores profiles as AES-256-GCM archives, decrypts only while running |
| Failure mode | 5 wrong guesses → wipe private profiles | Lose the keychain entry → lose all encrypted profiles |
| Enabled by | gv_setup_password |
GHOSTVAULT_ENCRYPTION_ENABLED=true |
| Default | Off | Off |
Use both for sensitive accounts. The gate stops the person at your keyboard; encryption stops the person with your hard drive. See Encryption at rest for the disk side.
A realistic setup¶
Say you have three accounts and want different protection levels:
| Account | Tier | Why |
|---|---|---|
Work |
public | You want the agent to read mail and drive docs without friction |
Personal |
private | Personal inbox — hide from casual access |
Banking-SSO |
private + encryption | High-value session, both layers |
# One-time setup
await gv.setup_password() # set the gate password
await gv.make_private("acc_personal")
await gv.make_private("acc_banking")
# Banking also gets encryption at rest (set via env, applies to all profiles):
# GHOSTVAULT_ENCRYPTION_ENABLED=true
Daily use:
You (morning): "Read my Work emails." → agent uses Work directly (public)
You (later): "Check my Personal inbox."
→ agent can't see Personal (locked)
→ you: "Unlock with password X"
→ agent: gv_unlock("..."), reads Personal
You (done): "Lock up." → gv_logout, Personal hidden again
Via the SDK¶
The SDK flow is identical to the conversation flow — same tools, same order. Here's a minimal script (condensed from the example):
import asyncio
from ghostvault_sdk import GhostvaultClient
async def main():
async with GhostvaultClient() as gv:
# 1. One-time: set the password (browser window opens)
await gv.setup_password()
# 2. Create + mark private
result = await gv.create_account(name="Secret", os="macos")
account_id = result["id"]
await gv.make_private(account_id)
# 3. While locked: private hidden
await gv.list_accounts() # "Secret" not in the list
# 4. Unlock
await gv.unlock("your-password")
# 5. While unlocked: private visible
await gv.list_accounts() # "Secret" appears
# 6. Re-lock
await gv.logout()
await gv.list_accounts() # "Secret" hidden again
asyncio.run(main())
gv_setup_password needs a display
The password setup opens a real browser window for entry (so the password never travels the MCP wire in plaintext). That means it needs a display — on a headless server you'll need Xvfb or VNC. The other gate tools (gv_unlock with a password argument, gv_make_private, etc.) do not open a window.
Troubleshooting¶
| Symptom | Fix |
|---|---|
gv_setup_password hangs |
It's waiting for you to complete entry in the browser window. Make sure GHOSTVAULT_HEADLESS=false and you have a display. |
| Private profile reappears after restart | The unlock is per-session. After a server restart the gate is always locked. Call gv_unlock again. |
| Forgot the password | Stop guessing (5 wrong = wipe). Delete ~/.ghostvault/auth.json to reset the gate; private profiles become inaccessible but public ones are fine. |
gv_make_private on an already-open account |
Close it first with gv_close_account. The flag applies on next open. |
| Agent can still see private profiles | Check gv_get_auth_status — the gate is probably unlocked from earlier. Call gv_logout to re-lock. |
Related guides¶
- Encryption at rest — the complementary disk-protection layer; use both for sensitive accounts
- Reading Gmail — typical reason to mark an account private (sensitive inbox)
- Tool reference — full list of lifecycle tools