Openfang infisical-sync-skill
Expert knowledge for the Infisical Sync Hand — Infisical API reference, vault operations, error patterns, security guidance
git clone https://github.com/RightNow-AI/openfang
T=$(mktemp -d) && git clone --depth=1 https://github.com/RightNow-AI/openfang "$T" && mkdir -p ~/.claude/skills && cp -r "$T/crates/openfang-hands/bundled/infisical-sync" ~/.claude/skills/rightnow-ai-openfang-infisical-sync-skill && rm -rf "$T"
crates/openfang-hands/bundled/infisical-sync/SKILL.mdInfisical Sync Expert Knowledge
1. Infisical API Reference
Base URL
All requests go to
$INFISICAL_URL. This is the self-hosted instance base URL, e.g. https://infisical.example.com.
Authentication — Universal Auth
Infisical uses Machine Identities with Universal Auth for agent-to-agent communication.
Endpoint:
POST /api/v1/auth/universal-auth/login
Request:
{ "clientId": "<INFISICAL_CLIENT_ID>", "clientSecret": "<INFISICAL_CLIENT_SECRET>" }
Response (success):
{ "accessToken": "eyJ...", "expiresIn": 7200, "accessTokenMaxTTL": 43200, "tokenType": "Bearer" }
curl example:
RESPONSE=$(curl -s -X POST "$INFISICAL_URL/api/v1/auth/universal-auth/login" \ -H "Content-Type: application/json" \ -d "{\"clientId\":\"$INFISICAL_CLIENT_ID\",\"clientSecret\":\"$INFISICAL_CLIENT_SECRET\"}") ACCESS_TOKEN=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['accessToken'])")
Token lifetime:
expiresIn seconds (usually 7200 = 2 hours). Re-authenticate when expired.
List Secrets
Endpoint:
GET /api/v4/secrets
Query parameters:
| Param | Required | Description |
|---|---|---|
| Yes | Infisical project ID |
| Yes | Environment slug (e.g. , , ) |
| No | Path prefix, default |
| No | Include imported secrets, default |
| No | Include secrets in sub-paths, default |
curl example:
curl -s -X GET \ "$INFISICAL_URL/api/v4/secrets?projectId=$PROJECT_ID&environment=$ENVIRONMENT&secretPath=/" \ -H "Authorization: Bearer $ACCESS_TOKEN"
Response shape:
{ "secrets": [ { "id": "uuid", "version": 1, "secretKey": "DATABASE_URL", "secretValue": "postgres://...", "secretComment": "", "environment": "prod", "workspace": "uuid" } ], "imports": [] }
Parse with:
echo "$RESPONSE" | python3 -c " import sys, json data = json.load(sys.stdin) for s in data.get('secrets', []): print(s['secretKey']) "
Create or Update a Secret
The API does not provide a single upsert endpoint.
POST creates only (returns 409 if the secret already exists); PATCH updates only (returns 404 if missing). Use the create-then-update pattern:
Step 1 — Try to create (POST)
Endpoint:
POST /api/v4/secrets/{secretName}
Request body:
{ "projectId": "<PROJECT_ID>", "environment": "<ENV>", "secretValue": "<VALUE>", "secretPath": "/" }
HTTP_STATUS=$(curl -s -o /tmp/infisical_response.json -w "%{http_code}" \ -X POST "$INFISICAL_URL/api/v4/secrets/$SECRET_NAME" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"projectId\":\"$PROJECT_ID\",\"environment\":\"$ENVIRONMENT\",\"secretValue\":\"$SECRET_VALUE\",\"secretPath\":\"/\"}")
Returns
201 on success.
Step 2 — If 409, update via PATCH
Endpoint:
PATCH /api/v4/secrets/{secretName}
if [ "$HTTP_STATUS" = "409" ]; then HTTP_STATUS=$(curl -s -o /tmp/infisical_response.json -w "%{http_code}" \ -X PATCH "$INFISICAL_URL/api/v4/secrets/$SECRET_NAME" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"projectId\":\"$PROJECT_ID\",\"environment\":\"$ENVIRONMENT\",\"secretValue\":\"$SECRET_VALUE\",\"secretPath\":\"/\"}") fi
Returns
200 on success. Any other status code is an error.
Important: URL-encode the secret name if it contains special characters.
Delete a Secret
Endpoint:
DELETE /api/v4/secrets/{secretName}
Query parameters:
projectId, environment, secretPath (default /)
curl example:
curl -s -X DELETE \ "$INFISICAL_URL/api/v4/secrets/$SECRET_NAME?projectId=$PROJECT_ID&environment=$ENVIRONMENT&secretPath=/" \ -H "Authorization: Bearer $ACCESS_TOKEN"
List Accessible Projects (Workspaces)
Endpoint:
GET /api/v1/workspace
curl -s -X GET "$INFISICAL_URL/api/v1/workspace" \ -H "Authorization: Bearer $ACCESS_TOKEN"
Response:
{ "workspaces": [{ "id": "uuid", "name": "...", "environments": [...] }] }
2. HTTP Error Codes
| Code | Meaning | Action |
|---|---|---|
| 200/201 | Success | Continue |
| 400 | Bad Request | Log the response body — likely malformed JSON or missing field |
| 401 | Unauthorized | Re-authenticate; token may have expired |
| 403 | Forbidden | Machine identity lacks permissions — check Infisical Access Control |
| 404 | Not Found | Secret or project doesn't exist |
| 429 | Rate Limited | Wait 60 seconds, retry once |
| 500/503 | Server Error | Log + retry once after 30 seconds; notify if still failing |
Always check HTTP status before trusting response body:
HTTP_STATUS=$(curl -s -o /tmp/infisical_response.json -w "%{http_code}" ...) if [ "$HTTP_STATUS" != "200" ] && [ "$HTTP_STATUS" != "201" ]; then # handle error fi RESPONSE=$(cat /tmp/infisical_response.json)
3. Sync State File Format
Stored at
infisical_sync_state.json:
{ "last_sync": "2025-01-15T10:30:00Z", "project_ids": ["uuid1", "uuid2"], "environment": "prod", "secrets": { "DATABASE_URL": { "hash": "sha256_of_key_plus_value", "version": 3, "last_synced": "2025-01-15T10:30:00Z" } }, "error_count": 0, "push_count": 12, "pull_count": 47 }
Hash computation (to detect changes without storing values):
echo -n "DATABASE_URL:postgres://..." | sha256sum | awk '{print $1}'
Or with Python:
import hashlib h = hashlib.sha256(f"{key}:{value}".encode()).hexdigest()
4. Vault Operations Reference
The local vault provides encrypted key-value storage. All secrets synced from Infisical go here.
| Operation | Description |
|---|---|
| Write or overwrite secret K |
| Read secret K |
| List all keys (values not returned) |
| Delete secret K |
Bulk sync pattern:
// Pull from Infisical → vault for each (key, value) in infisical_secrets: vault_set key=<key> value=<value> // Optionally remove orphans vault_list → local_keys infisical_keys = set of keys returned by Infisical for key in local_keys - infisical_keys: vault_delete key=<key>
5. Security Checklist
Before every sync cycle, verify:
-
is set and non-emptyINFISICAL_URL -
is set and non-emptyINFISICAL_CLIENT_ID -
is set and non-emptyINFISICAL_CLIENT_SECRET - The access token was freshly obtained this cycle (never reuse across cycles)
- No secret values appear in curl command echo output (use variables, not inline values)
- Response body is never logged verbatim (strip
fields before logging)secretValue
6. Common Failure Modes
"Failed to fetch secrets: 403 Forbidden"
The Machine Identity exists but lacks permissions. In Infisical:
- Go to Access Control → Machine Identities
- Find this agent's identity
- Assign it the
role (ormember
for read-only) on the projectviewer
"Connection refused / Could not connect to server"
INFISICAL_URL is wrong or the instance is down. Verify the URL is reachable:
curl -s "$INFISICAL_URL/api/status" | python3 -c "import sys,json; print(json.load(sys.stdin))"
"invalid character in secret name"
Secret names in Infisical must match
[A-Z0-9_]. If the vault has mixed-case keys, normalise before pushing:
echo "my_secret_key" | tr '[:lower:]' '[:upper:]'
"accessToken undefined in response"
Authentication failed. The response body will contain an error message. Check:
andINFISICAL_CLIENT_ID
are correctINFISICAL_CLIENT_SECRET- The Machine Identity is not disabled in Infisical
- The Machine Identity's token TTL hasn't been set to 0
7. Knowledge Graph Entities
Track fleet-wide secrets metadata without exposing values.
Entity types
— the Infisical instance itselfservice
— an Infisical workspace/projectsecret_project
— a named secret (key only, never value)secret
Relation types
→secret
→belongs_tosecret_project
→secret_project
→hosted_byservice
→secret
→synced_toagent_vault
Query examples
knowledge_query type=secret // list all known secrets knowledge_query type=secret_project // list all projects knowledge_query relation=belongs_to target=<project_id> // secrets in a project