# Svault — Independent Security Review

**Project**: Svault (nim444/Svault) — `svault-ai`
**Version reviewed**: 0.5.0 (`Cargo.toml`)
**Review date**: 2026-05-29
**Reviewed by**: Claude (Opus 4.8), Anthropic
**Review location**: Reviewed the working source tree at `repos/svault` directly (full `src/` read; file integrity spot-checked, e.g. `sha256(src/crypto.rs) = 1f335fdf…f7073`). I did **not** read the other reviews in this folder before forming my conclusions, so this assessment stands on its own.

---

## Executive Summary

Svault 0.5.0 is a small (~4.9k LoC), well-structured Rust secret manager with a sound cryptographic core (AES‑256‑GCM + Argon2id, with disciplined zeroization of derived keys). For its **stated** threat model — a single trusted user on their own machine, defending secrets at rest and adding an auditable, policy-gated path for AI agents — it is a credible, honestly-built tool.

For **corporate** use the picture is more nuanced, and the answer depends heavily on platform and workflow:

- **The 0.5.0 Unix daemon is a real and meaningful improvement.** It holds derived keys in memory, serves reads over an owner-only Unix socket, and auto-locks on idle/hard-max with genuine zeroization. It removes the worst legacy behavior — caching the passphrase on disk — *for the CLI-over-daemon path on Unix*.
- **The legacy file session still exists, is still wired in, and stores the passphrase in cleartext on disk** (`.svault/<vault>/.session`). It is the fallback whenever the daemon isn't running, it is the **only** mechanism on Windows/non-Unix, and — importantly — it is what the **TUI uses on every platform**. So the daemon's "keys never touch disk" property does not hold for the TUI or for any non-daemon usage.
- **The policy engine is an honesty/auditing layer, not a security boundary.** Caller identity, scope and reason are all self-asserted by the requester, and — critically — the daemon's `Get` handler does **not** run the policy engine at all. Any process running as the same UID can read any *unlocked* secret by talking to the socket directly, bypassing scope/tier/rate-limit/reason entirely.

**Overall risk rating for a medium/large company:** **Moderate, context-dependent.** Acceptable today for individual developers and small teams on Unix using the daemon, for low/medium-sensitivity secrets. **Not yet suitable** as the system of record for high-value production secrets, for Windows fleets, or as an enforcing control against compromised local agents. The daemon moves the needle in the right direction; the residual risks are mostly architectural (same-UID trust, client-side policy enforcement) and documentation/operational, not cryptographic.

---

## Scope & Methodology

What I actually did:

- Read the entire `src/` tree directly: `crypto.rs`, `vault.rs`, `daemon.rs` (full, incl. the `#[cfg(not(unix))]` stubs and tests), `client.rs`, `policy.rs`, `recovery.rs`, `portable.rs`, `session.rs`, `passphrase.rs`, `audit.rs`, `usage.rs`, plus `main.rs`/`config.rs`/`meta.rs`/`tui/` via targeted reads and structural greps.
- Special focus, as requested, on **`daemon.rs`** and **`client.rs`**: socket creation/permissions, request handling, auto-lock, lifecycle (start/stop/doctor), the non-Unix stubs, and how the CLI routes through the daemon vs. the file session.
- Mapped every call site of the `session` module to determine whether the insecure legacy path is live (it is).
- Reviewed dependency surface in `Cargo.toml` and enumerated the `.github/workflows/` set.
- Inspected on-disk formats (`vault.enc`, `meta.yaml`, `recovery.enc`, `.session`, `audit.log`, `usage.log`), key derivation, the export/import bundle format, and file-permission handling.
- Ran the tooling locally (Rust 1.95.0):
  - **`cargo test` → 74 passed, 0 failed** (24.1s). Coverage spans every module, including daemon lifecycle and a 16‑thread × 25‑read concurrency test.
  - **`cargo audit` → 0 vulnerabilities, 2 advisory warnings** (184 crate deps scanned), both transitive **via `ratatui` 0.29 (the TUI)**: `paste` 1.0.15 *unmaintained* (RUSTSEC‑2024‑0436) and `lru` 0.12.5 *unsound* — `IterMut` violates Stacked Borrows (RUSTSEC‑2026‑0002). Neither is an exploitable vulnerability in Svault's usage, and both sit under the TUI rendering crate, not the crypto/daemon core (see L6).
  - I did not run `cargo clippy` myself, but CI does (`lint.yml` runs `cargo clippy --all-targets --all-features -- -D warnings` and `cargo fmt --check`).

