v2.61.0

Credential vault

A user-owned place for API keys and tokens that agents can use but never read. C3 decodes secrets at the subprocess boundary, so the value reaches your command and never reaches the model's context.

v2.58.0 vault v2.59.0 Hub tab v2.61.0 search + drawer OS keyring Injection-first Write-only wire

Overview

The usual way an agent gets a secret is the worst way: you paste it into the chat. From that moment the value lives in the conversation transcript β€” stored, indexed, searchable, and replayed into every subsequent request. Deleting the message does not un-index it.

The C3 vault removes that step. You store the secret once; the agent refers to it by name. When a command actually needs the value, C3 decodes it into the subprocess environment and nowhere else.

vault (keyring) β†’ c3_shell subprocess env β†’ your command
βœ— model context βœ— transcript βœ— HTTP response βœ— .c3/config.json

Write-only wire. No HTTP route in C3 β€” project server or Hub β€” ever returns a stored value. Entries are serialized through an explicit allowlist that structurally cannot emit one; rows carry a length and, on demand, a fingerprint. A canary test sweeps every credentials endpoint on every CI run and fails if a planted secret appears in any response body.

What this is not

  • Not a team secret manager. It is local, single-user, and backed by your OS keyring. It does not sync, rotate, or issue short-lived credentials. If you need Vault or AWS Secrets Manager, use those β€” and store their bootstrap token here.
  • Not a substitute for scoping your tokens. The vault controls where a value travels inside C3. It cannot reduce what the token itself is allowed to do.
  • Not obfuscation dressed as encryption. Values sit in the OS keyring, which is exactly as strong as your OS login. Anything C3 could decode on its own would be reversible by anyone holding the code.

Quick start

1

Store it

The value is read from a hidden prompt β€” it never appears in your shell history.

c3 creds set NPM_TOKEN
2

Confirm it landed

Names and metadata only; the value is not printed.

c3 creds list
3

Use it

The agent names the secret; C3 injects it into the subprocess.

c3_shell(cmd='npm publish',
         env_creds='NPM_TOKEN')

Anything shared across every project β€” a personal GitHub PAT, an OpenAI key β€” belongs in the global vault:

c3 creds set OPENAI_API_KEY --global

Bulk-import an existing .env file (each line becomes an entry of type env):

c3 creds import .env
c3 creds import .env --global

Importing does not delete the source file. Once the values are in the vault, remove the .env (or confirm it is gitignored) β€” otherwise you have simply added a second copy.


Where values live

OS keyring The default home for every value, under service c3-creds. Backed by Windows Credential Manager, macOS Keychain, or Linux Secret Service.
.c3/secrets.enc Values larger than 1024 bytes (PEM keys, service-account JSON, .env blobs) β€” some keyring backends choke on long strings. Fernet-encrypted; the random master key is itself a keyring entry, so the file alone is useless.
.c3/config.json The non-secret registry: name, scope, type, description, env var, flags, byte length, storage backend, created/updated timestamps. Never the value.
.c3/cred_state.json Usage counters β€” last used, use count. Written when a value is actually resolved for injection.

C3 adds secrets.enc and cred_state.json to .c3/.gitignore when the first credential is stored, so they cannot be committed even in projects that deliberately track .c3/.

Fingerprints

A fingerprint is the first 8 hex characters of sha256(value), computed live on request and never persisted. It answers "is the thing in the vault the thing I think it is?" β€” compare two machines, or confirm a rotation actually replaced the value β€” without revealing anything about it.


Scopes & overriding

ScopeLives inVisible to
global~/.c3Every C3 project on this machine
project<project>/.c3That project only β€” and it overrides a same-named global entry there

Overriding is the mechanism for per-environment values. Keep a personal GITHUB_TOKEN in the global vault, then give one client project its own project-scoped GITHUB_TOKEN; work in that project resolves to the local one, everything else still gets the global.

The Hub shows the relationship from both directions: a project entry that wins over a global carries overrides global, and the global entry it beats carries overridden Γ—N with the project names on hover.

Resolution is realm-atomic. A name registered in the project realm resolves in the project realm or not at all β€” it never silently falls through to the global vault. Without this, a hostile repository could commit a .c3/config.json registering the name of one of your global secrets and have C3 hand it over. There is a test for exactly this.


