arca

command module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 1, 2026 License: MIT Imports: 29 Imported by: 0

README

arca logo

arca

arca (Latin): a strongbox or chest for keeping valuables under lock.

ci codeql OpenSSF Scorecard OpenSSF Baseline Go Report Card MIT

A small, file-based secrets manager built on age — designed to sit safely in front of AI agents. Secrets are encrypted per value with cleartext metadata in a single JSON store; every access is recorded in a local, fail-closed audit log attributed to the calling agent. No daemon, no account, no proprietary backend.


Contents


Why arca

  • Use secrets without revealing them. A command can use a secret (exec) or a config can reference one (inject) while the value never reaches stdout or an agent's context.
  • Every access is accountable. The audit log records who/what/when — including the auto-detected AI agent, version, and session — and is fail-closed by default.
  • Per-secret policy. Mark a secret exec-only (--no-print) or require human approval (--require-approval).
  • Git-friendly. The store is plain JSON: it diffs, merges, and lives happily in a dotfiles repo as the source of truth, with a free created/modified history from git log.
  • Inspectable & dependency-light. Stdlib + age + cobra; pure-Go SQLite for the audit log. No cgo, reproducible builds, signed releases.

Features

Area What you get
Encryption Per-value age (X25519) ciphertext, ASCII-armored; reuses your existing $SOPS_AGE_KEY_FILE
Store Single JSON doc; cleartext metadata (tags, description, timestamps, policy), encrypted values
Metadata & query ls/show list and filter without decrypting; --reads joins usage from the audit log
Rotation rotate (keeps created_at), --rotate-after dates, stale to find overdue/missing policies
Expiry (TTL) --ttl 30m|12h|7d|2w or --expires-at; expired secrets are refused on every access path and surfaced by stale
Audit Local SQLite log of every access; agent name/version/session attribution; fail-closed by default; hash-chained + per-session signed so tampering is detectable (log --verify)
AI-safety policies --no-print (exec-only), --require-approval (human approval), least-privilege exec --only
Canary secrets Plant realistic decoys (canary --template stripe); any use trips a loud, signed audit alert — leak detection, not just prevention
JIT grants --require-grant secrets are usable only via a grant scoped to a command, a use count, and a time window — bind a secret to what an agent does, not just whether it can see it
References arca://NAME resolved at render time by inject — agents manipulate references, not secrets
Teams Encrypt each value to multiple age recipients; recipients add/rm + reencrypt re-wrap the whole store
JSON output --json on ls/show/log/stale for agents and scripts
Completion completion bash|zsh|fish with dynamic secret-name + tag suggestions
Migration import a dotenv or JSON stream (audited); set NAME < file for blobs; env for shell eval
Supply chain Reproducible builds, SBOM, cosign-signed + SLSA-provenanced releases, govulncheck, CodeQL

Install

Homebrew (macOS / Linux):

brew install arenzana/tap/arca

Scoop (Windows):

scoop bucket add arenzana https://github.com/arenzana/scoop-bucket
scoop install arca

go install:

go install github.com/arenzana/arca@latest

Pre-built binaries for linux/macOS/windows (amd64 + arm64) are attached to each release, cosign-signed with SLSA provenance — see SECURITY.md to verify. Or build from source: git clone … && cd arca && make build.


Quickstart

arca init                                  # reuses $SOPS_AGE_KEY_FILE, or generates an identity
printf '%s' "$TOKEN" | arca set GITHUB_TOKEN --tag github,ci --desc "classic PAT"

arca ls --reads                            # metadata + last-read/count (no decryption)
arca get GITHUB_TOKEN                       # decrypt just this one; records a read

arca exec -- terraform apply                # inject secrets as env (value never hits stdout)
echo 'token = "arca://GITHUB_TOKEN"' | arca inject > config.toml   # resolve references

arca set DB_PASSWORD --no-print             # exec-only: get/env/inject refuse to reveal it
arca set ROOT_KEY --require-approval         # human must approve each release
arca set TMP_TOKEN --ttl 1h                  # ephemeral: refused everywhere after it expires

arca rotate GITHUB_TOKEN --rotate-after 2026-12-01
arca stale                                  # secrets past their rotate-after date, or expired
arca log GITHUB_TOKEN                        # who/what accessed it, and when

arca ls --json | jq '.[].name'              # structured output for agents/scripts

arca recipients add age1teammate...         # share with a teammate's key
arca reencrypt                              # re-wrap every secret to the new recipient set

Importing & migrating

arca takes values on stdin, so anything that can emit secrets pipes straight in. There are three ingest shapes:

