Openfang infisical-sync-skill

Expert knowledge for the Infisical Sync Hand — Infisical API reference, vault operations, error patterns, security guidance

install
source · Clone the upstream repo
git clone https://github.com/RightNow-AI/openfang
Claude Code · Install into ~/.claude/skills/
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"
manifest: crates/openfang-hands/bundled/infisical-sync/SKILL.md
source content

Infisical 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:

ParamRequiredDescription
projectId
YesInfisical project ID
environment
YesEnvironment slug (e.g.
prod
,
staging
,
dev
)
secretPath
NoPath prefix, default
/
includeImports
NoInclude imported secrets, default
false
recursive
NoInclude secrets in sub-paths, default
false

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

CodeMeaningAction
200/201SuccessContinue
400Bad RequestLog the response body — likely malformed JSON or missing field
401UnauthorizedRe-authenticate; token may have expired
403ForbiddenMachine identity lacks permissions — check Infisical Access Control
404Not FoundSecret or project doesn't exist
429Rate LimitedWait 60 seconds, retry once
500/503Server ErrorLog + 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.

OperationDescription
vault_set key=K value=V
Write or overwrite secret K
vault_get key=K
Read secret K
vault_list
List all keys (values not returned)
vault_delete key=K
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:

  • INFISICAL_URL
    is set and non-empty
  • INFISICAL_CLIENT_ID
    is set and non-empty
  • INFISICAL_CLIENT_SECRET
    is set and non-empty
  • 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
    secretValue
    fields before logging)

6. Common Failure Modes

"Failed to fetch secrets: 403 Forbidden"

The Machine Identity exists but lacks permissions. In Infisical:

  1. Go to Access Control → Machine Identities
  2. Find this agent's identity
  3. Assign it the
    member
    role (or
    viewer
    for read-only) on the project

"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:

  1. INFISICAL_CLIENT_ID
    and
    INFISICAL_CLIENT_SECRET
    are correct
  2. The Machine Identity is not disabled in Infisical
  3. 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

  • service
    — the Infisical instance itself
  • secret_project
    — an Infisical workspace/project
  • secret
    — a named secret (key only, never value)

Relation types

  • secret
    belongs_to
    secret_project
  • secret_project
    hosted_by
    service
  • secret
    synced_to
    agent_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