How agents use them

Three injection paths, all of which decode at the subprocess boundary:

1. Explicit env vars

c3_shell(cmd='npm publish', env_creds='NPM_TOKEN')
c3_shell(cmd='terraform apply', env_creds='AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY')

Each named entry is exported into the child process under its env_var (defaulting to the entry name).

2. Inline templates

c3_shell(cmd='curl -H "Authorization: Bearer {{cred:API_KEY}}" https://api.example.com')

{{cred:NAME}} is expanded server-side, after the model has produced the command string. The model writes the placeholder; it never writes the secret.

3. Auto-inject

An entry flagged inject is exported into every c3_shell run in its scope without being named. Convenient for a token every build step needs β€” and correspondingly broader, since any command that runs there can read it from the environment.

Echo redaction

Any value decoded during a session is tracked process-locally, and output is scrubbed on the way back: a command that echoes the secret returns [cred:NAME] instead. This is a safety net, not a guarantee β€” a transformed value (base64-encoded, split across lines, hashed) will not match and will pass through.

Cross-project shells run with credentials disabled. c3_project(action='shell') cannot inject another project's vault. Otherwise "operate on project B" would become a credential-exfiltration primitive.


Exposure flags

Two per-entry switches, both off by default, that control how far a value is allowed to travel. The Hub's settings drawer groups them under Exposure and spells out the blast radius of each.

FlagEffectCost
inject Auto-export into every c3_shell subprocess in this scope Every command that runs there β€” including third-party tooling you did not write β€” can read it from the environment.
agent_readable Permits c3_credentials(action='reveal') β€” the only value-returning action The plaintext enters the model's context and the conversation transcript, which are stored and searchable. Injection-only use does not need this flag.

agent_readable is user-only, by design. An agent may set it when creating an entry, but it can never raise the flag on an entry that already exists. Otherwise the first thing a confused β€” or prompt-injected β€” agent would do is grant itself read access to everything. Raising it is a deliberate act you perform in the Hub UI or the CLI, and in the Hub it requires typing the credential's name.

Reach for agent_readable only when the agent genuinely must reason about the value itself β€” pasting a key into a config file it is authoring, say. For "run this command with my token", injection is both sufficient and strictly safer.


Hub UI

Run c3 hub and open the Credentials tab in the top bar. It manages the global vault and every registered project from one place. The same manager appears in each project's drill panel and in the per-project dashboard, so there is one interface to learn.

Two sub-tabs

Global vault The shared ~/.c3 store. Everything here is visible in every C3 project.
Projects Every registered project, with its project-scoped entry count and β€” before you expand anything β€” badges for how many entries are agent-readable and how many override a global. Uninitialized projects are shown dimmed rather than hidden, so a missing .c3/ is visible instead of mysterious.

One project expands at a time. The Projects list is a single-open accordion. Before v2.61.0 every expanded project mounted its own manager with its own fetch loop, so ten open projects meant ten independent views drifting out of sync with each other after any change.

The settings drawer

Click any credential β€” or press ↡ on a focused row β€” to open its settings drawer. This is the only surface that edits an existing entry or replaces a value.

SectionContents
GeneralDescription, type, and the env var used at injection. The name is immutable β€” there is no rename; create a new entry and delete the old one.
ExposureThe inject and agent_readable switches, each with its blast radius written out. Turning one on requires confirmation; turning it off is immediate.
SecretCheck resolution probes whether the stored value still decodes and returns a fingerprint. Replace secret… opens a write-only field that starts empty, is never prefilled, and is cleared the moment the request settles.
Usage & relationshipsCreated / updated / last used / use count / storage backend, plus which projects override this name or are overridden by it.
Danger zoneDelete β€” visually separated from everything else.

Context menu

Right-click any credential row for the full action set. The same menu opens from the β‹― button on the row, and from Shift+F10 or the Menu key when the row is focused. Arrow keys navigate it; Esc closes it.

