Skip to content

v3.2.0 - Vault: environment keys inside Dispatch

Choose a tag to compare

@theyashgupta theyashgupta released this 31 Aug 09:14
· 1 commit to 0619ee49b837bd719f8ae5d553a0dc5ce4c1514b since this release
a954229

v3.2.0 - Vault: environment keys inside Dispatch

Agent sessions need API keys, and the worst place for those keys is anywhere the agent can read them. Dispatch now holds them itself. You write a key once, a session uses it by name, and no path in the product ever hands the value back: not to the browser, not to the agent, not to a log.

See it

Dispatch v3.2.0 the Vault page: add, fill, rotate, edit, delete

Higher-quality video: dispatch-v3.2.0-vault.mp4

A store that cannot be read back

The vault lives under ~/.dispatch/. Key names and their purposes are readable, because a session must discover what exists. Values are write-only. No API route, board payload, SSE frame, log line, or error path returns one. The values file is owner-only (0600), and the store re-asserts those permissions on every write. A harness seeds a sentinel value and sweeps every read path for it, and it fails when a read-back route is added.

The Vault page in Settings

Add a key, fill or rotate its value, edit its purpose, delete it. The value input carries four autofill opt-outs, so browser password managers do not capture what you type. The page works over the remote tunnel behind the same auth gate as the rest of the app. Values travel only in request bodies, never in a URL, which a wire-level capture verifies across a save, rotate, and reload cycle.

The runner and the guards

A session never sources the values file. It invokes vault-run with the key names it needs, then the wrapped command. The runner injects exactly those keys into that one command's environment. The invoking shell never carries the values. The runner refuses env dumpers (env, printenv, set, export, declare, typeset) and refuses unset keys, each with a message that points to Settings, Vault.

Direct reads of the store from inside a session are blocked by two layers: a PreToolUse guard hook and a permissions.deny rule. Every session kickoff includes a protocol block that teaches the agent to list key names, use the runner, and treat a refusal as expected behavior.

Boot self-configuration, uninstall, and import

A fresh install boots to a working, empty vault with zero manual steps. Every boot regenerates the runner, the guard, and the kickoff block, so an upgrade cannot strand a stale artifact. dispatch uninstall removes the regenerables and keeps the store unless you pass --purge. A one-time import copies your keys from ~/.claude/env-vault, skips names that already exist, leaves the source untouched, and reports exactly what it imported and skipped.

Audited from inside a real session

The milestone ends with an audit run from inside a real Claude session in bypass-permissions mode, driving a 12-vector exfiltration catalogue at a seeded sentinel: direct reads, cat, head, tail, interpreter open(), cp, curl, source, dot-source, chained reads, case variations, and relative paths.

Under the shipped configuration, all 12 vectors were blocked. With both guard layers removed, the same 12 vectors leaked the sentinel. That second run is the point: it proves the harness measures the guards, not the model's reluctance.

The residuals are documented in docs/ARCHITECTURE.md's threat model instead of rounded away. A legitimately wrapped command can print its own injected keys, which is inherent to per-command injection and accepted. permissions.deny alone leaked on CLI 2.1.245, so it ships as defense-in-depth while the guard hook is the load-bearing layer. A session launched below the hooks-capability floor has only the runner's refusal gate.


Full changelog: v3.0.0...v3.2.0 (includes v3.1)

Requires Node ≥ 22.22, tmux, ttyd, git, and the Claude Code CLI. Setup is in the README.