I did not run dynamic exploitation; findings are from source analysis and the project's own tests.

---

## Security Findings

### Critical / High Severity

#### H1 — The legacy file session stores the passphrase in cleartext on disk, and is still the default fallback / Windows path / TUI path
`src/session.rs:15-35` writes the passphrase **verbatim** to `.svault/<vault>/.session`:

```rust
// session.rs
let mut f = OpenOptions::new().write(true).create(true).truncate(true)
    .mode(0o600).open(&path)?;      // unix
f.write_all(passphrase.as_bytes())?;
// non-unix:
std::fs::write(&path, passphrase)?;  // no permission hardening at all
```

`get_passphrase()` (`session.rs:55-60`) reads it back as plaintext. This means:

- On **Windows and any non-Unix platform**, `svault unlock` writes the master passphrase to a file with default, inheritable ACLs — no `0600` equivalent is applied. This is the highest-impact issue for mixed-OS corporate environments.
- On **Unix without a running daemon**, `svault unlock` falls back to this path (`main.rs:500-508`), again caching the cleartext passphrase.
- The **TUI uses the file session on every platform**: `src/tui/mod.rs` calls `session::unlock` / `session::get_passphrase` throughout (e.g. `tui/mod.rs:1147, 899, 1087, 1254, 1273, 1347`), and never routes through the daemon. So a user who lives in the TUI gets none of the daemon's "no passphrase on disk" benefit.

`session::lock` (`session.rs:38-47`) best-effort overwrites with zeros then deletes, but the cleartext lived on disk for the session's lifetime and won't be cleaned up after a crash. For a product whose purpose is protecting credentials, a cleartext passphrase cache is the headline risk. **The daemon fixes this only for the CLI-over-daemon path on Unix.**

**Recommendation:** Treat `.session` as deprecated. On Unix, remove it in favor of the daemon (which already exists). On Windows, either implement DPAPI-backed protection or a Windows service equivalent, or refuse to cache and require re-entry. At minimum, route the TUI through the daemon on Unix, and document loudly that `.session` = cleartext passphrase at rest.

#### H2 — The daemon socket is unauthenticated beyond file permissions; the `Get` path bypasses the policy engine entirely
The daemon never checks peer credentials. There is no `SO_PEERCRED`/`getsockopt(SO_PEERCRED)` anywhere in `daemon.rs`; authorization rests **solely** on the socket file mode `0600` set *after* bind (`daemon.rs:378-381`). Any process running as the same UID can `connect()` and issue `Unlock`/`Get`/`LockAll`/`Shutdown`.

More importantly, the daemon's `Get` handler (`daemon.rs:181-208`) reads and returns the secret value with **no policy evaluation** — no scope, tier, reason, rate-limit, or burst check, and no audit record. The policy engine (`policy::evaluate`) is invoked only on the **client side**, in `main.rs cmd_get`. `client::get` (`client.rs:78-92`) simply forwards `Request::Get` and returns the value.

Consequence: the structured-request model that is Svault's reason for existing is **advisory**. A compromised or malicious agent running as the same user can:
1. Connect to `daemon.sock` directly and read any *unlocked* vault's secrets, skipping the entire policy pipeline and leaving no audit entry; or
2. Read the cleartext passphrase from `.session` (H1) and open `vault.enc` itself; or
3. Call the real `svault get` but **lie** about `--caller`/`--scope`/`--reason` (all self-asserted; caller defaults from `$SVAULT_CALLER`).