ItemNotes
Open settings…The drawer
Check resolutionResolvability + fingerprint
Replace secret…Opens the drawer with the replace field ready
Enable / disable auto-injectConfirmation required to enable
Allow / revoke agent readEnabling requires typing the credential name
Copy name Β· env var Β· fingerprintFingerprint is available only after a check. There is no copy-value item β€” the value is not in the browser to copy.
Open project drillJumps to that project's panel (project-scoped entries only)
Delete credential…Requires typing the credential name

Destructive actions state their blast radius. Deleting shows where the value is stored, that anything resolving the name will start failing, and β€” for a global entry β€” how many projects override it locally and are therefore unaffected. The confirm button stays disabled until you type the credential's name.

Importing a .env

Import .env in the manager toolbar takes pasted KEY=VALUE lines. Comments and export prefixes are tolerated; each line becomes an entry of type env. Existing names are skipped rather than overwritten, and the response reports created and skipped counts by name.



CLI commands

CommandDescription
c3 creds set NAMECreate or update. The value is read from a hidden getpass prompt by default; --stdin accepts a piped or multiline value, and --value exists but puts the secret in your shell history. Metadata flags: --type {token,env,multiline}, --env-var, --desc, --inject, --agent-readable. --global targets the shared vault.
c3 creds listMerged view β€” project entries plus the globals visible here. Names and metadata only.
c3 creds get NAMEEntry metadata, masked. --show prints the value to your terminal (not the agent's).
c3 creds rm NAMEDelete the value and the registry entry. --global to target the shared vault.
c3 creds import FILEImport KEY=VALUE lines. Names already registered in the target scope are skipped unless you pass --overwrite.

c3 creds get --show is the escape hatch for when you need to see a value β€” reading it into your own clipboard, not the agent's context. It is a local terminal command and has nothing to do with the agent_readable flag.


Tool actions

What the agent can call through c3_credentials. Read actions are safe in plan mode.

ActionGroupDescription
listReadMerged registry β€” names, scope, type, flags, usage. Never values.
describeReadMetadata, storage backend, and a live fingerprint.
checkReadDoes the stored value still decode?
setWriteCreate or replace an entry. agent_readable may be set at creation only.
deleteWriteRemove the value and the registry entry.
revealGatedThe only value-returning action β€” refused unless you enabled agent_readable on that entry.

The full parameter reference lives on the tool card.


Audit trail

Every mutation and every reveal is logged by name β€” never by value.

Project scope The target project's .c3/activity_log.jsonl (event cred_action) and its edit ledger, as cred://<NAME>.
Global scope ~/.c3/activity_log.jsonl, so the shared vault keeps its own trail independent of whichever project you were in.
Origin Hub-initiated changes are tagged via: "hub", which distinguishes them from CLI and agent-initiated ones.

To review what happened to one credential:

c3_edits(action='history', file='cred://NPM_TOKEN')

The vault is excluded from the Oracle Discovery API. External LLMs reaching C3 over MCP/HTTP cannot see credential tools at all β€” not the metadata, not the names. The exclusion is a hard exclusion, not a permission check.


Troubleshooting

SymptomCause & fix
The 'keyring' package is required pip install keyring. It is a declared dependency, but an in-place upgrade can leave it missing.
Check reports βœ— unresolvable The registry entry exists but the value is gone β€” usually a keyring reset, a new OS user profile, or a deleted .c3/secrets.enc. Re-store the value with c3 creds set NAME.
Hub shows not initialized That project has no .c3/ directory. Run c3 init there. The Hub deliberately refuses to create .c3/ directories remotely (it returns 409 needs_init) rather than scattering them across your disk.
Injected variable is empty in the command Check the entry's env_var β€” it defaults to the credential name, but if it was set to something else, that is the variable your command must read.
reveal refused Working as designed: agent_readable is off. If the agent genuinely needs the value, turn it on yourself in the Hub drawer or with c3 creds set NAME --agent-readable. If it only needs to run something, use env_creds instead.
Global entry ignored in one project That project has a project-scoped entry of the same name overriding it. Search shadow:true to see every override in play.

The vault is deliberately small: services/credential_store.py holds the storage layer, and the write-only wire is enforced by an allowlist serializer plus an endpoint-sweep canary test. If you find a route that returns a value, that is a security bug β€” please report it.