Architecture
This document describes arca's design: the actors that interact with it, the components they touch, and the actions that flow between them. It complements the trust model and the threat model.
Purpose
arca is a single-user command-line secrets manager. It keeps secrets age-encrypted at rest and records every access in a tamper-evident audit log. Its distinguishing goal is to sit safely in front of an AI agent: an agent can be allowed to use a secret (run a command with it) without the secret value entering the model's context, and an agent cannot quietly weaken the controls that record what it did.
Actors
| Actor | Description | How it interacts |
|---|---|---|
| Operator | The human running arca on their own machine. | Full CLI; can set/rotate/read secrets, approve gated reads, and relax audit/approval policy. |
| AI agent | A coding agent (Claude Code, Cursor, a generic AI_AGENT) invoking arca on the operator's behalf. |
Same CLI and the MCP server, but detected as an agent and held to stricter, non-overridable policy. |
| Subprocess | A command launched by arca exec / MCP run_with_secrets. |
Receives selected secrets via its environment; never via the command line. |
| Release pipeline | GitHub Actions. | Builds, tests, signs, and publishes releases and the website; never reads operator secrets. |
The operator and the agent are the two trust tiers. arca raises the bar for a cooperating agent; it does not defend against a hostile local user, who could bypass arca entirely (see the trust model).
Components
| Component | Source | Responsibility |
|---|---|---|
| CLI | main.go, manage.go, recipients.go, generate.go, completion.go |
Command tree (cobra), identity detection, policy enforcement, value I/O over TTY/stdin/stdout. |
| MCP server | mcp command |
Exposes list_secrets, show_secret, read_secret, run_with_secrets, audit_log over stdio JSON-RPC for agents. |
| Store | internal/store |
The age-encrypted JSON secret store: load (with size/version/validity checks and migrations), atomic save, per-secret metadata and expiry. The monotonic generation counter advances only after a save has landed, so a failed write cannot make the store look rolled back (THREAT-MODEL.md, T9). |
| Atomic writes | internal/atomicfile |
The one way arca replaces a file of local state: unique temp file in the destination's own directory → chmod → write → fsync → rename → fsync of the parent directory. Every state writer goes through it (the store, the pulled store, sync config and cursor, grants, handles, canaries, the escrow cursor, the rollback high-water mark), so none of them can drop a step the others keep. |
| Crypto | internal/crypto |
age (X25519) encrypt/decrypt of secret values; recipient management. |
| Audit | internal/audit |
Append-only SQLite log (WAL) of access metadata — never values. |
| Lock | lock.go |
Cross-process O_EXCL lockfile guarding store mutations. |
| Sync | sync.go, internal/remote |
Replicates age-envelope store snapshots through an untrusted S3-compatible backend with CAS; never in an access path (see SYNC.md). |
Data at rest
| File | Default location | Contents | Mode |
|---|---|---|---|
| Store | ARCA_STORE |
age-encrypted secret values + metadata (name, tags, timestamps, policy, expiry). | 0600 |
| Audit DB | ARCA_AUDIT |
Access events: op, name, time, actor, agent, session, caller. No values. | 0600 |
| Identity | ARCA_IDENTITY / sops key |
The age private key that decrypts the store. | 0600 |
Primary actions
Write a secret — set / rotate / generate / edit
- The value is read from a TTY (no echo) or piped stdin — never an argv argument.
- The name is validated against
^[A-Za-z_][A-Za-z0-9_]*$. - The store is locked, the value is age-encrypted, the store is written atomically, the lock is released.
- The operation is recorded in the audit log.
Read a secret — get / show / env / inject
- Policy is checked:
--no-printrefuses disclosure;--require-approvalrequires operator approval (an agent cannot self-approve). - The read is recorded before the value is disclosed. If recording fails and auditing is fail-closed (the default, and non-negotiable for an agent), the read aborts and nothing is disclosed.
- The decrypted value is written to stdout (or substituted into an
injecttemplate).
Use a secret without disclosing it — exec / MCP run_with_secrets
- Selected secrets are decrypted and placed in the subprocess environment.
- The subprocess runs; arca returns the subprocess's output, never the secret value itself.
- The use is audited.
--no-printsecrets are usable here but still never returned by arca.
Inspect history — log / MCP audit_log
- Reads the audit DB and prints access metadata. An agent cannot suppress its own read record (
get --no-logis honored only for a non-agent caller at a controlling terminal).
Agent-aware policy (the core invariant)
When arca detects that the caller is an AI agent, three operator escape hatches are disabled and cannot be re-enabled by the agent:
- It cannot self-approve a
--require-approvalsecret: approval needs an interactive terminal, which an agent doesn't have (there is noARCA_APPROVAL=allowbypass). - It cannot disable fail-closed auditing (
ARCA_STRICT_AUDIT=0). - It cannot suppress its own read record (
get --no-log).
These overrides are honored only for a non-agent (operator) caller. This is the property that lets an operator hand arca to an agent and still trust the audit log and the approval gates.
Trust boundaries
operator ──► arca CLI ──► store (encrypted) ──► disk (0600)
│ └──► audit (metadata) ──► disk (0600)
│
AI agent ─► arca CLI / MCP ─(agent-aware policy)─► same store & audit
│
└──► exec / run_with_secrets ──► subprocess (env, not argv)
The boundary that matters is between the agent and the policy/audit controls: everything an agent does crosses it, and the agent cannot move the boundary. See THREAT-MODEL.md for the assessed threats and how each is addressed.