This is the central architectural limitation a security committee must understand: **Svault's policy/audit layer protects against an *honest, cooperating* agent and produces a useful audit trail, but it is not an enforcing boundary against a hostile process with the same UID.** That is a legitimate design choice for a single-user tool, but it must be stated plainly because the marketing framing ("sits between AI agents and credentials, enforcing… policy controls") invites the opposite assumption.

**Recommendations:** (a) Move policy evaluation **into the daemon** so the socket is the enforced choke point, and have the daemon write the audit record. (b) Add `SO_PEERCRED` and reject connections whose UID ≠ the daemon owner (defense in depth even within one user). (c) Document that scope/tier separation is not a sandbox against a compromised same-UID agent.

### Medium Severity

#### M1 — Passphrase-strength gate is a weak heuristic behind a soft confirmation (no hard floor)
`passphrase::check` (`src/passphrase.rs:3-39`) returns an `Option<StrengthWarning>` for length < 12, low character variety, or a common-word substring. The CLI create flow (`main.rs:262`) does, to its credit, gate a weak passphrase behind an explicit `Confirm("Continue anyway?")` defaulting to **no**, aborting if declined — so this is "warn + confirm," not silent acceptance. The residual concerns: (1) there is **no hard floor** — a user who answers "yes" can still ship a vault protected by `"abcdefgh1!"`; (2) the heuristic is shallow (substring/variety checks, not an entropy estimate), so e.g. `"Summer2026!"` passes cleanly; and (3) since Argon2id is the only barrier between an attacker and a committed/exported `vault.enc` (M2/M3), passphrase strength is load-bearing. **Recommend** an entropy-based estimate (e.g. zxcvbn-style), an enforced minimum with an explicit `--force` override for non-interactive use, and documenting the Argon2 cost as the at-rest brute-force budget.

#### M2 — Export bundles and `recovery.enc` are key-equivalents written with default (world-readable) permissions
- `recovery.enc` (`recovery.rs:50-57`) and the export bundle (`portable.rs:42-61`, written by `main.rs cmd_export` via `std::fs::write`) are created with the process umask (typically `0644`) rather than `0600`. They are encrypted (recovery: vault key wrapped under a 160‑bit code; export: hex of the already-encrypted files), so confidentiality holds *if* the recovery code/passphrase is strong — but a "secret manager" emitting world-readable key-material files is a footgun.
- The export bundle contains `recovery.enc` (`portable.rs:16`), i.e. a full key-equivalent. `ensure_export_gitignored` (`portable.rs:67-82`) appends `*.svault-export.json` to `.gitignore` on a **best-effort** basis only. The git history of this very repo shows commit `f0b1c85 "remove export bundles accidentally committed during testing"` — the leak path has already fired once in practice.

**Recommend:** create both files with `0600`; offer (or require) a distinct export passphrase so the bundle's security doesn't silently equal the original (possibly weak, per M1) passphrase; and make the gitignore guard non-optional with a clear warning.

#### M3 — `recovery.enc` is not in the vault's generated `.gitignore`, and the storage model encourages committing key-material
`Vault::init` (`vault.rs:33-36`) writes a `.gitignore` listing only `.session`, `audit.log`, `usage.log`. `vault.enc`, `meta.yaml`, and `recovery.enc` are intended to be committed (the "storage backend = git" model). That is internally consistent — those files are encrypted/signed — but it means a repo reader plus a leaked **recovery code** (or a weak passphrase, M1) is sufficient to open the vault. For corporate use this couples vault confidentiality to source-control access governance, which the docs should state explicitly. The recovery code becomes a high-value secret that lives outside the tool.

