Status: PLAN, not yet implemented (2026-05-07).
Prerequisite for: Real OS-enforced trust boundary (Day 4 #4 closure).
Day 4 #4 attempted uid separation by giving a non-root brian-validator user
ownership of the HMAC key. Codex round-table flagged this as a fundamental
design flaw: HMAC is symmetric. The gate hook runs INSIDE Brian's CC process
and needs to verify signatures. To verify it must read the key. But if Brian's
CC process can read the key, it can also forge entries.
Symmetric crypto cannot give us uid separation when the verifier and the
forger run in the same process.
The fix: switch from HMAC-SHA256 to Ed25519 asymmetric signatures.
brian-validator (system user, no shell)
├── /root/.claude/system/secrets/validator_ed25519.key (private signing key, 0400 brian-validator)
└── /root/.claude/system/secrets/validator_ed25519.key.prev (rotation overlap)
shared (everyone-readable):
└── /root/.claude/system/secrets/validator_ed25519.pub (public verification key, 0444)
Brian-CC process (regular user):
├── invokes: sudo -u brian-validator /opt/agent/scripts/brian_validator.py
├── reads: validator_ed25519.pub for gate's _verify_signature
└── CANNOT read: validator_ed25519.key (private key, OS-enforced 0400)
The brian-validator process signs entries with its private key. Brian's CC
process verifies signatures using the public key. Even if Brian-CC is fully
compromised, it cannot forge — private key is unreachable.
cryptography package (already a dep) — Ed25519PrivateKey /Ed25519PublicKey_sign_entry(entry) using private key from validator_ed25519.keyhmac field (legacy) AND signature_verify_signature(entry) using public key from validator_ed25519.pubpython3 -c "
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import (
Encoding, PrivateFormat, PublicFormat, NoEncryption)
import os
priv = Ed25519PrivateKey.generate()
priv_bytes = priv.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption())
pub_bytes = priv.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
open('/root/.claude/system/secrets/validator_ed25519.key', 'wb').write(priv_bytes)
open('/root/.claude/system/secrets/validator_ed25519.pub', 'wb').write(pub_bytes)
os.chmod('/root/.claude/system/secrets/validator_ed25519.key', 0o400)
os.chmod('/root/.claude/system/secrets/validator_ed25519.pub', 0o444)
"
/opt/agent/scripts/setup_validator_uid_separation.sh prep
This creates brian-validator + sudoers entry. Update its key chown logic
to also chown the new Ed25519 private key.
brian-cc group + user/root/.claude/projects/-/ and other CC dirs to brian-ccsudo -u brian-validator /opt/agent/scripts/brian_validator.py .../etc/passwd (no, it shouldn't need to) butvalidator_ed25519.key_hmac_sign from validator runnerThis plan is the spec; the implementation should be done in a fresh session
with full operator attention. The Day 4 #4 prep script does the safe parts
(create user, install sudoers); this plan does the unsafe parts (uid switch
of running services).