Skip to content
Merged
67 changes: 67 additions & 0 deletions docs/resources/(resources)/openclaw.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
title: openclaw
description: A reference page for the openclaw resource
---

The openclaw resource installs [OpenClaw](https://docs.openclaw.ai/) — a self-hosted gateway that connects chat channels (Discord, Slack, Telegram, WhatsApp, Signal, iMessage, Matrix, Teams, and more) to AI agents — and manages its configuration. It handles installation via the official installer script and gives you declarative control over the gateway, agents, models, channels, tools, and every other section of the OpenClaw config.

## Parameters

- **settings**: *(object, optional)* Top-level keys to merge into `~/.openclaw/openclaw.json`. On apply, the declared keys are written; on destroy, only the declared keys are removed. Common sections include:
- `gateway` — `mode` (`"local"` | `"remote"`), `port` (default `18789`), `bind` (`"loopback"` | `"lan"` | `"tailnet"` | `"auto"` | `"custom"`), `auth`, `tls`, `controlUi`
- `agents` — `defaults.{workspace,model,thinking,heartbeat,memory,media,skills}`, `list[]` for per-agent overrides
- `models` — `pricing.enabled`, `mode` (`"merge"` | `"replace"`), `providers` (custom/local model providers such as Ollama or LM Studio)
- `channels` — per-provider sections under `channels.<provider>` (e.g. `discord`, `slack`, `telegram`, `whatsapp`, `signal`, `imessage`, `matrix`, `teams`). A channel starts automatically once its config section exists (unless `enabled: false`). Common fields: `dmPolicy`, `groupPolicy`, `allowFrom`, `mediaMaxMb`, `historyLimit`, plus provider-specific credentials (`token`, `botToken`, `appToken`, etc.)
- `tools` — `policy.{allow,deny}` lists controlling which tools (`exec`, `read`, `write`, `browser`, `web_search`, `cron`, etc.) agents can call
- `skills` — `allowBundled`, `load.extraDirs`, `install.nodeManager`
- `plugins` — `enabled`, `allow`/`deny`, `entries.*`
- `mcp` — `servers`, `sessionIdleTtlMs`
- `browser`, `logging`, `cron`, `hooks`, `ui`, `env`, `secrets`, `auth`, `discovery`, `acp` — see the [configuration reference](https://docs.openclaw.ai/gateway/configuration-reference) for the full list of fields

## Example usage

### Install OpenClaw with gateway and agent defaults

```json title="codify.jsonc"
[
{
"type": "openclaw",
"settings": {
"gateway": { "port": 18789, "bind": "loopback" },
"agents": { "defaults": { "model": "anthropic/claude-sonnet-4-6" } }
}
}
]
```

### OpenClaw with a Telegram channel and restricted tools

```json title="codify.jsonc"
[
{
"type": "openclaw",
"settings": {
"channels": {
"telegram": {
"botToken": "<Replace me here!>",
"dmPolicy": "allowlist",
"allowFrom": ["123456789"]
}
},
"tools": {
"policy": { "allow": ["exec", "read", "write", "web_search"] }
}
}
}
]
```

## Notes

- OpenClaw is installed via the official installer (`curl -fsSL https://openclaw.ai/install.sh | bash`) on both macOS and Linux.
- The configuration file lives at `~/.openclaw/openclaw.json` and uses JSON5 (Codify reads/writes it as plain JSON, so any comments in a hand-edited file will not be preserved).
- The `settings` parameter merges only the declared top-level keys. Existing sections not in your Codify config are left untouched.
- After applying settings changes, Codify runs `openclaw gateway restart` so the running gateway picks up the new configuration.
- On destroy, the declared `settings` keys are removed and the OpenClaw binary, config, and state directory (`~/.openclaw`) are removed.
- Model provider authentication (API keys) and full guided onboarding are not managed by this resource — configure credentials under `settings.models` / `settings.auth`, or run `openclaw onboard` manually for an interactive setup.
- See the [OpenClaw configuration reference](https://docs.openclaw.ai/gateway/configuration-reference) for the complete list of configuration sections and fields.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "default",
"version": "1.7.1",
"version": "1.8.3",
"description": "Default plugin for Codify - provides 50+ declarative resources for managing development tools and system configuration across macOS and Linux",
"main": "dist/index.js",
"scripts": {
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { Pnpm } from './resources/javascript/pnpm/pnpm.js';
import { MacosSettingsResource } from './resources/macos/macos-settings/macos-settings-resource.js';
import { MacportsResource } from './resources/macports/macports.js';
import { ClaudeCodeResource } from './resources/claude-code/claude-code.js';
import { OpenClawResource } from './resources/openclaw/openclaw.js';
import { ClaudeCodeProjectResource } from './resources/claude-code/claude-code-project.js';
import { OllamaResource } from './resources/ollama/ollama.js';
import { PgcliResource } from './resources/pgcli/pgcli.js';
Expand Down Expand Up @@ -127,6 +128,7 @@ runPlugin(Plugin.create(
new SyncthingDeviceResource(),
new SyncthingFolderResource(),
new RbenvResource(),
new OpenClawResource(),
],
{ minSupportedCliVersion: MIN_SUPPORTED_CLI_VERSION }
))
4 changes: 3 additions & 1 deletion src/resources/javascript/nvm/nvm.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ExampleConfig, getPty, Resource, ResourceSettings, SpawnStatus, Utils } from '@codifycli/plugin-core';
import { ApplyNotes, CodifyCliSender, ExampleConfig, getPty, Resource, ResourceSettings, SpawnStatus, Utils } from '@codifycli/plugin-core';
import { OS, ResourceConfig } from '@codifycli/schemas';
import * as os from 'node:os';

Expand Down Expand Up @@ -87,6 +87,8 @@ export class NvmResource extends Resource<NvmConfig> {
'[ -s "$NVM_DIR/nvm.sh" ] && \\. "$NVM_DIR/nvm.sh"'
])
}