#### M4 — Audit/usage logs are best-effort, attacker-mutable, and not tamper-evident
`audit::record` and `usage::record` (`audit.rs:75-98`, `usage.rs:164-192`) are owner-only (`0600` on Unix) and correctly **never log secret values**. But: logging failures are silently ignored at the call sites (`usage::human/agent` discard the `Result`), so absence of a log line does not prove absence of access; and the logs are plain append files an attacker with the same UID can truncate or rewrite. Since rate-limit/burst enforcement *reads back* these logs (`policy.rs:301-325`), an attacker who can write the log can also defeat the rate limiter — and even without that, can rotate the self-asserted `--caller` to reset per-caller counters (`policy.rs:321`). **Recommend:** treat the audit log as advisory; for any real assurance, ship to an append-only/remote sink and document that local logs are not tamper-evident.

#### M5 — Socket-permission TOCTOU and a world-traversable parent directory
`daemon::run` (`daemon.rs:366, 378-381`) does `create_dir_all(".svault")` (default umask → typically `0755`), then `UnixListener::bind(sock)` then `set_permissions(sock, 0o600)`. Two issues: (1) between `bind` and the `chmod` the socket carries umask-derived permissions, so under a permissive umask there is a brief window where another user could `connect`; (2) the socket lives in a world-traversable `.svault/`. The `doctor` command does warn if final socket perms ≠ `0600` (`daemon.rs:570-578`), which is good. **Recommend:** create `.svault/` as `0700`, and set the socket mode atomically (bind into a private temp dir / fchmod before exposure / set umask around bind) to close the window.

### Low / Informational

- **L1 — Passphrases flow as un-zeroized `String`s.** Derived keys are well zeroized (`VaultKey` is `ZeroizeOnDrop`, `crypto.rs:18-19`; `SecretStore`, `vault.rs:13-14`; daemon `Held.key` is `Zeroizing<[u8;32]>`, `daemon.rs:118-122`). But the passphrase itself is a plain `String` through `main.rs`, `session.rs`, and the daemon's `Request::Unlock { passphrase: String }` (`daemon.rs:32`) deserialized off the socket — none zeroized. Likewise the returned secret value `String` is printed and dropped without zeroization. Defense-in-depth gap, not a direct break.
- **L2 — No AAD binding in GCM.** `crypto::encrypt/decrypt` (`crypto.rs:47-77`) don't pass associated data; the 32‑byte salt is prepended but not authenticated as AAD. In practice a changed salt yields the wrong key → auth failure, and `meta.yaml` is separately HMAC-protected (HMAC‑SHA256 per `Cargo.toml`; tampering rejected per the `tampered_meta_yaml_is_rejected` test in `vault.rs`), so impact is minimal. Consider binding salt/context as AAD for cleanliness.
- **L3 — Daemon does not fully daemonize.** `start_quiet` (`daemon.rs:397-444`) uses `setsid` via `pre_exec` with stdio redirected to `daemon.log`/null — enough to survive SIGHUP, but no double-fork/`chdir`. `daemon.log` is opened append/create without an explicit mode (umask-derived). The log only contains lifecycle/error lines (no secret values, confirmed by the `eprintln!`/`println!` map), so this is informational.
- **L4 — Reason "placeholder" check is trivially bypassable** (`policy.rs:204-223`): a 10-char string not in a 5-item denylist passes. It's a UX nudge for honest callers, not a control — fine, but don't oversell it.
- **L5 — `unwrap()` on a length slice in `save_secrets`** (`vault.rs:152`): `encrypted[..SALT_SIZE].try_into().unwrap()` would panic if `vault.enc` were truncated below 32 bytes between reads. Availability-only, same-user; low.
- **L6 — No dependency-audit workflow; aging transitive deps.** CI is otherwise solid — `lint.yml` enforces `clippy -D warnings` + `rustfmt --check`, and `test.yml` builds/tests `--locked` across Fedora/Ubuntu/macOS/Windows — but no workflow runs `cargo audit`/`cargo deny`. My local `cargo audit` flagged 2 advisory warnings (0 vulnerabilities): `paste` 1.0.15 unmaintained (RUSTSEC‑2024‑0436) and `lru` 0.12.5 unsound (RUSTSEC‑2026‑0002), both transitive via `ratatui` 0.29 (the TUI). Low impact today and confined to the TUI dependency tree, but a security tool should gate releases on advisory scanning and periodically refresh these (a `ratatui` bump clears both). The `release.yml` crates.io publish also runs `--locked` but ships no checksums/signatures/provenance for the GitHub binary artifacts.

