arca is no longer maintained. The repository is archived and there will be no further releases or security fixes.
← All docs

Per-secret policies & agent controls

← README · related: Commands · MCP · Threat model

arca's distinguishing feature is a set of per-secret controls for putting a secret safely in front of an AI agent — constraining not just whether an agent can see a value, but how, how often, and for what, and catching misuse when it happens. Each has an honest boundary, documented in SECURITY.md.

--no-print (exec-only)

Refuses get / env / inject / edit for a secret, so its value can never be printed to stdout or opened in an editor — only exec (or MCP run_with_secrets) can inject it into a subprocess. It blocks disclosure, not use. To change a --no-print secret's value without revealing the old one, use rotate (which prompts for a new value) rather than edit.

arca set PROD_DB_PASSWORD --no-print
arca get PROD_DB_PASSWORD          # refused
arca exec --only PROD_DB_PASSWORD -- psql   # allowed (value in the child's env)

agent allow (deny-by-default agent exposure)

A per-secret opt-in (agent_exposed) that controls whether an agent sees a secret at all. When the MCP server runs with --strict (or ARCA_AGENT_STRICT=1), only allow-listed secrets are visible or usable to an agent; everything else is hidden from list_secrets and refused by show/read/run_with_secrets. This is a coarse reachability gate that sits in front of the finer policies above (--no-print, --require-approval, grants): a secret an agent can't reach is never subject to them in the first place.

arca agent allow DEPLOY_TOKEN      # opt this secret in
arca agent ls                      # review the allow-list
arca mcp --strict                  # agents now see only allow-listed secrets
arca agent deny DEPLOY_TOKEN       # revoke

Without --strict every secret is exposed (the server warns loudly on startup); arca doctor flags a store whose exposure isn't scoped. See AI agents & the MCP server.

--require-approval (human gate)

A human must confirm each release on the controlling terminal — every time. There is no environment bypass: ARCA_APPROVAL=allow is not honored (only ARCA_APPROVAL=deny, which can only refuse). An AI agent has no terminal, so it simply can't satisfy the gate — arca relies on the one thing an agent lacks (a TTY) rather than on env-var-based agent detection, which an agent could spoof.

arca set ROOT_SIGNING_KEY --require-approval

For "a human authorizes once, then a script or agent runs unattended," don't use --require-approval — use a grant or a handle: the operator sets it up interactively (scoped to a command, a number of uses, and a window) and the agent then scripts against it.

Output redaction

When a command run under exec (or MCP run_with_secrets) prints an injected secret, arca replaces the value with «arca:NAME» in the captured output before it reaches whoever is reading (an agent's context, a log), and records the catch in the audit log. It's on by default for captured/piped output and steps aside for an interactive terminal; --redact on|off forces it, --reveal shows a partial mask of long values.

$ arca exec --only PASSWORD -- sh -c 'echo connecting with $PASSWORD'
connecting with «arca:PASSWORD»          # the value never reaches the agent's context

Boundary: it matches the literal value, so a command that transforms the secret (encodes, splits, hashes it) before printing can still emit it. Defense in depth, not a guarantee.

Canary secrets (leak detection)

Plant a realistic decoy that should never legitimately be used. Any use trips a loud stderr alert and a distinct, signed op=canary audit event — turning the audit log into active leak detection rather than passive forensics.

arca canary AWS_PROD_KEY --template aws     # stripe | github | aws | slack | generic
# ... later, if anything uses it ...
#   ⚠  CANARY TRIPPED: "AWS_PROD_KEY" was accessed by malicious-agent (session …)
arca canary --list                          # which canaries exist, and which were tripped

The "this is a decoy" designation is kept in a local registry ($XDG_STATE_HOME/arca/stores/<store-key>/canaries.json, per store — see Configuration), never in the git-synced store — so someone who obtains the store file, the exact exfiltration a canary exists to catch, can't tell the decoys from the real secrets. The decoy's value is an ordinary-looking store entry. (Canaries planted before 0.6.2 kept the flag in the store; those are still honored, and re-running arca canary NAME moves the designation into the private registry.)

Boundary: a canary is a tripwire, not a barrier. Because the registry is local, the designation doesn't sync across machines — arm the decoy on each machine where arca runs. Its value is catching the common case of an agent (or attacker) that uses what it can reach through arca.

Just-in-time grants (--require-grant)

Bind a secret to what an agent does. A --require-grant secret is unusable until you issue a grant scoped to a command pattern, a use count, and a time window; then it auto-expires. Use counts come from the tamper-evident audit log, so they can't be rolled back.

arca set DEPLOY_KEY --require-grant                       # now unusable until granted
arca grant DEPLOY_KEY --command 'terraform *' --uses 3 --ttl 15m [--agent claude-code]
arca exec --only DEPLOY_KEY -- terraform apply           # allowed (use 1 of 3)
arca exec --only DEPLOY_KEY -- sh -c 'curl …'            # denied: command doesn't match
arca grants                                              # secret, command, uses, expiry
arca revoke DEPLOY_KEY                                   # remove the grant

Boundary: the command match is argv-based — a guardrail expressing intent, not a sandbox (an agent controls argv and can rename a binary or wrap it in sh -c). The uses and expiry checks are firm. The --agent restriction is advisory: it sniffs environment markers any process can set (CLAUDECODE=1 satisfies --agent claude-code). Treat it as a display/audit hint, not a containment boundary. Every grant, revoke, and use is recorded.

Rate limiting (--rate)

Cap how often a secret may be used (read/exec/env/inject) in a rolling window. Once the cap is reached the access is refused and the throttle is recorded (op=ratelimit) — stopping a runaway agent hammering a secret and surfacing the burst.

arca set SIGNING_KEY --rate 5/1h        # at most 5 uses per rolling hour
arca get SIGNING_KEY                     # ... the 6th within the hour is refused and recorded
arca set SIGNING_KEY --rate ""           # clear the limit

Boundary: a throttle, not a quota — a patient caller can stay under the cap by spreading use out.

Expiry (--ttl, --expires-at)

Give a secret a deadline. Past it, every access path refuses it, before any approval prompt or decryption. --ttl takes a relative duration (30m, 12h, 7d, 2w); --expires-at takes an absolute RFC3339 timestamp or YYYY-MM-DD. They are mutually exclusive.

arca set API_TOKEN --ttl 7d          # expires a week from now
arca set API_TOKEN --expires-at 2027-01-01
arca set API_TOKEN --ttl 1h          # shortening: no prompt
arca set API_TOKEN --ttl 90d         # extending: prompts on the terminal
arca set API_TOKEN --ttl ''          # clearing: prompts, like `--rate ''`
arca set API_TOKEN --desc 'note'     # no expiry flag: the deadline is left alone

Expiry is a policy, so extending or clearing one is a downgrade and needs an operator at a terminal, the same anchor --no-print, --require-approval, --require-grant, --rate and --canary carry. The ordering behind that: no expiry at all is the least protected state a secret can be in, and an earlier deadline is tighter than a later one. Shortening a deadline, or setting one on a secret that had none, only tightens and stays headless.

Both spellings resolve to an instant before being compared, so "am I extending this?" is one question regardless of which flag you reached for. rotate carries the same anchor.

Boundary: expiry refuses future use through arca. It does not reach a copy a caller already took, and it is not revocation at the issuer — rotate the underlying credential for that.

Capability handles

Over MCP, hand an agent an opaque hdl_… token instead of a secret name. It can use the secret via run_with_handle — inject it into a command — without ever learning the secret's name or value, and without being able to enumerate the store.

# operator mints a command-scoped, expiring handle (injects the value as $PGPASSWORD):
arca handle create DB_PASSWORD --as PGPASSWORD --command 'psql *' --ttl 1h
#   hdl_3fd698a47ed0c05e21c41d30
arca handle ls                              # operator view: which secret each unlocks
arca handle revoke hdl_…                     # revoke
# the agent, over MCP, runs a command via the handle — never seeing the name or value:
#   run_with_handle(handle="hdl_…", command="psql", args=["-c","select 1"])

Boundary: a handle reduces discovery, not misuse — a scoped, expiring, revocable, audited bearer token; the command under it still has the value in its environment.

How they compose

Policies are enforced at a single gate in this order: canary trips first (so it fires even on an access another policy then refuses), then hard expiry, then grant, then rate limit, then approval. A secret can carry several at once — e.g. a canary that's also rate-limited will alert on every attempt, including the throttled ones.