CLI and policies as code
The vulnify command keeps organization policies in git. It validates YAML offline, pulls the policies stored in Vulnify, applies a change plan, and runs test cases. The same commands, flags, and exit codes ship in the Node.js package and in the Python extra.
The file format is on Policy files. The HTTP routes are on Policies. The JSON Schema those files must match is policies.v1.json, copied from schema/policies.v1.json in the Node.js SDK.
Install
Section titled “Install”Node.js 18 or newer. The binary is inside @vulnify/sdk 0.3.0. There is no separate npm package name.
npx -p @vulnify/sdk@0.3.0 vulnify --helpPython 3.9 or newer. The core vulnify install stays on the standard library. The CLI extra pulls PyYAML and jsonschema.
pip install "vulnify[cli]==0.4.0"vulnify --helpvulnify --version prints the package version. The CLI does not send telemetry. Every command accepts --json.
Credentials
Section titled “Credentials”vulnify login checks the key, then writes ~/.config/vulnify/credentials.json with mode 0600. The directory is mode 0700.
vulnify login --api-key "$VULNIFY_API_KEY"vulnify login --api-key "$VULNIFY_API_KEY" --base-url https://api.vulnify.ioVULNIFY_API_KEY and VULNIFY_BASE_URL override the file. The default base URL is https://api.vulnify.io. Do not commit the key. vulnify init writes a hint to vulnify/.gitignore.
Login probes GET /v1/events/00000000-0000-4000-8000-000000000000. HTTP 200, 403, or 404 means the key was accepted (404 is the usual result: that event id does not exist). HTTP 401 is an auth failure. A 404 whose message starts with Cannot GET means that base URL has no decision API.
Commands
Section titled “Commands”| Command | What it does |
|---|---|
vulnify init |
Creates vulnify/policies/example.yaml, vulnify/tests/example.test.yaml, and vulnify/.gitignore. Refuses to overwrite any of them. |
vulnify login |
Stores the key after the probe above. |
vulnify check |
One decision through POST /v1/events. |
vulnify policies validate [path] |
Offline check of kind: Policy files. The default path is vulnify/policies. |
vulnify policies pull [--out dir] |
GET /v1/policies. Writes one YAML file per policy. The default directory is vulnify/policies. |
vulnify policies apply [path] [--prune] [--dry-run] |
POST /v1/policies/apply. The default path is vulnify/policies. |
vulnify test [path] [--local] |
POST /v1/policies/test. The default path is vulnify/tests. |
vulnify initvulnify policies validatevulnify policies pull --out vulnify/policiesvulnify policies apply --dry-runvulnify policies apply --prunevulnify test --localvulnify check --agent support-bot --action EXPORT_DATA --resource customers-db --records 5000check requires --agent, --action, and --resource. --action is an event action (EXPORT_DATA and the rest). Optional --destination is INTERNAL, EXTERNAL_EMAIL, or EXTERNAL_API. --records is an integer of 0 or more. --sensitive is accepted and is not sent: POST /v1/events has no field for it, and the CLI prints that warning on stderr.
A key that starts with vln_live_ prints a warning, because check records a real event. TEST keys stay in the sandbox. If Vulnify cannot be reached, check exits 1. It does not treat the SDK fail-closed fallback as a policy BLOCK.
policies validate prints file:line errors. It does not call the API. Several documents may share a file, separated by ---.
policies pull keeps metadata.id and metadata.updatedAt. policies apply does not send those fields back. Matching is by metadata.name. --dry-run returns the plan and writes nothing. --prune deletes organization policies whose names are missing from the files. Apply runs in one transaction. Each create, update, and delete appends an audit entry with metadata.source policies-as-code and invalidates the decision cache.
Apply requires an org-wide LIVE key: agentId is null, and the prefix is vln_live_. A TEST key, or a key bound to one agent, is HTTP 403 with error forbidden and message policies:apply requires an org-wide LIVE key. --dry-run uses that same route, so it needs the same key. Read and test accept any non-revoked key of the organization.
vulnify test sends the cases. --local also sends the kind: Policy documents in vulnify/policies, and those policies are evaluated instead of the stored ones. Without --local, the server uses the enabled stored policies. The call writes no security event and no audit entry. At most 200 cases. pass: true prints PASS, pass: null prints SKIP (the case had no expect), and any other pass value prints FAIL. The exit code is 1 when any case fails.
When pull, apply, or test receives HTTP 404, the CLI prints This Vulnify server does not support policies as code yet and exits 4. Production https://api.vulnify.io serves these routes. A missing key is exit 5.
Exit codes
Section titled “Exit codes”| Code | Meaning |
|---|---|
| 0 | Success, or ALLOW from check |
| 1 | Validation error, test failure, or another command failure |
| 2 | REVIEW from check |
| 3 | BLOCK from check |
| 4 | This server has no policies-as-code API (HTTP 404 on pull, apply, or test) |
| 5 | Authentication error, including HTTP 401 and HTTP 403 |
GitHub Actions
Section titled “GitHub Actions”Validate on every pull request. That step needs no key. Apply only from the default branch, with an org-wide LIVE key in the repository secret. test --local can use any non-revoked key. A TEST key is enough for tests and does not record a security event.
name: policieson: pull_request: push: branches: [main]jobs: policies: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - name: Validate policy files run: npx -p @vulnify/sdk@0.3.0 vulnify policies validate - name: Test local policies env: VULNIFY_API_KEY: ${{ secrets.VULNIFY_API_KEY }} run: npx -p @vulnify/sdk@0.3.0 vulnify test --local - name: Dry-run the change plan if: github.event_name == 'pull_request' env: VULNIFY_API_KEY: ${{ secrets.VULNIFY_APPLY_KEY }} run: npx -p @vulnify/sdk@0.3.0 vulnify policies apply --dry-run - name: Apply on main if: github.ref == 'refs/heads/main' && github.event_name == 'push' env: VULNIFY_API_KEY: ${{ secrets.VULNIFY_APPLY_KEY }} run: npx -p @vulnify/sdk@0.3.0 vulnify policies applyVULNIFY_APPLY_KEY must be an org-wide LIVE key. VULNIFY_API_KEY in the test step can be a TEST key. The Python extra runs the same commands after pip install "vulnify[cli]==0.4.0".
The rate limit on the three policy routes is 60 requests per minute per key. A 429 body is {"error":"rate_limited"}.

