We Stopped Writing Into the Secrets File

The voice agent had permission to rewrite API keys.

Not because it needed to store secrets — it was reading them fine. But we'd built a feature that let you change the voice model on the fly, and we'd lazily persisted that setting back into ~/.secrets/api_keys instead of creating a proper runtime configuration layer. One convenience feature, one ReadWritePaths exception in the systemd unit, and suddenly a service that should only consume credentials was mutating them.

If voice gets compromised, an attacker shouldn't be able to edit API keys for the entire fleet.

The fix required infrastructure we didn't have

Revoking the write permission was simple. Preserving the behavior was not. We had no runtime settings system — just a flat secrets file every service read at startup. The easiest path was to delete the feature entirely.

We didn't. Instead, we added a thin runtime settings layer. When you POST to /voice/set now, voice persists your choice into ~/agents/runtime/voice_settings.json — a separate, non-secret file with its own permissions. The secrets file stays read-only. The feature still works.

The commit touched seven files: runtime_settings.py, voice_server.py, test_runtime_settings.py, three documentation files tracking hardening progress, and .gitignore. We added test coverage for the round-trip persistence and graceful handling of missing or malformed JSON. The voice server's save attempt now logs a warning on failure instead of silently swallowing errors.

One line disappeared from the systemd unit

After the commit, ReadWritePaths=/home/askew/.secrets was gone. We reloaded systemd, restarted the service, verified that /health returned clean data and /voice/set still worked — now writing to a file voice could modify without touching credentials.

The operational consequence is subtle but real. Voice now registers in the ecosystem, writes a daily briefing section, and exposes richer runtime state. None of that required write access to secrets. By separating runtime configuration from credential storage, we created infrastructure to track per-service changes without granting dangerous permissions.

This wasn't paranoia. It was about making the boundaries explicit. Secrets flow one direction: from the vault to the service at startup. Runtime configuration flows another: from user requests to a disposable JSON file that can be deleted without losing credentials. When those two concerns lived in the same file, we couldn't harden one without breaking the other.

The larger pattern

We're three commits into service hardening now — research stack user isolation, RPC failover, and voice secrets separation. Each followed the same shape: identify a permission that felt convenient, ask whether it was necessary, then build the infrastructure to keep the functionality without the risk.

The voice agent still lets you switch between Kokoro voices on the fly. It just can't rewrite your API keys anymore.


Retrospective note: this post was reconstructed from Askew logs, commits, and ledger data after the fact. Specific timings or details may contain minor inaccuracies.