Shape Command Use it for
dotenv lines arca import KEY=value streams (.env, sops, printenv)
JSON object arca import --json {"KEY":"value"} — what most secret stores emit
a single value arca set NAME < file one secret, including multi-line blobs (PEM keys, a service-account JSON)

import validates every name ([A-Za-z_][A-Za-z0-9_]*) and skips anything else, and each imported secret is recorded in the audit log like any other write. With --json, string values pass through verbatim (a JSON-escaped multi-line key round-trips), numbers and booleans are stringified, and null/nested values are skipped.

By default import skips a name that already exists (so a re-run never silently clobbers a secret); pass --overwrite to replace them. A few flags shape the load:

Flag Effect
--dry-run Print what would be imported (new vs overwrite vs skip) and write nothing
--overwrite Replace existing secrets instead of skipping them
--prefix P Prepend P to every imported name (e.g. --prefix STRIPE_)
--tag t Attach tags to every imported secret (repeatable or comma-separated)
# dotenv — a plain file, or decrypted from sops
arca import < .env
sops -d ~/.dotfiles/secrets/secrets.env | arca import

# JSON — straight from a cloud secret store, no jq gymnastics
aws secretsmanager get-secret-value --secret-id prod/app --query SecretString --output text \
  | arca import --json
gcloud secrets versions access latest --secret=prod-app | arca import --json
vault kv get -format=json secret/prod/app | jq '.data.data' | arca import --json
op item get prod-app --format json | jq '[.fields[]|select(.value)|{(.label):.value}]|add' \
  | arca import --json

# any KEY=value source via dotenv
pass show prod/env | arca import
printenv | grep '^APP_' | arca import

# preview first, then namespace + tag a load
arca import --json --dry-run < secrets.json
arca import --prefix STRIPE_ --tag billing,prod < stripe.env
arca import --overwrite < refreshed.env          # replace existing (default skips them)

# one secret, multi-line, as a single value (not dotenv)
arca set TLS_KEY < server.key
arca set GCP_SA_JSON < service-account.json

A non-JSON source whose values span lines (a raw PEM, a certificate) doesn't fit dotenv — import it as one named secret with set NAME < file, or wrap it in a JSON object and use --json.


Recipes

Use a secret in a command — never on stdout

arca exec -- terraform apply
arca exec --only AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY -- terraform apply   # least privilege

If the command itself prints an injected secret, arca redacts it from the output before it reaches whoever is reading — replacing the value with «arca:NAME» and recording the catch in the audit log. Redaction is automatic when output is captured (piped to an agent or a log) and steps aside for an interactive terminal; --redact on|off forces it, and --reveal shows a partial mask of long values instead of the name.

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

Plant a canary to catch an agent that grabs everything

arca canary AWS_PROD_KEY --template aws    # a realistic decoy; using it should never happen
# ... later, if anything reads it ...
#   ⚠  CANARY TRIPPED: "AWS_PROD_KEY" was accessed by claude-code (session …)
arca canary --list                         # which canaries exist, and which have been tripped

Grant a secret just-in-time, scoped to a command

arca set DEPLOY_KEY --require-grant                       # now unusable until granted
arca grant DEPLOY_KEY --command 'terraform *' --uses 3 --ttl 15m
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

Render a config from a template (the value only lands in the rendered file):

echo 'database_url = "arca://DATABASE_URL"' > app.toml.tmpl
echo 'api_key      = "arca://STRIPE_KEY"'  >> app.toml.tmpl
arca inject < app.toml.tmpl > app.toml

Generate, then rotate on a schedule:

arca generate DB_PASSWORD --length 40 --no-print      # random value, exec-only
arca rotate   DB_PASSWORD --rotate-after 2026-12-01    # new value + next rotation date
arca stale --within 30                                 # what's due (or expired) in 30 days

Short-lived tokens:

arca generate CI_DEPLOY_TOKEN --ttl 1h                 # refused on every path after an hour

Exec-only and human-approved secrets:

arca set PROD_DB_PASSWORD --no-print                   # get/env/inject refuse to print it
arca set ROOT_SIGNING_KEY --require-approval           # a human confirms each release on the TTY

Share with a teammate (add their age public key, re-wrap the store):

arca recipients add age1teammate...
arca reencrypt

Load into a shell or write a dotenv:

eval "$(arca env)"                                     # export every non-no-print secret
arca env --no-export > .env

Use from an AI agent (MCP):

claude mcp add arca -- arca mcp
# the agent calls run_with_secrets to USE a secret without seeing it, and read_secret
# (policy-gated, audited) only when the value must enter its context.