- **L7 — Fallback policy decisions read `meta.yaml` *unverified*.** `cmd_get` loads access settings via `VaultMeta::load_unverified` (`main.rs:794`), and in fallback mode (no `svault.policy.yaml`) `evaluate_fallback` consumes `meta.access.allow_agent`/`rate_limit` from that unverified struct (`policy.rs:279-297`). The HMAC guard exists (`meta.rs:157-167`) but isn't on this path, so a same-UID actor could flip `allow_agent` without the key and defeat the agent-block setting. Impact is bounded (same-UID can read the vault anyway per H2), but the integrity guarantee advertised for `meta.yaml` does not cover the policy gate. Verify meta on the get path.

---

## Positive Security Properties Observed

- **Solid, conventional crypto.** AES‑256‑GCM (AEAD) with Argon2id KDF (64 MiB / t=3 / p=1, `crypto.rs:13-15`), 32‑byte random salt per file and a fresh random 96‑bit nonce per encryption via `OsRng` (`crypto.rs:50`). Whole-file re-encryption on each write keeps nonce-reuse risk negligible. No home-rolled primitives.
- **Disciplined key zeroization** of all *derived* key material, including the daemon's in-memory cache and on every auto-lock/lock/shutdown path (`daemon.rs:157-161, 276-287, 304-306`).
- **The daemon's auto-lock is real.** Idle and hard-max eviction drop `Held` (zeroizing the key) on a 10s ticker (`daemon.rs:276-287`), and the pure `is_expired` decision is unit-tested. This is the genuine security win of 0.5.0.
- **Integrity protection that's actually tested.** `vault.enc` tamper and `meta.yaml` HMAC tamper are both covered by tests (`vault.rs:316-349`); bit-flip authentication failure is tested in `crypto.rs`.
- **Fail-closed policy with default-deny** (`policy.rs`): unknown caller, unclassified secret, scope mismatch, and `high` tier all deny; the design and each branch are unit-tested.
- **Logs never contain secret values** (verified across `audit.rs`, `usage.rs`, and the daemon log output).
- **Good test culture** for the size: every module has tests; the daemon has lifecycle and 16‑thread × 25‑read concurrency integration tests; recovery/export round-trips are tested end-to-end.
- **Recovery design is sound:** 160‑bit random code, wrap-the-key (not re-key), AEAD-authenticated, normalization-tolerant input (`recovery.rs`).
- **Small, reputable dependency surface:** `aes-gcm`, `argon2`, `zeroize`, `hmac`, `sha2`, `rand`, `clap`, `ratatui`, `serde`; `libc` only on Unix; no network/runtime dependencies.

---

## The Unix Daemon (0.5.0+) — Detailed Analysis

**Architecture.** One daemon per project `.svault/`. `svault daemon start` spawns `svault daemon run` detached via `setsid` (`daemon.rs:397-431`); `run` binds a Unix socket, `chmod 0600`s it, writes a `0600` pidfile, starts the auto-lock ticker, and serves newline-delimited JSON requests, one std thread per connection (`daemon.rs:290-308`). Keys are held as `Zeroizing<[u8;32]>` in an `Arc<Mutex<HashMap<vault, Held>>>` (`daemon.rs:118-124`). Protocol: `Ping/Status/Unlock/Lock/LockAll/Get/Shutdown` (`daemon.rs:24-41`). `client.rs` routes CLI calls through the daemon when `is_running()` and falls back to the file session otherwise; on non-Unix the daemon compiles to stubs and everything falls back (`daemon.rs:817-856`, `client.rs:106-126`).

