Skip to content
Get early access

Personal overrides

Some values belong in the environment but not to the team. Your own OAuth refresh token. The API key for your personal sandbox tenant. The port your machine happens to have free.

A personal override stores your value for a key alongside the shared one, encrypted so that only you can decrypt it — and it wins over the shared value when you run the app. Teammates keep seeing theirs.

Every environment has a shared stream — the one everybody with a grant sees — and, per member, an optional personal overlay. What cendarum run injects is the effective map, composed under a frozen precedence:

ambient < shared < personal

Your real process environment is inherited, the shared values overlay it, and your personal overrides overlay those. run takes no --scope flag, deliberately: injecting the composition is the feature, and a flag would imply it is optional. If you need a single stream as a file, that is export --scope.

A personal override is a scope inside an environment, not a third level of hierarchy. It has no name of its own, no grants of its own, and no existence apart from the environment it overrides.

Not your teammates. Not a project admin. Not the server, and not us.

The key that encrypts your overlay is derived on your device from your own private key. It is never wrapped, never uploaded, and exists nowhere else — the server stores no key material for this feature at all. And the API has no route that could serve your overlay to another user: neither personal endpoint takes a user id, so cross-user access is unrepresentable, not merely refused.

How the personal key is derived has the full construction.

When a personal override shadows a shared value, every run (and every export --scope effective) says so — one value-free stderr line, key names only:

cendarum: personal overrides shadow the shared values of: API_URL — verify these are still current (a rotated team value stays shadowed by your override)

The warning cannot know “stale” — only “winning”. If a teammate rotates a shared credential, your override keeps winning silently for you; this line is the disclosure. A personal-only key is not a shadow and is not named, and a key excluded by --only or the loader denylist is not named either, because it is not injected.

import and export require --scope. There is no default, deliberately: a merged default is how a personal secret ends up inside a .env you hand to a teammate. You name the stream you are writing, every time, and a missing --scope is refused before any password prompt or network call.

Terminal window
# Write the team's stream
cendarum import --env dev --scope shared --file .env
# Write your own overlay — readable by you alone
printf 'MY_TOKEN=personal-tok\n' | cendarum import --env dev --scope personal

export adds a third token, effective — exactly what run would inject, shadows included. import --scope effective does not exist: a write targets one stream. The details live in import and export.

A shadowed key renders as grouped rows: your personal (winning) row first, badged personal — overrides shared, with the shared twin adjacent — dimmed and marked overridden. Both stay revealable and editable; which scope wins is never ambiguous, because the badge is the disclosure.

The add and edit drawer carries a scope selector — Shared preselected, personal (only you) beside it. Adding a personal value for a key that already exists in shared is not an error; it is the feature, and the drawer says what will happen: “This key exists in shared — your personal value will override it, for you only.”

A read grant is enough to write your own overlay. The feature exists for exactly the read-only collaborator who needs a personal token. The shared stream still requires a write grant, and no grant at all still means the uniform not-found on every surface — a personal write mutates nothing anyone else ever decrypts.

Your overlay is a second encrypted blob, per member, with its own version counter — same optimistic concurrency as the shared payload, same client-side three-way merge, same refusal to pick a winner silently. Conflicts park per scope: a shared conflict and a personal conflict on the same environment are resolved separately.

There is no delete. Clearing your overlay is a write of an empty overlay — versions only ever grow.

That monotonicity powers an alarm. Once your client has seen your overlay exist, it remembers that durably. A server that later claims you have none is not believed: the CLI fails closed (cendarum: error: personal_missing) and the app raises a stop-and-verify alarm rather than silently falling back to the shared values — which is exactly the substitution that omission would otherwise buy. A blob only one person ever reads has no honest second reader to catch a suppression, so your own client carries the burden of proof.

Event Your personal overrides
Password change Survive — they are rooted in your key pair, not your password
Personal recovery (Recovery Key) Survive — the key pair is unchanged
Admin-assisted recovery Destroyed — a new key pair is minted; see below
Environment key rotation Survive — deliberately not bound to the environment’s key epoch
Your grant is revoked Retained server-side, unreachable until you are re-granted

Admin-assisted recovery mints a new key pair. Your overlays were encrypted under a key derived from the old one, no admin ever held it — that was the point — and the server deletes the now-unreadable blobs when recovery completes, rather than leaving them to fail forever as apparent tampering. Gone by construction, not by policy. Rotation surviving cuts the other way: after a teammate rotates a shared credential, re-read the shadow warning.

Writing a personal override records personal.write, account-scoped with empty metadata — which means it does not appear in the project audit read today, the same account-level gap as password changes and recoveries. Personal reads are not recorded at all, by design: the only possible reader is the owner, and a project-admin-readable trail of a colleague’s personal-override activity is a surveillance surface this feature has no reason to create.

Both halves are chosen trade-offs, stated rather than discovered: this feature’s one mutation currently has zero observability in the product.