Audit — who touched what:

arca log                       # recent access across all secrets
arca log PROD_DB_PASSWORD      # one secret, with agent / session / actor
arca ls --reads                # last-read time + count per secret

Keep the store in a dotfiles repo (the JSON diffs cleanly; the audit DB stays local):

export ARCA_STORE=~/.dotfiles/arca/store.json

Script against JSON output:

arca ls --json | jq -r '.[] | select(.expired) | .name'    # expired secrets
arca log --json | jq '.[] | {op, name, agent, time}'

The model

arca is three pieces with deliberately different jobs:

 ┌──────────────────────────┐        ┌───────────────────────────┐
 │ store.json (git-synced)  │        │ audit.db  (local, SQLite) │
 │ cleartext metadata +     │        │ append-only access log    │
 │ per-value age ciphertext │        │ op, name, time, AGENT,    │
 │ (changes only on set/    │        │ version, session, actor   │
 │  rotate/rm)              │        │ (read tracking lives here)│
 └──────────────────────────┘        └───────────────────────────┘
             ▲  encrypt/decrypt with the age identity (≈ your sops key)
  • The store is the source of truth and is meant to be git-synced. Reads never touch it, so it only changes on real mutations — clean history, no churn.
  • The audit log is local and never synced. Keeping read-tracking here is what lets the store stay quiet and lets log/show --reads answer "who accessed this?".
  • Per-value encryption means get/inject/exec decrypt only the one secret asked for, and unchanged secrets keep byte-identical ciphertext (clean diffs).
Access shapes (and what each exposes)
Command Exposes the value to… Blocked for --no-print? Audited
get / env stdout (the caller/agent sees it) yes yes
inject stdout, but only inside a rendered template yes yes (per ref)
exec a subprocess's environment only — never stdout no (this is the sanctioned path) yes (per secret)

Rule of thumb for agents: let a command use a secret with exec, or a config reference one with inject; reach for get only when a human needs the raw value.

Fail-closed auditing

By default, if an access cannot be recorded, the operation is aborted — and for reads it aborts before the value is revealed, so a secret an agent accesses is never disclosed without a trail. Set ARCA_STRICT_AUDIT=0 to fall back to best-effort (swallow audit errors).

Agent attribution

Each event is tagged with the calling AI agent, auto-detected from the environment:

  • Claude Code — name, session (CLAUDE_CODE_SESSION_ID), version (from the exec path)
  • Cursor — via CURSOR_TRACE_ID
  • GenericAI_AGENT=name_version_agent
  • plus an explicit ARCA_ACTOR label you can set yourself.

Command reference

Command Purpose Key flags
init Create the store (reuse or generate an age key) --force
set NAME Add/update a secret (value from TTY or stdin) --tag --desc --rotate-after --ttl --expires-at --meta k=v --no-print --require-approval
generate NAME Create a secret with a random value -l/--length --charset --tag --desc --ttl --no-print --show
get NAME Decrypt and print one secret (records a read) -n (newline), --no-log
rotate NAME Replace value, keep created_at, log a rotation --rotate-after --ttl --expires-at
ls List secrets + metadata (no decryption) --tag, --reads, --json
show NAME Show one secret's metadata (no value) --json
stale Secrets overdue/soon for rotation, or expired/expiring --within N, --missing, --json
rm NAME Remove a secret
edit NAME Edit a secret's value in $EDITOR (re-encrypted)
rename OLD NEW Rename a secret, preserving metadata/history (alias mv) --force
recipients List age recipients; add/rm subcommands manage the set
reencrypt Re-encrypt every secret to the current recipient set
import Bulk-load secrets from stdin (dotenv lines, or a JSON object) --json, --dry-run, --overwrite, --prefix P, --tag t
inject Resolve arca://NAME references on stdin → stdout
exec -- CMD Run CMD with secrets injected as env (audited); injected values are redacted from its output --only a,b, --redact auto|on|off, --reveal
env Emit export … for eval "$(arca env)" --no-export
log [NAME] Access history (agent/session/actor); --verify checks the log's integrity --limit N, --json, --verify
canary [NAME] Plant a decoy secret (any use trips a signed alert), or list canaries and their trips --template, --list, --tag, --desc
grant SECRET Authorize a --require-grant secret for a command, a number of uses, and a window --command, --uses, --ttl, --agent
grants List active grants and their remaining uses
revoke SECRET Remove the active grant for a secret
mcp Run an MCP server exposing arca to AI agents (stdio)
completion SHELL Shell completion script (bash/zsh/fish/powershell)