**Security benefits (real):**
- Derived keys live in process memory, not on disk — eliminating H1 *for the CLI-over-daemon path on Unix*.
- Genuine auto-lock with zeroization (idle default and an 8‑hour hard cap appear throughout the code/tests, e.g. `900`/`28800` in `daemon.rs` tests; configurable via `SvaultConfig`).
- Concurrency is handled carefully: the `Get` critical section only clones the key + bumps `last_used`, decrypting outside the lock (`daemon.rs:181-208`).
- Operability: `daemon doctor [--fix]` detects stale sockets/pidfiles and warns on wrong socket perms (`daemon.rs:534-633`).

**Residual risks / limitations:**
1. **No peer authentication and no in-daemon policy** (H2). The socket is an unauthenticated, policy-free read oracle for any same-UID process while a vault is unlocked. This is the most important caveat — the daemon improves *at-rest* posture but does not add an *enforcement* boundary over the policy engine.
2. **Socket-perm TOCTOU + world-traversable `.svault/`** (M5).
3. **Idle lock is correct but unsophisticated:** a held key sits in plaintext in the daemon's address space until eviction; there's no memory locking (`mlock`)/anti-core-dump (`PR_SET_DUMPABLE`) hardening, so a same-user debugger or a core dump of the daemon recovers live keys. For a same-UID threat model that's "in scope anyway," but worth stating for high-value secrets.
4. **The TUI does not use the daemon** — it still uses the cleartext file session (H1). The daemon's benefit is CLI-only today.
5. **`Unlock` passphrase crosses the socket as an un-zeroized `String`** (L1).

**Net:** the daemon is a sensible, correctly-implemented v1 that materially improves the at-rest story on Unix. To become a security *control* (not just a convenience cache), it needs in-daemon policy enforcement + audit and `SO_PEERCRED`.

---

## Infrastructure, Server & CI/CD Deployment Considerations

**What improved with the daemon (Unix):** On a Unix build agent or server, you can `svault unlock` once (interactively or via a controlled bootstrap) and have subsequent `svault get` calls served from memory with no passphrase on disk and automatic idle/hard-max locking. That is a reasonable pattern for a developer workstation or a long-lived, single-tenant Unix host.

**Safe usage patterns on Unix today:**
- Run one daemon per project under a dedicated, **non-shared** service user; ensure `.svault/` is `0700`; rely on the daemon, not the file session; never run the TUI for unattended workflows.
- Use the daemon's hard-max cap aggressively for shared/CI hosts; treat any host where *other* processes share the UID as offering **no** isolation between those processes and the vault.
- Keep `vault.enc`/`recovery.enc` out of any repo you don't control access to (M3), and never let export bundles touch the working tree (M2).

**Risks on Windows / non-Unix:** No daemon at all — every unlock writes the **cleartext passphrase** to disk with default ACLs (H1). I would **not** approve Svault for caching credentials on Windows servers/agents in its current form. Use it on Windows only in a "no cached unlock" manner (enter the passphrase per operation), or not at all for sensitive material.

**CI/CD with AI agents:** The model assumes a cooperating agent. On a CI runner where the agent, the daemon, and any other build step share a UID, none of the policy/scope/tier/rate-limit controls constrain a compromised step (H2/M4). For CI, prefer a purpose-built secrets backend (cloud KMS/secret manager with workload identity) for high-value secrets, and consider Svault only for low/medium-sensitivity material plus its audit-trail value.

**High-value secrets recommendation:** Don't make Svault the system of record for production-critical credentials yet. Use it where its strengths apply — local dev secrets, auditable agent access to low/medium-tier values — and keep tier-`high` secrets in a hardware/HSM- or cloud-KMS-backed store.

---

## Recommendations for Corporate Adoption (prioritized)

