This document describes ClawVault's security architecture, threat model, and troubleshooting guidance. It is intended for both human developers and AI agents that operate on or integrate with ClawVault.
Secret values must never enter AI-visible context.
This means secret values must never appear in:
- Error messages, thrown exceptions, or rejection reasons
- CLI stdout/stderr output
- HTTP response bodies
- Log output (console.log, console.error, console.warn)
- Test output or assertion messages
- Configuration files or environment variable definitions visible to agents
Risk: If secret values or names are interpolated into shell command strings, an attacker who controls a secret value could execute arbitrary commands.
Mitigation:
- All storage providers use
execFile()with argument arrays, notexec(). execFilebypasses the shell entirely -- arguments are passed as C-level argv entries, so metacharacters like$(), backticks, pipes, and semicolons have no special meaning.- Secret names are validated against
/^[A-Z][A-Z0-9_]*$/before any command is constructed. This prevents injection via crafted names.
Verification: The tests in test/unit/storage/ verify that dangerous
characters in secret values pass through safely. The context-leak tests in
test/security/ scan source files for patterns that could leak values.
Risk: The "confidant" plugin executes npx @aiconnect/confidant, which
downloads and runs arbitrary code from npm at runtime. A compromised package
could exfiltrate secrets.
Mitigation:
- ClawVault never uses
npx,npm exec, or any runtime code download. - All external commands are OS-provided binaries with known paths:
- Linux:
secret-tool,gdbus,systemctl - macOS:
security - Windows:
cmdkey,powershell
- Linux:
- Dependencies are locked in
package-lock.json. Usenpm ci(notnpm install) in CI to ensure reproducible builds. - Run
npm run audit:securityto check for known vulnerabilities.
Risk: The web UI starts an HTTP server that accepts secret submissions. If exposed to the network (via tunneling, binding to 0.0.0.0, or port forwarding), any network attacker could submit or enumerate secrets.
Mitigation:
- Server binds to
localhost(127.0.0.1) by default. - Binding to any non-localhost address triggers a prominent security warning.
- Bearer token authentication: A cryptographically random 64-character token
is generated at startup and printed to the terminal. All API requests require
Authorization: Bearer <token>. - Rate limiting:
/api/submitis limited to 30 requests per 15-minute window. - CORS: Origin is locked to the server's own
scheme://host:port. Cross-origin requests from malicious browser pages are blocked. - Helmet: Comprehensive security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, etc.) are applied.
- No secret retrieval endpoint: There is intentionally no API to read secret values. Only metadata (names, counts) is returned.
Risk: Other processes running as the same user could access the keyring or the web server's API.
Mitigation:
- Platform keyrings (macOS Keychain, GNOME Keyring, Windows Credential Manager) are session-locked and require user authentication for access.
- The fallback encrypted file provider uses file permissions (mode 0600) and machine-specific key derivation to limit access.
- The web server's bearer token is only printed to the terminal; other local processes would need to read the terminal output to obtain it.
Risk: The fallback provider (used when no keyring is available) derives its encryption key from a machine identifier. If the machine-id is predictable (e.g., in Docker containers), the key is weaker.
Mitigation:
- The fallback provider reads
/etc/machine-id(Linux) as primary key material. - A 32-byte random salt is generated on first use and stored in
~/.clawvault/.saltwith mode 0600. - If no machine-id is available, a username-based fallback is used with an explicit warning.
- The fallback provider always emits a prominent warning encouraging users to install platform keyring tools.
| Provider | Platform | Backend | Command | Injection-Safe |
|---|---|---|---|---|
LinuxKeyringProvider |
Linux | GNOME Keyring | secret-tool, gdbus |
Yes (execFile) |
MacOSKeychainProvider |
macOS | Keychain | security |
Yes (execFile) |
WindowsCredentialManager |
Windows | Credential Manager | cmdkey, powershell |
Yes (execFile) |
FallbackProvider |
Any | Encrypted file | None (crypto only) | N/A |
Request → Helmet → CORS → Body Parser → Auth (Bearer) → Rate Limiter → Route Handler
helmet: Security headers (CSP, HSTS, X-Frame-Options, etc.)cors: Origin allowlist locked to server's own addressexpress.json/express.urlencoded: Body parsing with 64KB limit- Auth middleware: Validates
Authorization: Bearer <token>header express-rate-limit: 30 requests per 15 minutes on/api/submit
AuditedStorageProvider wraps any StorageProvider and emits structured JSON
events for every operation. Events contain:
{
"timestamp": "2026-02-09T12:00:00.000Z",
"operation": "set",
"secretName": "OPENAI_API_KEY",
"success": true
}Events never contain secret values. Audit handler failures are caught and silently ignored to prevent audit issues from blocking secret access.
- The
securityCLI returned an error. Check that the user has keychain access. - If the error is about a duplicate, the provider handles this automatically.
- If persistent, the user may need to unlock their keychain:
security unlock-keychain.
- Secret names must match
/^[A-Z][A-Z0-9_]*$/. - Common mistakes: lowercase letters, hyphens, spaces, leading digits.
- Example valid names:
OPENAI_API_KEY,DB_PASSWORD,AWS_SECRET_KEY.
- The web server requires a bearer token for all API routes except
/health. - The token is printed to the terminal when the server starts.
- Include it as:
Authorization: Bearer <token>in the request header.
- Rate limit hit on
/api/submit(30 requests per 15-minute window). - Wait 15 minutes or restart the server to reset the window.
- The browser is making a cross-origin request from a different origin.
- The server only allows requests from its own origin (e.g.,
http://localhost:3000). - Ensure the browser is accessing the same host:port the server is bound to.
- No platform keyring tools detected. Install them:
- Linux:
apt install libsecret-tools - macOS: Built-in (should not see this on macOS)
- Windows: Built-in (should not see this on Windows)
- Linux:
- The user passed
--hostwith an address other than localhost/127.0.0.1/::1. - This exposes the secret submission endpoint to the network.
- Only appropriate on trusted, firewalled networks with TLS enabled.
- Check that the secret name in the config matches exactly (case-sensitive).
- Run
clawvault listto verify the secret exists in the keyring. - Check
systemctl --user show-environment(Linux) to see injected vars. - The gateway injection writes to the process environment and optionally to systemd user sessions.
- Always run:
npm run build && npm test && npm run lint - Pay attention to
test/security/context-leak.test.ts-- it scans source files for patterns that could leak secret values. - Check that no
exec()calls were introduced (onlyexecFileis allowed).
- No
exec()orexecSync()calls with string interpolation of user/secret data - All new external commands use
execFile()with argument arrays - Secret values never appear in error messages, logs, or HTTP responses
- Secret names validated against
/^[A-Z][A-Z0-9_]*$/before use - New CLI commands do not call
storage.get()or expose retrieved values -
test/security/context-leak.test.tsstill passes - Web routes return metadata only (names, lengths, counts)
- No
npx,npm exec, or runtime code download introduced - Dependencies added to
package.jsonare necessary and from trusted sources