name: arete-infisical
description: This skill should be used for any secrets work in an Areté (aretecp / Areté Intelligence) repo — when you need to read, set, rotate, or inject API keys / env secrets; wire a repo to Infisical; run an app with secrets injected; debug a "secret not found" / 404; set up GitHub Actions or a VPS systemd service to load secrets; or answer "how do we manage secrets here". Areté self-hosts Infisical at secrets.areteintelligence.ai with a folder-per-repo layout. Triggers include "infisical", "where are the secrets", "add an API key", "infisical run", "load secrets in CI", "machine identity", "rotate the key", and seeing a repo with a .infisical.json or an empty .env next to a real app.
Areté Infisical — Secrets Management
Areté manages secrets with a self-hosted Infisical instance. This skill captures the org's actual layout, the CLI workflow, the CI action, the VPS/systemd pattern, and the gotchas — so you can work with secrets correctly without re-deriving the topology every time.
The one-paragraph mental model
There is one Infisical instance, a small number of projects, and inside each project one folder per repo/app. A secret is addressed by (instance → project → folder path → environment) → KEY. Locally you authenticate as yourself (infisical login) and inject secrets into a process with infisical run -- <cmd>; in CI a machine identity does the same; the app code just reads process.env / System.get_env and never knows Infisical exists. Per-repo isolation is the folder path, not a separate project.
Topology (memorize this)
| Thing | Value |
|---|---|
| Instance (self-hosted) | https://secrets.areteintelligence.ai — not Infisical Cloud |
| Internal project | slug arete-internal (UUID 989486e4-b880-40e0-97cd-6abf56abe6bc) |
| External project | slug arete-external |
| Shared / org-wide project | slug arete-shared |
| Environments | dev, staging, prod (slugs, orthogonal to path) |
| Folder path | /<repo-name> — e.g. /areteos, /arilearn-phx, /ari-panelist, /contact-intelligence, /main-website, … |
| GitHub org | aretecp |
- Slug vs UUID: the CLI's
.infisical.jsonand--projectIduse the UUID; the GitHub Action and the Infisical UI use the slug. Slugs are stable, public identifiers — store them as Actions variables, never secrets. - Most app repos live in
arete-internalunder their own folder.arete-sharedholds cross-cutting infra (Terraform, Entra, etc.) that multiple repos import.
CLI workflow (local dev — the common case)
One-time per machine / per repo
brew install infisical/get-cli/infisical # macOS
infisical login # pick the self-hosted instance, your @aretecp.com / @aretepartners account
infisical init # in the repo root → writes .infisical.json (workspaceId only — safe to commit)
infisical login stores the instance + session under ~/.infisical/. infisical init writes a .infisical.json containing only the workspaceId — it does not pin the path or environment, so it is safe to commit and is not a secret.
Inject secrets into a process — the primary verb
# Run any command with the repo's secrets as env vars (env + path select the set):
infisical run --env=dev --path=/<repo> -- npm run dev
infisical run --env=prod --path=/<repo> -- node server.js
infisical run --env=prod --path=/<repo> -- mix phx.server
infisical run injects the secrets only into that child process — nothing is written to disk. This is the canonical way to run an app: the source of truth stays in Infisical, there is no long-lived .env.
Read / write / delete
# List (CAUTION: prints VALUES). Prefer -o json piped to extract names when inspecting:
infisical secrets --env=prod --path=/<repo>
infisical secrets --env=prod --path=/<repo> -o json | jq -r '.secrets[].secretKey' # names only
# Get one:
infisical secrets get ANTHROPIC_API_KEY --env=prod --path=/<repo>
# Set (Infisical overwrites if it exists — set again to update):
infisical secrets set "ANTHROPIC_API_KEY=sk-..." --env=prod --path=/<repo> --silent
infisical secrets set "A=1" "B=2" --env=prod --path=/<repo> # multiple
# Delete:
infisical secrets delete OLD_KEY --env=prod --path=/<repo>
# Folders (discover the per-repo layout):
infisical secrets folders get --env=dev --path=/
When inspecting someone else's secrets, never echo values
Pull names only into a transcript / log. Use -o json | jq -r '.secrets[].secretKey', or check presence with booleans:
infisical run --env=dev --path=/<repo> --silent -- \
node -e 'console.log({ANTHROPIC:!!process.env.ANTHROPIC_API_KEY})'
CI / GitHub Actions
Use the shared composite action aretecp/github-actions/actions/load-infisical-secrets@v1 — do not hand-roll the Infisical action. It defaults domain to the self-hosted instance and exports each secret to $GITHUB_ENV for later steps.
OIDC is the recommended auth method (no stored Infisical credentials — a short-lived GitHub OIDC token is exchanged at runtime):
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # ← required for OIDC
steps:
- uses: aretecp/github-actions/actions/load-infisical-secrets@v1
with:
method: oidc
identity-id: ${{ vars.INFISICAL_OIDC_IDENTITY_ID }} # non-secret UUID, store as a var
project-slug: ${{ vars.INFISICAL_INTERNAL_PROJECT_SLUG }} # = arete-internal
environment: prod
path: /<repo>
- run: echo "secrets are in env now"
Universal Auth (legacy) passes client-id + client-secret from ${{ secrets.INFISICAL_CLIENT_ID/SECRET }} instead of method: oidc. It's being phased out in favor of OIDC.
Layering shared + project-specific (when a repo needs both arete-shared infra and its own folder): load shared first, project-specific second — the action writes to $GITHUB_ENV and last write wins, so the project value authoritatively overrides any shared-folder collision. Only use recursive: true on the shared load if you actually need its subfolders.
See aretecp/github-actions/actions/load-infisical-secrets/README.md for the full input table, JSON-output mode, and collision rules.
VPS / systemd (long-running prod service, non-interactive)
A systemctl service can't run infisical login interactively. Two correct patterns:
Machine identity (Universal Auth) — create/grant a machine identity read access on the project, then have the unit inject at start:
[Service] # client id/secret live in a root-owned env file, mode 600, NOT in the repo EnvironmentFile=/etc/ari/infisical.id ExecStart=/usr/bin/infisical run --projectId <UUID> --env=prod --path=/<repo> -- /path/to/node server.jsWith
INFISICAL_CLIENT_ID/INFISICAL_CLIENT_SECRET(orINFISICAL_TOKEN) in that env file,infisical runauthenticates non-interactively..envfallback (interim/offline) —EnvironmentFile=<app>/.envwith achmod 600file. Acceptable as a backup or for an air-gapped on-stage box, but the source of truth is still Infisical: regenerate the file withinfisical export --env=prod --path=/<repo> --format=dotenv > .env # or: infisical run ... (preferred, no file)
Prefer (1) for anything networked; keep (2) only as a deliberate offline backup, and say so in the deploy doc.
Conventions / best practices
- Source of truth is Infisical, not
.env. A committed empty.env.exampleis documentation; a real.envis a local/offline cache at best. New keys go into Infisical (all relevant environments), not into a teammate's.env. - Folder path = repo name. When onboarding a repo, create
/<repo>inarete-internalandinfisical initthe repo. Don't make a new project per repo. - Grant, don't multiply, identities. A new repo using CI gets the existing
gh-actions-sharedmachine identity granted read access on its project — don't create a per-repo identity. - Slugs are public, values are not. Project slugs / identity UUIDs are fine as Actions variables or committed literals. Client secrets, tokens, and secret values never go in chat, logs, commits, or AI tools.
- Rotate machine-identity client secrets quarterly (and immediately on suspected exposure): create the new secret, update the consumer, green a smoke run, then revoke the old one — revocation last so a failed rollout can revert.
- Keep all envs in sync. ari-panelist, for example, holds the same key set across
dev/staging/prod; diverging silently is a deploy footgun.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
404 Not Found / "requested entity is not found" on a path you know exists |
Almost always auth, not the path. Self-hosted Infisical returns 404 (not 401/403) for a workspace your session can't see. Run infisical login again, confirm the instance is secrets.areteintelligence.ai, then retry. Also check you init'd against the right project. |
unknown flag: --format |
This CLI uses -o json (--output), not --format. Valid: yaml, json, dotenv. |
| Secrets missing inside the app | You ran the app directly instead of via infisical run (or the systemd unit isn't wrapping it). infisical run injects only into its child process. |
infisical run can't find config |
It walks up from CWD for .infisical.json. Run from inside the repo, or pass --projectId <UUID>. |
| CI: a shared key shadows the app's value | Load order. Load arete-shared first, the project folder second — last write wins. Drop recursive on shared if you don't need subfolders. |
| "Secret not found" but it's in the UI | Wrong --env or --path. Verify: infisical secrets --env=<env> --path=/<repo> -o json | jq -r '.secrets[].secretKey'. |
Canonical sources in the monorepo set
aretecp/github-actions/actions/load-infisical-secrets/— the CI action + its README (input table, OIDC vs universal, layering).aretecp/github-actions/docs/runbooks/infisical-machine-identity.md— identity setup, org-secret wiring, rotation, compromise response.areteos/.claude/skills/infisical/SKILL.md,areteos/bin/deploy,areteos/bin/setup-secrets— the original repo-level pattern (Docker Compose +infisical runwrapper, idempotent setup).