open-wa
DocsPlugin Security Model

Plugin Security Model

Understand what plugins can and cannot do, how config validation works, and how to handle secrets safely.

Plugin Security Model

Plugins load as JavaScript modules in the open-wa Node.js process and run with that process's operating-system permissions. The SDK does not create a filesystem, network, or process sandbox, so review a plugin and its dependencies before loading it.

API restrictions

Cannot Emit Events

The events object supplied to init() exposes on(), once(), and off(); it has no emit() method. This limits access through that object, but it does not prevent plugin code from importing Node.js modules or using other host-process capabilities.

// This is NOT possible:
events.emit('custom.event', data); // No emit method exists

Internal and sensitive events

The gateway checks the event metadata registered by the host. Events marked internal or sensitive are ignored and logged when a plugin subscribes; the policy is metadata-based, not a blanket block on particular name prefixes. Unknown event names are passed to the underlying emitter, so the gateway should not be treated as a sandbox or as an authorization boundary for arbitrary code.

No direct browser access through the SDK

PluginInput includes a client proxy, not a browser or CDP handle. The client can call WhatsApp methods exposed by window.WAPI; ask(method, args) forwards the requested method name to that runtime, so it is not a method allowlist. The SDK client interface does not expose methods to:

  • Execute CDP commands
  • Manipulate the browser DOM directly
  • Access browser cookies or storage directly
  • Inject scripts into the WhatsApp Web page

What the SDK does not isolate

Node's dynamic import() runs both a plugin's top-level code and init() in the host process. The event gateway and client proxy describe SDK interfaces; they do not limit a plugin's Node.js imports or OS permissions.

Review the plugin source, transitive dependencies, and requested permissions before installation. If code must be treated as untrusted, run it in a separately managed service with an operating-system or container boundary and communicate through an external API. Configure that isolation in your deployment environment; open-wa does not provide a plugin sandbox.

What Plugins Can Do

Subscribe to Events Allowed by the Gateway

events.on('message.received', (payload) => { ... });
events.once('client.ready', () => { ... });

Call WhatsApp Methods Through the Client Proxy

The convenience methods and ask() call methods provided by the connected WhatsApp runtime:

await client.sendText('123@c.us', 'Hello');
await client.getHostNumber();
await client.ask('getAllChats');

Mount HTTP routes

init: async () => ({
  routes: () => {
    const app = new Hono();
    app.get('/health', (c) => c.json({ ok: true }));
    return app;
  },
})

The host mounts the returned Hono app at /plugins/<name>/.

Declare AI Tools

init: async () => ({
  tool: {
    myTool: {
      description: 'Does something useful',
      args: { input: z.string() },
      execute: async (args, ctx) => { ... },
    },
  },
})

This declares a tool in the plugin hooks. The current MCP endpoint does not expose plugin-declared tools; see AI Tools for the host integration requirement.

Add dashboard pages

init: async () => ({
  pages: [{ path: '/', title: 'Status', icon: '📊' }],
})

The dashboard reads these declarations from the plugin manifest and renders its standard plugin status view. They do not grant a plugin a custom React component or a separate process.

Config Validation

How It Works

If you provide a configSchema in your plugin definition, the host validates the configuration before calling init().

export default createPlugin({
  meta: { name: 'my-plugin' },
  configSchema: z.object({
    apiUrl: z.string().url(),
    apiKey: z.string().min(1),
  }),
  init: async ({ config }) => {
    // config is guaranteed to match the schema
  },
});

If Validation Fails

  • The plugin will not register
  • An error is logged with details about what failed
  • During client creation, the registration error aborts plugin setup and the client start path; the current host does not skip the invalid plugin and continue with the remaining list

Handling Missing Config Gracefully

Use Zod defaults and optional fields:

const configSchema = z.object({
  apiUrl: z.string().url().default('https://api.example.com'),
  apiKey: z.string().optional(),
  retries: z.number().int().positive().default(3),
});

If apiKey is optional, your plugin must accept an undefined value:

init: async ({ config }) => {
  if (!config.apiKey) {
    logger.warn('No API key configured, some features will be disabled');
  }
}

Secret Management

Environment Variables

Reference environment variables in your wa.config.js:

// wa.config.js
export default {
  pluginConfig: {
    'my-plugin': {
      apiKey: process.env.MY_PLUGIN_API_KEY,
    },
  },
};

Read values from a .env file

If your app loads dotenv values, keep them in a .env file in the project root:

MY_PLUGIN_API_KEY=sk-abc123...

The config file can read from process.env which picks up .env values if your setup loads them.

Are Config Values Logged?

Plugin config values are not automatically logged. But if your plugin logs the config object directly, secrets will appear in the logs. Always redact sensitive fields:

// BAD: logs the API key
logger.info('Config loaded', { config });

// GOOD: logs only non-sensitive fields
logger.info('Config loaded', { apiUrl: config.apiUrl, hasKey: !!config.apiKey });

Validating API Keys Are Present

const configSchema = z.object({
  apiKey: z.string().min(1, 'API key is required'),
});

This will fail validation with a clear error message if the key is missing or empty.

Rotating Keys Without Restart

Currently, config is validated once at plugin initialization. To replace keys:

  1. Update the environment variable or config file
  2. Restart the session
  3. The plugin will reload with the new key

Error Handling Patterns

API Failures (Rate Limits, Timeouts, 401)

'message.received': async ({ message }) => {
  try {
    const result = await fetchExternalAPI(message);
  } catch (error) {
    if (error.status === 429) {
      logger.warn('Rate limited, will retry', { retryAfter: error.retryAfter });
      // Queue for retry
    } else if (error.status === 401) {
      logger.error('Invalid API key');
      // Notify admin or disable feature
    } else {
      logger.error('API call failed', { error: error.message });
      // Continue processing other messages
    }
  }
}

Network Errors

try {
  const response = await fetch(url, { signal: AbortSignal.timeout(10000) });
} catch (error) {
  if (error.name === 'TimeoutError') {
    logger.warn('Request timed out');
  } else if (error.code === 'ECONNREFUSED') {
    logger.error('Connection refused');
  }
}

Malformed Messages

Always validate message data before processing:

'message.received': async ({ message }) => {
  const msg = message as { body?: string; from?: string };
  if (!msg.body || !msg.from) {
    logger.debug('Skipping malformed message');
    return;
  }
  // Process the message
}

WhatsApp Connection Drops

Use lifecycle hooks to detect connection state:

'core.stopping': async ({ reason }) => {
  logger.info('Session stopping', { reason });
  // Flush queues, save state
},

'client.ready': async ({ sessionId }) => {
  logger.info('Reconnected', { sessionId });
  // Resume processing
},

Throw an error or continue

  • logger.error() and continue: For recoverable errors (network timeouts, rate limits), the plugin keeps processing other messages.
  • Throw from an event handler: The host catches the rejection and logs a plugin_hook_error; it does not provide an automatic retry or unregister operation.
  • Throw from init(): Registration fails and the client start path aborts, so use this for unrecoverable initialization errors.
  • dispose(): Use this hook to release resources during host shutdown. The current API has no plugin-level unregister operation.
  • Queue for retry: For transient failures (network errors, rate limits), implement retry with exponential backoff.

Was this helpful?

Your answer includes the page path and docs version.

On this page