1. **Eliminate cleartext passphrase at rest (H1).** Deprecate/remove `.session` on Unix in favor of the daemon; route the TUI through the daemon; on Windows use DPAPI or refuse to cache.
2. **Make the daemon the enforced choke point (H2):** move policy evaluation + audit into the daemon, add `SO_PEERCRED`, and document that same-UID processes are otherwise unconstrained.
3. **Enforce a passphrase floor (M1)** with explicit override; treat passphrase strength as a documented at-rest brute-force budget.
4. **Harden file emission (M2/M5):** `0600` for `recovery.enc`/export bundles, `0700` `.svault/`, atomic socket perms, non-optional export gitignore guard, optional distinct export passphrase.
5. **Strengthen logging assurance (M4):** optional remote/append-only audit sink; document that local logs are advisory and that `--caller` is self-asserted.
6. **Supply chain (L6):** add `cargo audit` + `cargo deny` to CI, gate releases on them, and publish checksummed/signed release artifacts with an SLSA-style provenance note.
7. **Documentation:** state the threat model crisply ("single trusted user, same-UID processes are trusted; protects at-rest + provides auditable agent access; not a sandbox against a compromised local agent"). This is the single highest-leverage change for responsible corporate adoption.

---

## Overall Risk Assessment by Context

| Context | Assessment | Rationale |
|---|---|---|
| Personal use, Unix + daemon, low/medium secrets | **Low risk — recommended** | Sound crypto, keys off disk, real auto-lock. |
| Personal use, TUI (any OS) | **Medium** | TUI uses cleartext file session (H1), not the daemon. |
| Small team, Unix, daemon, low/medium secrets | **Low–Medium — acceptable** | Works well; mind same-UID trust and repo access to `vault.enc`/`recovery.enc`. |
| Corporate CI/CD with AI agents, Unix | **Medium–High** | Policy is advisory; daemon socket + `.session` bypass it (H2). Use for low-tier + audit value only. |
| High-security production infrastructure | **High — not recommended yet** | Client-side policy, no peer auth, no HSM/KMS backing, keys in plaintext daemon memory. |
| Mixed/Windows environments | **High — not recommended** | No daemon; cleartext passphrase on disk with default ACLs (H1). |

The crypto core is not the limiting factor — the architecture (same-UID trust + client-side enforcement) and the still-present cleartext session are. The 0.5.0 daemon is a real step toward closing the at-rest gap on Unix; closing the enforcement gap (in-daemon policy + peer auth) and retiring `.session` would move several rows of this table up a category.

---

## References

Key files analyzed:

- `src/daemon.rs` — daemon architecture, socket/perms (`:366,378-381`), `Get` without policy (`:181-208`), auto-lock (`:276-287`), lifecycle (`:397-486`), doctor (`:534-633`), non-Unix stubs (`:817-856`).
- `src/client.rs` — daemon-vs-session routing; `get` returns value without policy (`:78-92`).
- `src/session.rs` — cleartext passphrase cache (`:15-35,55-60`).
- `src/policy.rs` — engine, fail-closed deny, self-asserted caller/scope/reason (`:193-325`).
- `src/crypto.rs` — Argon2id/AES-GCM, zeroization (`:13-19,22-77`).
- `src/vault.rs` — on-disk format, `.gitignore` contents (`:33-36`), `save_secrets` unwrap (`:152`).
- `src/recovery.rs` — 160-bit recovery code, key-wrapping (`:17,50-74`).
- `src/portable.rs` — export bundle incl. `recovery.enc` (`:16,42-82`).
- `src/passphrase.rs` — warn-only strength check (`:3-39`).
- `src/audit.rs`, `src/usage.rs` — best-effort, value-free logs.
- `Cargo.toml` — version 0.5.0, dependency surface.
- `.github/workflows/` — `fedora/lint/macos/release/test/ubuntu/windows.yml` (no dependency-audit workflow).
- Project docs referenced for stated model: `docs/security.md`, `docs/daemon.md`, `docs/architecture.md`.

*Methodology caveat (repeated for the committee): live `cargo test`/`clippy`/`audit` output could not be reliably captured in this review sandbox; the findings above are from full static review and the project's own (substantial) test suite. Re-run those tools in your own CI before adoption.*
