Documentation
¶
Overview ¶
Safer agent defaults. The MCP server (mcp.go) historically exposed EVERY secret to a connected AI agent, with the per-secret guards (--no-print, --require-approval, --require-grant) all opt-in. This flips the model to deny-by-default under --strict: only secrets explicitly marked agent-exposed (`arca agent allow NAME`) are visible or usable to an agent.
Rollout is warn-then-flip: today --strict is opt-in and the default still exposes everything but prints a loud notice (and `doctor` flags it). A future major makes --strict the default.
Shell completion helpers. Cobra already provides the `completion` command that emits bash/zsh/fish/powershell scripts; these functions add *dynamic* completion so that, with that script sourced, `arca get <TAB>` offers the actual secret names and `arca ls --tag <TAB>` the tags in the store. Completion runs the binary, so it reads the live store on every TAB.
`arca doctor`: a read-only, secure-by-default health check. It turns the security tribal knowledge ("don't commit your identity key", "who can decrypt this?", "is a master key on too many hosts?", "is the MCP server wide open to agents?") into one command every user can run. Findings are ranked by severity, each with a one-line why and the exact remediation; `--json` for CI (non-zero exit on any HIGH), `--fix` applies only the unambiguously safe repairs.
Audit escrow (SEC-14, Option B): every sync replicates the local audit log off-machine as append-only, age-encrypted segments — audit/<machine-id>/<seq>.age.
The local SQLite log remains the operational, fail-closed witness; escrow adds an off-machine copy a local tamperer can't retract. Each segment carries full rows (chain hashes, signatures, store generations) plus the chain coordinates of its tail — which are exactly an anchor token, so `log --verify --remote` is CheckAnchor against a witness this machine cannot quietly rewrite. Escrow is best-effort by design: a failure warns and never breaks the sync (let alone an access).
Exposure / blast-radius visibility: `who-can-read` and `exposure` make "who can decrypt this, and how sensitive is it" something an operator can SEE, rather than discover during an incident.
Under today's single-store model every secret is encrypted to the same recipient set, so per-secret readership equals the store's recipients. The commands still frame it per-secret so the mental model generalizes if per-tier stores ever land.
JSON output for the read/inspect commands (ls, show, log, stale). Agents and scripts get a stable, parseable shape via --json; the human-facing tabwriter output stays the default.
Command arca is an age-encrypted secret store with cleartext metadata and a local audit log, designed to sit safely in front of AI agents.
The CLI is intentionally split into three "access shapes" with different trust levels:
- get / env — reveal a value to stdout (blocked for --no-print secrets);
- inject — resolve arca://NAME references in a template to stdout (also blocked for --no-print secrets);
- exec — inject values into a subprocess's environment, so a command can *use* a secret while the value never appears on arca's stdout or in an agent's context. This is the sanctioned path for --no-print secrets.
Every access is written to the audit log with the calling AI agent's name/version/session (auto-detected) plus an explicit $ARCA_ACTOR, so `arca log` can answer who touched what.
MCP server: exposes arca to AI agents as audited, policy-respecting tools over stdio.
The design goal is "use without revealing": an agent runs commands with secrets injected (run_with_secrets) or inspects metadata (list/show) without the raw value ever entering the model's context. read_secret is the explicit, policy-gated, audited escape hatch for when a value genuinely must be returned. Every tool honours --no-print, --require-approval, and the fail-closed audit, just like the CLI.
NOTE: handlers must never write to stdout (that's the JSON-RPC channel) — they only return results. All output goes through the returned CallToolResult.
Control-plane anchoring (T11/T12). arca's strongest per-secret controls — --require-approval, --require-grant, --no-print, the kill switch, deny-by-default MCP exposure — are all enforced on the paths that *release* a value, and `approve()` anchors the strongest of them to an interactive confirmation on the controlling terminal (SEC-06).
The commands that *change those rules* had no such anchor. An agent refused by --require-grant could run `arca grant NAME --ttl 52w --uses 0` and retry; an agent hidden by `mcp --strict` could run `arca agent allow NAME`; and widest of all, `arca recipients add <own key>` + `arca reencrypt` re-wraps every value to a key the operator does not hold — permanently, on every machine the store reaches, without ever entering an access path, so no per-secret policy applies and no canary trips. `handle create` is the widest of the self-serve paths, because run_with_handle skips the whole gate() ladder rather than satisfying one rung of it; it already refused a *detected* agent, which is the advisory half, and now carries the terminal anchor as well.
So the disclosure path was anchored and the control plane was not, which meant every control on the disclosure path could be edited away by the party it governs. This file closes that.
Deliberately NOT anchored: the commands that only ever *restrict* — `agent deny`, `disable`, `recipients rm`, `handle revoke`. The rule is **anchor the loosening direction only; tightening must always work headless.** arca's existing convention is that the environment may refuse but never grant (ARCA_APPROVAL=deny is honored, =allow is not), a caller that can only tighten its own constraints is not an escalation path, and `recipients rm` is emergency revocation — putting a prompt on the incident-response path costs speed exactly when speed is what matters.
Multi-recipient / team support. A store encrypts every value to one or more age recipients (see Store.Recipients); `set`/`rotate`/`import` already wrap to all of them. These commands manage that recipient set and re-wrap existing secrets when it changes — e.g. to add or remove a teammate's key. Changing the set does NOT touch existing ciphertext until `reencrypt` runs, so the workflow is: recipients add/rm → reencrypt.
Per-store state directories (finding R5, design D4).
`stateDir()` is `$XDG_STATE_HOME/arca` for every store on the machine, so before this change `store.gen`, `sync.json`, `sync-state.json`, `grants.json`, `handles.json`, `canaries.json`, `escrow-state.json`, the audit DB and the session signing keys were shared by every store a machine had. Two stores — the documented personal/work split, which is one `ARCA_STORE` away — clobbered each other two ways: a `sync` run against store B reconciled B against store A's backend and replaced B's contents, and B's legitimately lower generation tripped the SEC-14 rollback warning against A's high-water mark. Both are reproduced by tests in this package.
Per-store files now live in `stateDir()/stores/<key>`, keyed to the store they belong to. `machine-id` deliberately stays flat in `stateDir()`: it identifies the MACHINE to escrow, not the store, and keying it per-store would silently fork this machine's escrow identity into one lineage per store.
arca sync — replicate the store through an untrusted network backend (S3-compatible), replacing "keep the store file in a git repo" as the only multi-machine story.
The pushed envelope is the whole store file wrapped in ONE MORE age layer to the same recipients, so the backend sees no secret names, tags, or policy — only bytes. The local file remains the source of truth for every read/exec; sync never sits in an access path. Concurrency is compare-and-swap: lost updates are impossible, conflicts are reported with both sides' generations and never auto-merged.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
atomicfile
Package atomicfile writes a file so that a concurrent reader never sees a partial document, the destination always lands at exactly the mode asked for, and a power loss immediately after a successful write cannot resurrect the old contents.
|
Package atomicfile writes a file so that a concurrent reader never sees a partial document, the destination always lands at exactly the mode asked for, and a power loss immediately after a successful write cannot resurrect the old contents. |
|
audit
Package audit is arca's access log: an append-only history of every secret access, backed by a local SQLite database.
|
Package audit is arca's access log: an append-only history of every secret access, backed by a local SQLite database. |
|
crypto
Package crypto is arca's thin encryption layer over filippo.io/age.
|
Package crypto is arca's thin encryption layer over filippo.io/age. |
|
remote
Package remote replicates opaque store envelopes to an untrusted network backend.
|
Package remote replicates opaque store envelopes to an untrusted network backend. |
|
store
Package store is arca's on-disk secret store: a single JSON document holding cleartext metadata plus per-value age ciphertext.
|
Package store is arca's on-disk secret store: a single JSON document holding cleartext metadata plus per-value age ciphertext. |