CodifyCliSender.sendApplyNote(ApplyNotes.NEW_SHELL_REQUIRED, 'nvm');
}

override async destroy(): Promise<void> {
Expand Down
90 changes: 90 additions & 0 deletions src/resources/openclaw/openclaw.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { describe, expect, it } from 'vitest';
import { settingsSchema } from './openclaw.js';
import { OpenClawSettingsParameter } from './settings-parameter.js';

describe('openclaw settings schema', () => {
it('rejects tools.policy — must use tools.allow / tools.deny directly', () => {
const result = settingsSchema.safeParse({
tools: { policy: { allow: ['exec', 'read'] } },
});
expect(result.success).toBe(false);
expect(JSON.stringify(result.error?.issues)).toContain('policy');
});

it('rejects skills.workspace and skills.autoLoad — must use skills.load.extraDirs', () => {
const result = settingsSchema.safeParse({
skills: { workspace: '$HOME/openclaw-skills', autoLoad: true },
});
expect(result.success).toBe(false);
const issues = JSON.stringify(result.error?.issues);
expect(issues).toContain('workspace');
expect(issues).toContain('autoLoad');
});

it('rejects cron.jobs — jobs are stored in ~/.openclaw/cron/jobs.json, not in openclaw.json', () => {
const result = settingsSchema.safeParse({
cron: { jobs: [{ name: 'morning-briefing', schedule: '0 7 * * *' }] },
});
expect(result.success).toBe(false);
expect(JSON.stringify(result.error?.issues)).toContain('jobs');
});

it('accepts valid tools config', () => {
const result = settingsSchema.safeParse({
tools: { allow: ['exec', 'read', 'write', 'web_search'] },
});
expect(result.success).toBe(true);
});

it('accepts valid skills config with load.extraDirs', () => {
const result = settingsSchema.safeParse({
skills: { load: { extraDirs: ['$HOME/openclaw-skills'] } },
});
expect(result.success).toBe(true);
});

it('accepts valid cron config without jobs', () => {
const result = settingsSchema.safeParse({
cron: { enabled: true, maxConcurrentRuns: 8 },
});
expect(result.success).toBe(true);
});

it('accepts the full example config shape', () => {
const result = settingsSchema.safeParse({
gateway: { mode: 'local', port: 18789, bind: 'loopback' },
agents: {
defaults: {
model: 'anthropic/claude-sonnet-4-6',
workspace: '$HOME/openclaw-workspace',
maxConcurrent: 4,
},
},
channels: {
telegram: {
botToken: 'token',
dmPolicy: 'allowlist',
allowFrom: ['123456789'],
},
},
tools: {
allow: ['exec', 'read', 'write', 'web_search', 'browser', 'skills'],
},
skills: {
load: { extraDirs: ['$HOME/openclaw-skills'] },
},
browser: { enabled: true, headless: true },
cron: { enabled: true, maxConcurrentRuns: 8 },
});
expect(result.success).toBe(true);
});

it('passes through unknown top-level keys (hooks, session, memory, etc.)', () => {
const result = settingsSchema.safeParse({
hooks: { enabled: true, token: 'abc', path: '/hooks' },
session: { dmScope: 'main' },
memory: { backend: 'default' },
});
expect(result.success).toBe(true);
});
});
Loading
Loading