Skip to main content

Node.js SDK

Supported modes: Hosted Hybrid Local Available in: Free Solo Teams

When NOT to use this

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:

Hosted mode requires async initialization

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 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.

Configuration

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' },
],
},
});
Legacy and canonical action names both work

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

OptionEnvironment VariableTypeDefaultDescription
apiKeyCONTROLZERO_API_KEYstring-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.
policyFileCONTROLZERO_POLICY_FILEstring-Path to a YAML or JSON policy file.
strictHosted-boolfalseRaise on hybrid (API key + local policy) instead of warn.
logPath-string./controlzero.logLocal audit log file path.
logRotation-stringdailyAudit log rotation interval.
logRetention-string30 daysHow long to keep rotated logs.
logCompression-stringnullCompress rotated logs (e.g. gz).
logFormat-stringjsonAudit 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:

NameTypeRequiredDescription
toolstringYesTool name (e.g., "database").
optsGuardOptionsNoOptions object with method, args, raiseOnDeny, context.

GuardOptions:

FieldTypeDefaultDescription
methodstring"*"Method name. Combined with tool to form "tool:method" action.
argsobject{}Arguments for DLP scanning and conditional evaluation.
raiseOnDenybooleanfalseIf true, throws PolicyDeniedError on deny.
contextobject-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().

AttributeTypeDescription
effectstring"allow" or "deny".
policyIdstring or nullThe policy that matched, if any.
reasonstringHuman-readable explanation.
deniedbooleanConvenience: true when effect is "deny".
dlpFindingsarrayList 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

FunctionProviderImport 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';