Values are always read from a TTY (no echo) or piped stdin — never passed as arguments.


Configuration (ops)

All paths are overridable so the store can live in your dotfiles while the audit log stays local.

Variable Purpose Default
ARCA_STORE JSON store path (sync this) ~/.config/arca/store.json
ARCA_AUDIT SQLite audit DB (do not sync) ~/.local/state/arca/audit.db
ARCA_IDENTITY age private key $SOPS_AGE_KEY_FILE, else ~/.config/arca/identity.txt
ARCA_STRICT_AUDIT fail-closed auditing enabled; set 0/false/off/no for best-effort
ARCA_ACTOR explicit actor label in the audit — (agent auto-detected)
ARCA_APPROVAL short-circuit the approval prompt — (allow/deny; else interactive /dev/tty)
XDG_CONFIG_HOME / XDG_STATE_HOME base dirs ~/.config / ~/.local/state

Typical deployment: point ARCA_STORE at a (private) dotfiles repo to version the store; leave the audit DB local and gitignored. The age private key is your single decrypt root — back it up (e.g. to a password manager). On a new machine: restore the key, git clone, done.

make targets: build (reproducible), test, cover, vet, vuln (govulncheck), sbom (CycloneDX), verify.


Storage model

// store.json  (git-syncable; 0600)
{
  "version": 1,
  "recipients": ["age1…"],                  // re-encrypted to on every set
  "secrets": {
    "GITHUB_TOKEN": {
      "value": "-----BEGIN AGE ENCRYPTED FILE-----\n…",  // armored age ciphertext
      "created_at": "…", "updated_at": "…",
      "tags": ["github","ci"], "description": "…",
      "rotate_after": "2026-12-01",
      "no_print": false,                     // exec-only when true
      "require_approval": false,             // requires human approval when true
      "meta": { }                            // open-ended extensibility bag
    }
  }
}

Read tracking (last_read, counts, full history with agent/session) lives in the audit DB, not here — so reads never dirty git.


Designed for AI agents

arca is a file-based secrets broker you can safely put in front of an AI agent:

  • Use without revealing. arca exec -- <cmd> injects secrets into a subprocess's environment — the command uses the value, but it never prints into the agent's context or transcript.
  • References, not values. Put arca://NAME in a config/template and arca inject resolves it at render time, so an agent manipulates references rather than raw secrets.
  • --no-print (exec-only) secrets. get, env, and inject refuse to reveal them — only exec can inject them into a subprocess.
  • --require-approval gates. A human confirms each release on the terminal; an agent can request but cannot self-approve (no terminal ⇒ denied).
  • Attributed audit. Every access is logged with the calling agent, version, and session, plus an explicit ARCA_ACTOR override — arca log answers which agent touched what, and when.
  • Fail-closed auditing. If an access can't be recorded the operation is aborted — and for reads, aborted before disclosing the value.
  • Least privilege. exec --only a,b injects just the secrets a task needs.

MCP server

arca mcp runs a Model Context Protocol server over stdio, so an agent accesses secrets through controlled, audited tools instead of raw shell — the same --no-print / --require-approval / fail-closed-audit policies apply.

Tool What it does
list_secrets Names + metadata (tags, policy, last read) — never values
show_secret Metadata for one secret
run_with_secrets Run a command with named secrets injected as env; returns the command's output, not the values
read_secret Reveal a value (refused for --no-print, requires --require-approval confirmation, audited) — the escape hatch
audit_log Recent access events

The intended flow is use, don't reveal: an agent calls run_with_secrets so a command can use a secret, reserving read_secret for when the value genuinely must enter the model context.

Register it with Claude Code:

claude mcp add arca -- arca mcp

Security & supply chain

Built as security software: reproducible builds (CGO_ENABLED=0, -trimpath, pinned timestamps), cosign-signed checksums, a CycloneDX SBOM, and SLSA build-provenance attestations on every release. CI runs go vet, go test -race (~92% coverage, enforced), go mod verify, govulncheck, CodeQL, OpenSSF Scorecard, dependency review, and SHA-pinned actions under a hardened runner. See SECURITY.md for the disclosure policy and release-verification steps.


Stability

From v1.0, arca follows Semantic Versioning. STABILITY.md defines exactly which surfaces are covered — commands/flags, exit codes, the store schema, arca:// references, the ARCA_* configuration, --json output, and the MCP tools — and what may change (human-readable text, internal packages). Parse --json, not the table output.


License

MIT

Documentation

Overview

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.

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.

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.

Directories

Path Synopsis
internal
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.
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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL