Node.js SDK
Supported modes: Hosted Hybrid Local Available in: Free Solo Teams
If you are not changing your application code, the gateway proxy gives you governance with zero code changes. If you are governing developer AI tools (Claude Code, Cursor, Codex CLI), use coding hooks instead.
The Control Zero Node.js SDK provides policy enforcement for AI agents running in JavaScript and TypeScript environments.
Installation
npm install @controlzero/sdk
Requirements:
- Node.js 18 or later
- TypeScript 5.0+ (optional, for type-checked usage)
Quickstart
Choose your deployment mode:
Unlike the Python SDK, the Node.js new Client({apiKey}) constructor throws an error
when apiKey is provided without a local policy. Use the async factory for hosted mode:
// Hosted mode -- MUST use async factory
const cz = await Client.create({ apiKey: 'cz_live_your_api_key_here' });
// Local mode only -- synchronous constructor works
const cz = new Client({ policyFile: 'controlzero.yaml' });
- Hosted
- Hybrid
- Local
Hosted Policy pulled from dashboard. Audit in cloud. Recommended for most teams.
import { Client } from '@controlzero/sdk';
// MUST use async factory for hosted mode
const client = await Client.create({ apiKey: 'cz_live_your_api_key_here' });
// Or use env var: export CONTROLZERO_API_KEY="cz_live_..."
The SDK pulls the signed policy bundle from the dashboard at first call, verifies its signature, decrypts locally, and enforces every call against the dashboard policy. Audit ships to the remote trail automatically.
Hybrid You own the policy file. API key enables remote audit only.
import { Client } from '@controlzero/sdk';
const client = await Client.create({
apiKey: 'cz_live_your_api_key_here',
policyFile: './controlzero.yaml',
});
Your controlzero.yaml governs enforcement. The API key sends audit to your dashboard.
Local No API key. No network calls. Fully offline. Air-gap compatible.
import { Client } from '@controlzero/sdk';
// From a file -- synchronous constructor works for local mode
const client = new Client({ policyFile: './controlzero.yaml' });
// Or inline policy
const client = new Client({
policy: {
rules: [
{ allow: 'database:query', reason: 'Reads are permitted' },
{ deny: 'database:execute', reason: 'Writes are blocked' },
],
},
});
Audit written to ./controlzero.log.
Configuration
Hosted Hosted Mode (Recommended)
Pass only your project API key via the async Client.create factory.
The SDK pulls the signed policy bundle from the dashboard at first
call, verifies its signature, decrypts locally, and enforces every
call against the dashboard policy. Audit ships to the remote trail
automatically.
import { Client } from '@controlzero/sdk';
const client = await Client.create({ apiKey: 'cz_live_your_api_key_here' });
new Client({ apiKey }) (synchronous) refuses to construct with an API
key alone and points you at Client.create() -- hosted bootstrap is an
I/O call and must be await-able.
Local With a Local Policy File
import { Client } from '@controlzero/sdk';
const client = new Client({ policyFile: './controlzero.yaml' });
Inline Policy
import { Client } from '@controlzero/sdk';
const client = new Client({
policy: {
rules: [
{ allow: 'database:query', reason: 'Reads are permitted' },
{ deny: 'database:*', reason: 'All other database operations are blocked' },
],
},
});
database:query, database:execute, and database:delete are legacy action names that match the same calls as the canonical database:read, database:write, and database:admin classes. New policies should prefer the canonical names; existing rules with the legacy names continue to work without changes.
Wildcard resource matching
A rule that lists resources: ['*'] matches every call regardless of whether the caller passed a context.resource. Use this when a rule should apply universally and you do not have (or do not want to pass) a per-call resource:
// Policy rule with universal resource matching
// { effect: 'allow', actions: ['database:read'], resources: ['*'] }
// This call matches even though context.resource is not set:
cz.guard('database', { method: 'query', args: { sql: 'SELECT 1' } });
Non-wildcard resource patterns (for example, resources: ['table/orders']) still require the caller to supply a matching context.resource for the rule to fire.
Environment Variables
You can configure the SDK using environment variables instead of passing arguments directly:
export CONTROLZERO_POLICY_FILE="./controlzero.yaml"
import { Client } from '@controlzero/sdk';
// Reads CONTROLZERO_POLICY_FILE from environment, or looks for ./controlzero.yaml in cwd
const client = new Client();
Configuration Options
| Option | Environment Variable | Type | Default | Description |
|---|---|---|---|---|
apiKey | CONTROLZERO_API_KEY | string | - | Project API key. Use Client.create({ apiKey }) to enable hosted mode (auto-pulls the signed policy bundle from the dashboard; remote audit). |
policy | - | object | - | Inline policy object with rules. |
policyFile | CONTROLZERO_POLICY_FILE | string | - | Path to a YAML or JSON policy file. |
strictHosted | - | bool | false | Raise on hybrid (API key + local policy) instead of warn. |
logPath | - | string | ./controlzero.log | Local audit log file path. |
logRotation | - | string | daily | Audit log rotation interval. |
logRetention | - | string | 30 days | How long to keep rotated logs. |
logCompression | - | string | null | Compress rotated logs (e.g. gz). |
logFormat | - | string | json | Audit log format (json or pretty). |
Policy resolution order: policy option, policyFile option, CONTROLZERO_POLICY_FILE env var, ./controlzero.yaml in cwd, then no-op pass-through with a one-time warning.
Basic Usage
Evaluating a Tool Call
The primary method is guard(), which evaluates a tool call against the loaded policy:
import { Client } from '@controlzero/sdk';
const client = new Client({ policyFile: './controlzero.yaml' });
const decision = client.guard('database', {
method: 'query',
args: { sql: 'SELECT * FROM orders' },
});
console.log(decision.effect); // "allow" or "deny"
console.log(decision.reason); // human-readable reason
console.log(decision.policyId); // matching policy ID
Handling Denied Actions
When a policy denies the action, check the decision effect or catch PolicyDeniedError:
import { Client, PolicyDeniedError } from '@controlzero/sdk';
const client = new Client({ policyFile: './controlzero.yaml' });
const decision = client.guard('filesystem', {
method: 'write_file',
args: { path: '/data/output.csv', content: '...' },
});
if (decision.effect === 'deny') {
console.log(`Blocked: ${decision.reason}`);
console.log(`Policy: ${decision.policyId}`);
}
Or use raiseOnDeny to throw on deny:
try {
client.guard('filesystem', {
method: 'write_file',
args: { path: '/data/output.csv' },
raiseOnDeny: true,
});
} catch (error) {
if (error instanceof PolicyDeniedError) {
console.log(`Blocked: ${error.message}`);
}
}
API Reference
Client
The main client class. Synchronous construction, synchronous guard().
new Client(options?)
Creates a new Control Zero client. See Configuration Options for available parameters. No initialize() call is needed; the constructor loads the policy immediately.
guard(tool, opts?): PolicyDecision
Evaluates a tool call against the loaded policy.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
tool | string | Yes | Tool name (e.g., "database"). |
opts | GuardOptions | No | Options object with method, args, raiseOnDeny, context. |
GuardOptions:
| Field | Type | Default | Description |
|---|---|---|---|
method | string | "*" | Method name. Combined with tool to form "tool:method" action. |
args | object | {} | Arguments for DLP scanning and conditional evaluation. |
raiseOnDeny | boolean | false | If true, throws PolicyDeniedError on deny. |
context | object | - | Optional context with resource and tags for matching. |
Returns: PolicyDecision with effect, reason, policyId, and dlpFindings.
close(): Promise<void>
Flushes buffered audit logs and releases resources.
PolicyDecision
Returned by guard().
| Attribute | Type | Description |
|---|---|---|
effect | string | "allow" or "deny". |
policyId | string or null | The policy that matched, if any. |
reason | string | Human-readable explanation. |
denied | boolean | Convenience: true when effect is "deny". |
dlpFindings | array | List of DLP matches found in the arguments. |
PolicyDeniedError
Thrown by guard() when raiseOnDeny is true and the policy denies the action.
class PolicyDeniedError extends Error {
readonly decision: PolicyDecision;
}
Usage with MCP
Control Zero integrates naturally with the Model Context Protocol. Here is an example of wrapping MCP tool calls with policy enforcement:
import { Client, PolicyDeniedError } from '@controlzero/sdk';
const cz = new Client({ policyFile: './controlzero.yaml' });
function callMCPToolGoverned(server: string, tool: string, args: Record<string, unknown>): unknown {
// Evaluate the policy before calling the tool
cz.guard(server, {
method: tool,
args,
raiseOnDeny: true,
});
// Policy check passed. Call the tool
return mcpClient.callTool(server, tool, args);
}
LLM Provider Wrappers
The Node.js SDK includes lightweight wrappers that add governance to popular LLM provider clients. Each wrapper intercepts calls, evaluates policies, and logs decisions without changing the provider's native API surface.
Google AI (Gemini)
import { Client } from '@controlzero/sdk';
import { GoogleGenerativeAI } from '@google/generative-ai';
const cz = new Client({ policyFile: './controlzero.yaml' });
const genAI = new GoogleGenerativeAI('your-google-ai-key');
const model = genAI.getGenerativeModel({ model: 'gemini-2.0-flash' });
// Use the governance-aware wrapper from integrations
import { wrapGoogle } from '@controlzero/sdk/integrations';
const wrappedModel = wrapGoogle(model, cz);
const result = await wrappedModel.generateContent('Summarize the quarterly report');
console.log(result.response.text());
Anthropic
import { Client } from '@controlzero/sdk';
import Anthropic from '@anthropic-ai/sdk';
const cz = new Client({ policyFile: './controlzero.yaml' });
const anthropic = new Anthropic({ apiKey: 'your-anthropic-key' });
import { wrapAnthropic } from '@controlzero/sdk/integrations';
const wrappedClient = wrapAnthropic(anthropic, cz);
const message = await wrappedClient.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Summarize the quarterly report' }],
});
console.log(message.content[0].text);
Available Wrappers
| Function | Provider | Import Path |
|---|---|---|
wrapGoogle() | Google AI (Gemini) | @controlzero/sdk/integrations |
wrapAnthropic() | Anthropic (Claude) | @controlzero/sdk/integrations |
wrapOpenAI() | OpenAI | @controlzero/sdk/integrations |
All wrappers apply pre-flight checks (model blocking, cost estimation, PII detection) and log the request to the audit trail. If a policy denies the request, PolicyDeniedError is thrown before the call reaches the provider.
Error Handling
import { Client, PolicyDeniedError } from '@controlzero/sdk';
const client = new Client({ policyFile: './controlzero.yaml' });
try {
client.guard('database', {
method: 'query',
args: { sql: 'SELECT 1' },
raiseOnDeny: true,
});
} catch (error) {
if (error instanceof PolicyDeniedError) {
console.log(`Denied: ${error.decision.reason}`);
}
}
await client.close();
CommonJS Support
The SDK supports both ESM and CommonJS:
// ESM
import { Client } from '@controlzero/sdk';
// CommonJS
const { Client } = require('@controlzero/sdk');
The legacy name ControlZeroClient is exported as a backward-compatibility alias for Client.
TypeScript
The SDK is written in TypeScript and ships with full type declarations. All types are exported from the main entry point:
import type { PolicyDecision } from '@controlzero/sdk';
import type { ClientOptions, GuardOptions } from '@controlzero/sdk';