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.