Skill: redcontritio/config-field (version 1.0.0)

--- name: config-field description: Validate OpenClaw configuration fields against the official Zod schema. Use when reading or writing openclaw.json to check if configuration fields exist and are valid. Covers agents, channels, tools, logging, session configuration with 136+ field definitions. --- # Config Field Validator Validate OpenClaw configuration fields against the official Zod schema. ## When to Use This Skill - **Before editing configurations** - Verify a field exists before adding it - **Debugging config errors** - Check if invalid fields are causing issues - **Migrating configs** - Validate fields after version upgrades - **Reviewing configs** - Ensure all fields are schema-compliant ## How It Works This skill automatically manages schema synchronization: 1. **Check Version** - Detects local OpenClaw version 2. **Sync Schema** - Downloads matching schema from GitHub if needed 3. **Generate Fields** - Parses Zod schema to extract field definitions 4. **Validate** - Uses generated schema to validate configuration ## Quick Start ```bash # Validate a single field (auto-syncs schema if needed) python3 scripts/validate_field.py agents.defaults.model.primary # Validate entire config file python3 scripts/validate_config.py /path/to/openclaw.json # Force schema re-sync python3 scripts/sync_schema.py --force # Check current schema status python3 scripts/sync_schema.py --status ``` ## Field Path Format Field paths use dot notation: ``` agents.defaults.model.primary → agents.defaults.model.primary channels.telegram.botToken → channels.telegram.botToken tools.web.search.provider → tools.web.search.provider ``` ## Workflow ### For Users Simply use validation commands - schema sync is automatic: ```bash # This will auto-sync schema if version mismatch detected python3 scripts/validate_field.py agents.defaults.timeoutSeconds ``` ### For Schema Management ```bash # Check schema status python3 scripts/sync_schema.py --status # Output: Schema version: 2.1.0 (matches OpenClaw) # Force re-sync (if needed) python3 scripts/sync_schema.py --force # Generate fresh field reference python3 scripts/generate_fields.py ``` ## Schema Storage Schema is cached locally at: ``` ~/.config/openclaw/skills/config-field/ ├── schema/ # Downloaded TypeScript schema files ├── cache/ # Parsed schema cache └── schema-fields.md # Generated field reference ``` ## Reference ### Complete Field Reference **[references/schema-fields.md](references/schema-fields.md)** - Auto-generated from official Zod schema ## Scripts | Script | Purpose | |--------|---------| | `validate_field.py <path>` | Validate single field | | `validate_config.py <file>` | Validate entire config | | `field_info.py <path>` | Get field details | | `sync_schema.py` | Manage schema sync | | `generate_fields.py` | Regenerate field docs | ## Common Fields ### Agent Configuration - `agents.defaults.model.primary` - Default model ID - `agents.defaults.workspace` - Workspace path - `agents.defaults.timeoutSeconds` - Request timeout - `agents.defaults.sandbox.mode` - Sandbox mode ### Channel Configuration - `channels.telegram.botToken` - Telegram bot token - `channels.discord.token` - Discord bot token - `channels.slack.botToken` - Slack bot token ### Tools - `tools.web.search.enabled` - Enable web search - `tools.web.search.provider` - Search provider - `tools.exec.security` - Execution security mode ## Troubleshooting ### Schema Out of Date If you see warnings about unknown fields that should exist: ```bash # Force schema refresh python3 scripts/sync_schema.py --force ``` ### Validation Errors ```bash # Check field info for correct usage python3 scripts/field_info.py agents.defaults.model # Verify config syntax python3 scripts/validate_config.py ~/.config/openclaw/openclaw.json ```

path: README.md
content: # OpenClaw Config Field Validator

Validate OpenClaw configuration fields against the official Zod schema.

## Features

- ✅ **Version Sync** - Auto-detects local OpenClaw version and syncs schema
- ✅ **Field Validation** - Check if configuration fields exist in schema
- ✅ **Typo Detection** - Suggests corrections for invalid field paths
- ✅ **Config Validation** - Validate entire openclaw.json files
- ✅ **Field Info** - Get detailed field type information and enum values

## Installation

### Method 1: Direct Download

```bash
# Download the skill file to OpenClaw skills directory
curl -L -o ~/.config/openclaw/skills/config-field.skill \
  https://github.com/YOUR_USERNAME/openclaw-config-field/releases/latest/download/config-field.skill

# Restart OpenClaw or reload skills
openclaw skills reload
```

### Method 2: Manual Install

```bash
# Clone the repository
git clone https://github.com/YOUR_USERNAME/openclaw-config-field.git

# Package the skill
cd openclaw-config-field
python3 /path/to/openclaw/skills/skill-creator/scripts/package_skill.py ./config-field

# Copy to skills directory
cp config-field.skill ~/.config/openclaw/skills/
```

## Usage

Once installed, the skill automatically triggers when working with OpenClaw configurations.

### Validate a Field

```bash
python3 ~/.config/openclaw/skills/config-field/scripts/validate_field.py agents.defaults.model.primary
```

### Validate Config File

```bash
python3 ~/.config/openclaw/skills/config-field/scripts/validate_config.py ~/.config/openclaw/openclaw.json
```

### Get Field Information

```bash
python3 ~/.config/openclaw/skills/config-field/scripts/field_info.py logging.level
```

### Check Schema Status

```bash
python3 ~/.config/openclaw/skills/config-field/scripts/sync_schema.py --status
```

### Force Schema Resync

```bash
python3 ~/.config/openclaw/skills/config-field/scripts/sync_schema.py --force
```

## How It Works

1. **Version Detection** - Reads local OpenClaw version from package.json
2. **Schema Sync** - Maintains cached schema in `~/.config/openclaw/skills/config-field/`
3. **Field Generation** - Generates field reference from schema.json
4. **Validation** - Uses cached schema to validate configuration fields

## Supported Fields

The skill includes 136+ field definitions covering:

- `agents` - Agent configurations (defaults, list, sandbox, memory)
- `channels` - Channel configs (telegram, discord, slack, etc.)
- `tools` - Tool settings (web, exec, fs, profile)
- `logging` - Logging configuration
- `session` - Session management
- `commands` - Command settings
- And more...

See [references/schema-fields.md](config-field/references/schema-fields.md) for complete list.

## Development

### Project Structure

```
config-field/
├── SKILL.md                      # Skill definition
├── scripts/
│   ├── validate_field.py         # Single field validation
│   ├── validate_config.py        # Config file validation
│   ├── field_info.py             # Field information
│   ├── sync_schema.py            # Schema synchronization
│   ├── generate_fields.py        # Generate field reference
│   ├── schema_loader.py          # Schema loading utilities
│   └── schema.json               # Built-in field definitions
└── references/
    └── schema-fields.md          # Generated field reference
```

### Building

```bash
# Package the skill
python3 /path/to/openclaw/skills/skill-creator/scripts/package_skill.py ./config-field

# Output: config-field.skill
```

### Testing

```bash
# Test field validation
cd config-field
python3 scripts/validate_field.py agents.defaults.model.primary

# Test config validation
python3 scripts/validate_config.py /path/to/your/openclaw.json
```

## License

MIT License - See [LICENSE](LICENSE) for details.

## Contributing

1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test thoroughly
5. Submit a pull request

## Credits

Built for OpenClaw - The AI agent runtime.


path: references/schema-fields.md
content: # OpenClaw Configuration Schema Fields

Auto-generated from official Zod schema.

## Legend

- **Field**: Configuration field path
- **Type**: Data type
- **Optional**: Whether field is optional (✓) or required (✗)

## Field Reference

| Field | Type | Optional |
|-------|------|----------|
| `$schema` | string | ✓ |
| `agents` | object | ✓ |
| `agents.defaults` | object | ✓ |
| `agents.defaults.contextTokens` | number | ✓ |
| `agents.defaults.elevatedDefault` | enum (off, on, ask, full) | ✓ |
| `agents.defaults.imageMaxDimensionPx` | number | ✓ |
| `agents.defaults.imageModel` | object | ✓ |
| `agents.defaults.imageModel.fallbacks` | array | ✓ |
| `agents.defaults.imageModel.primary` | string | ✓ |
| `agents.defaults.mediaMaxMb` | number | ✓ |
| `agents.defaults.memorySearch` | object | ✓ |
| `agents.defaults.memorySearch.enabled` | boolean | ✓ |
| `agents.defaults.memorySearch.provider` | enum (openai, local, gemini, voyage, mistral) | ✓ |
| `agents.defaults.model` | object | ✓ |
| `agents.defaults.model.fallbacks` | array | ✓ |
| `agents.defaults.model.primary` | string | ✓ |
| `agents.defaults.models` | object | ✓ |
| `agents.defaults.repoRoot` | string | ✓ |
| `agents.defaults.sandbox` | object | ✓ |
| `agents.defaults.sandbox.mode` | enum (off, non-main, all) | ✓ |
| `agents.defaults.sandbox.workspaceAccess` | enum (none, ro, rw) | ✓ |
| `agents.defaults.skipBootstrap` | boolean | ✓ |
| `agents.defaults.thinkingDefault` | enum (off, minimal, low, medium, high, ...) | ✓ |
| `agents.defaults.timeFormat` | enum (auto, 12, 24) | ✓ |
| `agents.defaults.timeoutSeconds` | number | ✓ |
| `agents.defaults.userTimezone` | string | ✓ |
| `agents.defaults.verboseDefault` | enum (off, on, full) | ✓ |
| `agents.defaults.workspace` | string | ✓ |
| `agents.list` | array | ✓ |
| `agents.list.default` | boolean | ✓ |
| `agents.list.id` | string | ✗ |
| `agents.list.model` | string | ✓ |
| `agents.list.name` | string | ✓ |
| `agents.list.skills` | array | ✓ |
| `agents.list.workspace` | string | ✓ |
| `bindings` | array | ✓ |
| `browser` | object | ✓ |
| `browser.defaultProfile` | string | ✓ |
| `browser.enabled` | boolean | ✓ |
| `browser.headless` | boolean | ✓ |
| `channels` | object | ✓ |
| `channels.discord` | object | ✓ |
| `channels.discord.dmPolicy` | enum (allowlist, open, disabled, pairing) | ✓ |
| `channels.discord.enabled` | boolean | ✓ |
| `channels.discord.groupPolicy` | enum (allowlist, open, disabled) | ✓ |
| `channels.discord.historyLimit` | number | ✓ |
| `channels.discord.token` | string | ✓ |
| `channels.imessage` | object | ✓ |
| `channels.irc` | object | ✓ |
| `channels.signal` | object | ✓ |
| `channels.slack` | object | ✓ |
| `channels.slack.appToken` | string | ✓ |
| `channels.slack.botToken` | string | ✓ |
| `channels.slack.enabled` | boolean | ✓ |
| `channels.slack.mode` | enum (socket, http) | ✓ |
| `channels.slack.signingSecret` | string | ✓ |
| `channels.telegram` | object | ✓ |
| `channels.telegram.allowFrom` | array | ✓ |
| `channels.telegram.botToken` | string | ✓ |
| `channels.telegram.defaultTo` | string | ✓ |
| `channels.telegram.dmPolicy` | enum (allowlist, open, disabled, pairing) | ✓ |
| `channels.telegram.enabled` | boolean | ✓ |
| `channels.telegram.groupPolicy` | enum (allowlist, open, disabled) | ✓ |
| `channels.telegram.historyLimit` | number | ✓ |
| `channels.telegram.tokenFile` | string | ✓ |
| `channels.whatsapp` | object | ✓ |
| `commands` | object | ✓ |
| `commands.config` | boolean | ✓ |
| `commands.debug` | boolean | ✓ |
| `commands.native` | boolean | ✓ |
| `commands.nativeSkills` | boolean | ✓ |
| `commands.restart` | boolean | ✓ |
| `cron` | object | ✓ |
| `cron.enabled` | boolean | ✓ |
| `diagnostics` | object | ✓ |
| `diagnostics.enabled` | boolean | ✓ |
| `diagnostics.flags` | array | ✓ |
| `env` | object | ✓ |
| `env.shellEnv` | object | ✓ |
| `env.shellEnv.enabled` | boolean | ✓ |
| `env.shellEnv.timeoutMs` | number | ✓ |
| `env.vars` | object | ✓ |
| `hooks` | object | ✓ |
| `hooks.enabled` | boolean | ✓ |
| `logging` | object | ✓ |
| `logging.consoleLevel` | enum (silent, fatal, error, warn, info, ...) | ✓ |
| `logging.consoleStyle` | enum (pretty, compact, json) | ✓ |
| `logging.file` | string | ✓ |
| `logging.level` | enum (silent, fatal, error, warn, info, ...) | ✓ |
| `logging.maxFileBytes` | number | ✓ |
| `logging.redactPatterns` | array | ✓ |
| `logging.redactSensitive` | enum (off, tools) | ✓ |
| `meta` | object | ✓ |
| `meta.lastTouchedAt` | string | ✓ |
| `meta.lastTouchedVersion` | string | ✓ |
| `models` | object | ✓ |
| `models.mode` | enum (merge, replace) | ✓ |
| `models.providers` | object | ✓ |
| `session` | object | ✓ |
| `session.idleMinutes` | number | ✓ |
| `session.scope` | enum (per-sender, global) | ✓ |
| `tools` | object | ✓ |
| `tools.allow` | array | ✓ |
| `tools.alsoAllow` | array | ✓ |
| `tools.deny` | array | ✓ |
| `tools.exec` | object | ✓ |
| `tools.exec.ask` | enum (off, on-miss, always) | ✓ |
| `tools.exec.host` | enum (sandbox, gateway, node) | ✓ |
| `tools.exec.security` | enum (allowlist, deny, full) | ✓ |
| `tools.exec.timeoutSec` | number | ✓ |
| `tools.fs` | object | ✓ |
| `tools.fs.workspaceOnly` | boolean | ✓ |
| `tools.links` | object | ✓ |
| `tools.media` | object | ✓ |
| `tools.profile` | enum (minimal, coding, messaging, full) | ✓ |
| `tools.sessions` | object | ✓ |
| `tools.web` | object | ✓ |
| `tools.web.fetch` | object | ✓ |
| `tools.web.fetch.enabled` | boolean | ✓ |
| `tools.web.fetch.maxChars` | number | ✓ |
| `tools.web.fetch.timeoutSeconds` | number | ✓ |
| `tools.web.search` | object | ✓ |
| `tools.web.search.apiKey` | string | ✓ |
| `tools.web.search.enabled` | boolean | ✓ |
| `tools.web.search.maxResults` | number | ✓ |
| `tools.web.search.provider` | enum (brave, perplexity, grok) | ✓ |
| `ui` | object | ✓ |
| `ui.seamColor` | string | ✓ |
| `update` | object | ✓ |
| `update.channel` | enum (stable, beta, dev) | ✓ |
| `update.checkOnStart` | boolean | ✓ |
| `web` | object | ✓ |
| `web.enabled` | boolean | ✓ |
| `wizard` | object | ✓ |
| `wizard.lastRunAt` | string | ✓ |
| `wizard.lastRunVersion` | string | ✓ |

## Usage Examples

### Validate a field:
```bash
python3 scripts/validate_field.py agents.defaults.model.primary
```

### Validate config file:
```bash
python3 scripts/validate_config.py ~/.config/openclaw/openclaw.json
```

---

*Generated automatically from OpenClaw Zod Schema*

path: scripts/field_info.py
content: #!/usr/bin/env python3
"""
Get detailed information about a configuration field.
Auto-syncs schema if needed.
"""

import sys
from schema_loader import (
    ensure_schema_synced,
    get_field_info,
    SCHEMA_FIELDS,
    load_schema_json
)


def format_field_info(field_path: str, info: dict) -> str:
    """Format field information for display."""
    if not info:
        return f"Field not found: {field_path}"
    
    lines = []
    lines.append(f"Field: {field_path}")
    lines.append(f"{'=' * 50}")
    
    # Type
    field_type = info.get("type", "unknown")
    lines.append(f"Type: {field_type}")
    
    # Optional/Required
    optional = info.get("optional", True)
    lines.append(f"Required: {'No (optional)' if optional else 'Yes'}")
    
    # Enum values
    if "values" in info and info["values"]:
        lines.append(f"\nAllowed values:")
        for value in info["values"]:
            lines.append(f"  - {value}")
    
    # Parent info for child fields
    if "parent" in info:
        lines.append(f"\nParent: {info['parent']}")
        lines.append("(This field is a child of the above object)")
    
    return "\n".join(lines)


def main():
    if len(sys.argv) < 2:
        print("Usage: python3 field_info.py <field-path>")
        print("Example: python3 field_info.py agents.defaults.model")
        print()
        print("Common field paths:")
        print("  agents.defaults.model.primary")
        print("  agents.defaults.workspace")
        print("  channels.telegram.enabled")
        print("  logging.level")
        print()
        print("This command auto-syncs schema if needed.")
        sys.exit(1)
    
    field_path = sys.argv[1]
    
    # Ensure schema is synced
    if not ensure_schema_synced():
        print("Warning: Could not sync schema, using fallback if available")
    
    # Load fields
    fields = SCHEMA_FIELDS if SCHEMA_FIELDS else load_schema_json()
    
    if not fields:
        print("Error: Schema not loaded. Run: python3 scripts/sync_schema.py")
        sys.exit(1)
    
    info = get_field_info(field_path, fields)
    
    if info:
        print(format_field_info(field_path, info))
    else:
        print(f"Field not found: {field_path}")
        
        # Try to provide context by checking parent paths
        parts = field_path.split('.')
        for i in range(len(parts) - 1, 0, -1):
            parent_path = '.'.join(parts[:i])
            parent_info = get_field_info(parent_path, fields)
            if parent_info:
                print(f"\nParent field '{parent_path}' exists:")
                print(format_field_info(parent_path, parent_info))
                break
        
        sys.exit(1)


if __name__ == "__main__":
    main()


path: scripts/generate_fields.py
content: #!/usr/bin/env python3
"""
Generate schema-fields.md from schema.json.
"""

import json
import sys
from pathlib import Path
from typing import Dict, Any

SCRIPT_DIR = Path(__file__).parent
SCHEMA_FILE = SCRIPT_DIR / "schema.json"
OUTPUT_FILE = SCRIPT_DIR.parent / "references" / "schema-fields.md"


def format_type(field_info: Dict[str, Any]) -> str:
    """Format field type for display."""
    type_name = field_info.get("type", "unknown")
    
    if type_name == "enum":
        values = field_info.get("values", [])
        if values:
            display_vals = ", ".join(values[:5])
            if len(values) > 5:
                display_vals += ", ..."
            return f"enum ({display_vals})"
    
    return type_name


def generate_markdown(fields: Dict[str, Any]) -> str:
    """Generate markdown documentation."""
    lines = []
    
    lines.append("# OpenClaw Configuration Schema Fields")
    lines.append("")
    lines.append("Auto-generated from official Zod schema.")
    lines.append("")
    lines.append("## Legend")
    lines.append("")
    lines.append("- **Field**: Configuration field path")
    lines.append("- **Type**: Data type")
    lines.append("- **Optional**: Whether field is optional (✓) or required (✗)")
    lines.append("")
    lines.append("## Field Reference")
    lines.append("")
    lines.append("| Field | Type | Optional |")
    lines.append("|-------|------|----------|")
    
    # Sort fields by path
    for field_path in sorted(fields.keys()):
        info = fields[field_path]
        type_str = format_type(info)
        optional = "✓" if info.get("optional", True) else "✗"
        lines.append(f"| `{field_path}` | {type_str} | {optional} |")
    
    lines.append("")
    lines.append("## Usage Examples")
    lines.append("")
    lines.append("### Validate a field:")
    lines.append("```bash")
    lines.append("python3 scripts/validate_field.py agents.defaults.model.primary")
    lines.append("```")
    lines.append("")
    lines.append("### Validate config file:")
    lines.append("```bash")
    lines.append("python3 scripts/validate_config.py ~/.config/openclaw/openclaw.json")
    lines.append("```")
    lines.append("")
    lines.append("---")
    lines.append("")
    lines.append("*Generated automatically from OpenClaw Zod Schema*")
    
    return "\n".join(lines)


def main():
    print("Generating field reference from schema...")
    
    if not SCHEMA_FILE.exists():
        print(f"Error: Schema file not found: {SCHEMA_FILE}")
        sys.exit(1)
    
    with open(SCHEMA_FILE, 'r') as f:
        data = json.load(f)
    
    fields = data.get("fields", {})
    print(f"Loaded {len(fields)} field definitions")
    
    markdown = generate_markdown(fields)
    
    OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
    with open(OUTPUT_FILE, 'w') as f:
        f.write(markdown)
    
    print(f"✓ Generated: {OUTPUT_FILE}")


if __name__ == "__main__":
    main()


path: scripts/schema.json
content: {
  "version": "2026.2.22-2",
  "generated_at": "2026-02-24",
  "source": "openclaw zod-schema",
  "fields": {
    "$schema": {"type": "string", "optional": true},
    
    "meta": {"type": "object", "optional": true},
    "meta.lastTouchedVersion": {"type": "string", "optional": true},
    "meta.lastTouchedAt": {"type": "string", "optional": true},
    
    "env": {"type": "object", "optional": true},
    "env.shellEnv": {"type": "object", "optional": true},
    "env.shellEnv.enabled": {"type": "boolean", "optional": true},
    "env.shellEnv.timeoutMs": {"type": "number", "optional": true},
    "env.vars": {"type": "object", "optional": true},
    
    "wizard": {"type": "object", "optional": true},
    "wizard.lastRunAt": {"type": "string", "optional": true},
    "wizard.lastRunVersion": {"type": "string", "optional": true},
    
    "diagnostics": {"type": "object", "optional": true},
    "diagnostics.enabled": {"type": "boolean", "optional": true},
    "diagnostics.flags": {"type": "array", "optional": true},
    
    "logging": {"type": "object", "optional": true},
    "logging.level": {"type": "enum", "optional": true, "values": ["silent", "fatal", "error", "warn", "info", "debug", "trace"]},
    "logging.file": {"type": "string", "optional": true},
    "logging.maxFileBytes": {"type": "number", "optional": true},
    "logging.consoleLevel": {"type": "enum", "optional": true, "values": ["silent", "fatal", "error", "warn", "info", "debug", "trace"]},
    "logging.consoleStyle": {"type": "enum", "optional": true, "values": ["pretty", "compact", "json"]},
    "logging.redactSensitive": {"type": "enum", "optional": true, "values": ["off", "tools"]},
    "logging.redactPatterns": {"type": "array", "optional": true},
    
    "update": {"type": "object", "optional": true},
    "update.channel": {"type": "enum", "optional": true, "values": ["stable", "beta", "dev"]},
    "update.checkOnStart": {"type": "boolean", "optional": true},
    
    "browser": {"type": "object", "optional": true},
    "browser.enabled": {"type": "boolean", "optional": true},
    "browser.headless": {"type": "boolean", "optional": true},
    "browser.defaultProfile": {"type": "string", "optional": true},
    
    "ui": {"type": "object", "optional": true},
    "ui.seamColor": {"type": "string", "optional": true},
    
    "models": {"type": "object", "optional": true},
    "models.mode": {"type": "enum", "optional": true, "values": ["merge", "replace"]},
    "models.providers": {"type": "object", "optional": true},
    
    "agents": {"type": "object", "optional": true},
    "agents.defaults": {"type": "object", "optional": true},
    "agents.defaults.model": {"type": "object", "optional": true},
    "agents.defaults.model.primary": {"type": "string", "optional": true},
    "agents.defaults.model.fallbacks": {"type": "array", "optional": true},
    "agents.defaults.imageModel": {"type": "object", "optional": true},
    "agents.defaults.imageModel.primary": {"type": "string", "optional": true},
    "agents.defaults.imageModel.fallbacks": {"type": "array", "optional": true},
    "agents.defaults.models": {"type": "object", "optional": true},
    "agents.defaults.workspace": {"type": "string", "optional": true},
    "agents.defaults.repoRoot": {"type": "string", "optional": true},
    "agents.defaults.skipBootstrap": {"type": "boolean", "optional": true},
    "agents.defaults.contextTokens": {"type": "number", "optional": true},
    "agents.defaults.userTimezone": {"type": "string", "optional": true},
    "agents.defaults.timeFormat": {"type": "enum", "optional": true, "values": ["auto", "12", "24"]},
    "agents.defaults.timeoutSeconds": {"type": "number", "optional": true},
    "agents.defaults.mediaMaxMb": {"type": "number", "optional": true},
    "agents.defaults.imageMaxDimensionPx": {"type": "number", "optional": true},
    "agents.defaults.thinkingDefault": {"type": "enum", "optional": true, "values": ["off", "minimal", "low", "medium", "high", "xhigh"]},
    "agents.defaults.verboseDefault": {"type": "enum", "optional": true, "values": ["off", "on", "full"]},
    "agents.defaults.elevatedDefault": {"type": "enum", "optional": true, "values": ["off", "on", "ask", "full"]},
    "agents.defaults.sandbox": {"type": "object", "optional": true},
    "agents.defaults.sandbox.mode": {"type": "enum", "optional": true, "values": ["off", "non-main", "all"]},
    "agents.defaults.sandbox.workspaceAccess": {"type": "enum", "optional": true, "values": ["none", "ro", "rw"]},
    "agents.defaults.memorySearch": {"type": "object", "optional": true},
    "agents.defaults.memorySearch.enabled": {"type": "boolean", "optional": true},
    "agents.defaults.memorySearch.provider": {"type": "enum", "optional": true, "values": ["openai", "local", "gemini", "voyage", "mistral"]},
    
    "agents.list": {"type": "array", "optional": true},
    "agents.list.id": {"type": "string", "optional": false},
    "agents.list.name": {"type": "string", "optional": true},
    "agents.list.default": {"type": "boolean", "optional": true},
    "agents.list.workspace": {"type": "string", "optional": true},
    "agents.list.model": {"type": "string", "optional": true},
    "agents.list.skills": {"type": "array", "optional": true},
    
    "tools": {"type": "object", "optional": true},
    "tools.web": {"type": "object", "optional": true},
    "tools.web.search": {"type": "object", "optional": true},
    "tools.web.search.enabled": {"type": "boolean", "optional": true},
    "tools.web.search.provider": {"type": "enum", "optional": true, "values": ["brave", "perplexity", "grok"]},
    "tools.web.search.apiKey": {"type": "string", "optional": true},
    "tools.web.search.maxResults": {"type": "number", "optional": true},
    "tools.web.fetch": {"type": "object", "optional": true},
    "tools.web.fetch.enabled": {"type": "boolean", "optional": true},
    "tools.web.fetch.maxChars": {"type": "number", "optional": true},
    "tools.web.fetch.timeoutSeconds": {"type": "number", "optional": true},
    "tools.media": {"type": "object", "optional": true},
    "tools.links": {"type": "object", "optional": true},
    "tools.sessions": {"type": "object", "optional": true},
    "tools.exec": {"type": "object", "optional": true},
    "tools.exec.host": {"type": "enum", "optional": true, "values": ["sandbox", "gateway", "node"]},
    "tools.exec.security": {"type": "enum", "optional": true, "values": ["allowlist", "deny", "full"]},
    "tools.exec.ask": {"type": "enum", "optional": true, "values": ["off", "on-miss", "always"]},
    "tools.exec.timeoutSec": {"type": "number", "optional": true},
    "tools.fs": {"type": "object", "optional": true},
    "tools.fs.workspaceOnly": {"type": "boolean", "optional": true},
    "tools.profile": {"type": "enum", "optional": true, "values": ["minimal", "coding", "messaging", "full"]},
    "tools.allow": {"type": "array", "optional": true},
    "tools.deny": {"type": "array", "optional": true},
    "tools.alsoAllow": {"type": "array", "optional": true},
    
    "channels": {"type": "object", "optional": true},
    "channels.telegram": {"type": "object", "optional": true},
    "channels.telegram.enabled": {"type": "boolean", "optional": true},
    "channels.telegram.botToken": {"type": "string", "optional": true},
    "channels.telegram.tokenFile": {"type": "string", "optional": true},
    "channels.telegram.dmPolicy": {"type": "enum", "optional": true, "values": ["allowlist", "open", "disabled", "pairing"]},
    "channels.telegram.groupPolicy": {"type": "enum", "optional": true, "values": ["allowlist", "open", "disabled"]},
    "channels.telegram.allowFrom": {"type": "array", "optional": true},
    "channels.telegram.defaultTo": {"type": "string", "optional": true},
    "channels.telegram.historyLimit": {"type": "number", "optional": true},
    
    "channels.discord": {"type": "object", "optional": true},
    "channels.discord.enabled": {"type": "boolean", "optional": true},
    "channels.discord.token": {"type": "string", "optional": true},
    "channels.discord.dmPolicy": {"type": "enum", "optional": true, "values": ["allowlist", "open", "disabled", "pairing"]},
    "channels.discord.groupPolicy": {"type": "enum", "optional": true, "values": ["allowlist", "open", "disabled"]},
    "channels.discord.historyLimit": {"type": "number", "optional": true},
    
    "channels.slack": {"type": "object", "optional": true},
    "channels.slack.enabled": {"type": "boolean", "optional": true},
    "channels.slack.botToken": {"type": "string", "optional": true},
    "channels.slack.appToken": {"type": "string", "optional": true},
    "channels.slack.signingSecret": {"type": "string", "optional": true},
    "channels.slack.mode": {"type": "enum", "optional": true, "values": ["socket", "http"]},
    
    "channels.whatsapp": {"type": "object", "optional": true},
    "channels.irc": {"type": "object", "optional": true},
    "channels.signal": {"type": "object", "optional": true},
    "channels.imessage": {"type": "object", "optional": true},
    
    "bindings": {"type": "array", "optional": true},
    
    "commands": {"type": "object", "optional": true},
    "commands.native": {"type": "boolean", "optional": true},
    "commands.nativeSkills": {"type": "boolean", "optional": true},
    "commands.config": {"type": "boolean", "optional": true},
    "commands.debug": {"type": "boolean", "optional": true},
    "commands.restart": {"type": "boolean", "optional": true},
    
    "session": {"type": "object", "optional": true},
    "session.scope": {"type": "enum", "optional": true, "values": ["per-sender", "global"]},
    "session.idleMinutes": {"type": "number", "optional": true},
    
    "cron": {"type": "object", "optional": true},
    "cron.enabled": {"type": "boolean", "optional": true},
    
    "hooks": {"type": "object", "optional": true},
    "hooks.enabled": {"type": "boolean", "optional": true},
    
    "web": {"type": "object", "optional": true},
    "web.enabled": {"type": "boolean", "optional": true}
  }
}


path: scripts/schema_loader.py
content: #!/usr/bin/env python3
"""
Shared schema loading utilities.
"""

import json
import sys
from pathlib import Path
from typing import Dict, Any, Optional, List

# Paths
CACHE_DIR = Path.home() / ".config" / "openclaw" / "skills" / "config-field"
CACHE_SCHEMA_FILE = CACHE_DIR / "schema.json"
BUILTIN_SCHEMA_FILE = Path(__file__).parent / "schema.json"


def load_schema_json() -> Dict[str, Any]:
    """Load schema from JSON file."""
    # Try cached schema first
    if CACHE_SCHEMA_FILE.exists():
        try:
            with open(CACHE_SCHEMA_FILE, 'r') as f:
                data = json.load(f)
                return data.get("fields", {})
        except (json.JSONDecodeError, IOError):
            pass
    
    # Fall back to built-in schema
    if BUILTIN_SCHEMA_FILE.exists():
        try:
            with open(BUILTIN_SCHEMA_FILE, 'r') as f:
                data = json.load(f)
                return data.get("fields", {})
        except (json.JSONDecodeError, IOError):
            pass
    
    return {}


def save_schema_json(fields: Dict[str, Any], version: str):
    """Save schema to cache."""
    CACHE_DIR.mkdir(parents=True, exist_ok=True)
    data = {
        "version": version,
        "generated_at": "auto",
        "source": "openclaw zod-schema",
        "fields": fields
    }
    with open(CACHE_SCHEMA_FILE, 'w') as f:
        json.dump(data, f, indent=2)


def get_field_info(field_path: str, fields: Dict[str, Any]) -> Optional[Dict[str, Any]]:
    """Get information about a specific field."""
    # Try exact match first
    if field_path in fields:
        return fields[field_path]
    
    # Try matching parent paths
    parts = field_path.split('.')
    for i in range(len(parts) - 1, 0, -1):
        parent_path = '.'.join(parts[:i])
        if parent_path in fields:
            parent_info = fields[parent_path]
            # Check if parent is an object that could contain this field
            if parent_info.get('type') == 'object':
                # Field might be valid but not documented individually
                return {
                    "type": "unknown (child of object)",
                    "optional": True,
                    "parent": parent_path,
                }
    
    return None


def find_similar_fields(field_path: str, fields: Dict[str, Any], max_results: int = 3) -> List[str]:
    """Find similar field paths for typo suggestions."""
    from difflib import get_close_matches
    
    all_paths = list(fields.keys())
    
    # Get close matches
    matches = get_close_matches(field_path, all_paths, n=max_results, cutoff=0.6)
    
    # Also try matching just the last part
    parts = field_path.split('.')
    if len(parts) > 1:
        last_part = parts[-1]
        for path in all_paths:
            if path.endswith(f".{last_part}") and path not in matches:
                matches.append(path)
                if len(matches) >= max_results:
                    break
    
    return matches[:max_results]


def get_schema_version() -> Optional[str]:
    """Get the version of the cached schema."""
    if CACHE_SCHEMA_FILE.exists():
        try:
            with open(CACHE_SCHEMA_FILE, 'r') as f:
                data = json.load(f)
                return data.get("version")
        except (json.JSONDecodeError, IOError):
            pass
    return None


def ensure_schema_synced() -> bool:
    """Ensure schema is synced before validation."""
    # Load current schema
    fields = load_schema_json()
    
    if not fields:
        print("Schema not found. Please run: python3 scripts/sync_schema.py")
        return False
    
    return True


# Load fields on module import
SCHEMA_FIELDS = load_schema_json()


path: scripts/sync_schema.py
content: #!/usr/bin/env python3
"""
Synchronize OpenClaw schema to match local version.
Generates schema.json from local OpenClaw installation.
"""

import os
import sys
import json
import subprocess
from pathlib import Path
from typing import Optional

# Cache directory for schema storage
CACHE_DIR = Path.home() / ".config" / "openclaw" / "skills" / "config-field"
CACHE_SCHEMA_FILE = CACHE_DIR / "schema.json"
BUILTIN_SCHEMA_FILE = Path(__file__).parent / "schema.json"
VERSION_FILE = CACHE_DIR / "version.json"


def get_openclaw_version() -> Optional[str]:
    """Get the currently installed OpenClaw version."""
    # Try command line first
    try:
        result = subprocess.run(
            ["openclaw", "--version"],
            capture_output=True,
            text=True,
            timeout=10
        )
        if result.returncode == 0:
            output = result.stdout.strip()
            parts = output.split()
            for part in parts:
                if "/" in part and not part.startswith("node-"):
                    return part.split("/")[1]
    except (subprocess.SubprocessError, FileNotFoundError):
        pass
    
    # Fallback: read from package.json
    try:
        package_paths = [
            Path.home() / ".npm-global" / "lib" / "node_modules" / "openclaw" / "package.json",
            Path("/usr") / "lib" / "node_modules" / "openclaw" / "package.json",
        ]
        for package_path in package_paths:
            if package_path.exists():
                with open(package_path, 'r') as f:
                    data = json.load(f)
                    return data.get("version")
    except (json.JSONDecodeError, IOError):
        pass
    
    return None


def get_cached_version() -> Optional[str]:
    """Get the version of cached schema."""
    if VERSION_FILE.exists():
        try:
            with open(VERSION_FILE, 'r') as f:
                data = json.load(f)
                return data.get("version")
        except (json.JSONDecodeError, IOError):
            return None
    
    # Also check schema.json
    if CACHE_SCHEMA_FILE.exists():
        try:
            with open(CACHE_SCHEMA_FILE, 'r') as f:
                data = json.load(f)
                return data.get("version")
        except (json.JSONDecodeError, IOError):
            pass
    
    return None


def save_version_info(version: str):
    """Save version info to cache."""
    CACHE_DIR.mkdir(parents=True, exist_ok=True)
    info = {
        "version": version,
        "source": "local"
    }
    with open(VERSION_FILE, 'w') as f:
        json.dump(info, f, indent=2)


def copy_builtin_schema() -> bool:
    """Copy built-in schema to cache."""
    if not BUILTIN_SCHEMA_FILE.exists():
        print(f"Built-in schema not found: {BUILTIN_SCHEMA_FILE}")
        return False
    
    try:
        CACHE_DIR.mkdir(parents=True, exist_ok=True)
        
        with open(BUILTIN_SCHEMA_FILE, 'r') as f:
            data = json.load(f)
        
        with open(CACHE_SCHEMA_FILE, 'w') as f:
            json.dump(data, f, indent=2)
        
        return True
    except Exception as e:
        print(f"Error copying schema: {e}")
        return False


def sync_schema(force: bool = False) -> bool:
    """Synchronize schema with local OpenClaw version."""
    local_version = get_openclaw_version()
    
    if not local_version:
        print("Error: Could not determine OpenClaw version")
        print("Make sure OpenClaw is installed")
        return False
    
    print(f"Local OpenClaw version: {local_version}")
    
    cached_version = get_cached_version()
    
    if cached_version:
        print(f"Cached schema version: {cached_version}")
    else:
        print("No cached schema found")
    
    # Check if sync is needed
    if not force and cached_version == local_version:
        print("✓ Schema is up to date")
        return True
    
    if force:
        print("Force sync requested...")
    else:
        print(f"Version mismatch detected, syncing...")
    
    # For now, copy built-in schema
    # In the future, this could extract schema from local OpenClaw installation
    if copy_builtin_schema():
        save_version_info(local_version)
        print(f"✓ Schema synchronized to version {local_version}")
        return True
    else:
        print("✗ Failed to sync schema")
        return False


def main():
    import argparse
    parser = argparse.ArgumentParser(description="Sync OpenClaw schema")
    parser.add_argument("--force", "-f", action="store_true", help="Force re-sync")
    parser.add_argument("--status", "-s", action="store_true", help="Show status only")
    args = parser.parse_args()
    
    if args.status:
        local_version = get_openclaw_version()
        cached_version = get_cached_version()
        
        print("Schema Sync Status")
        print("=" * 40)
        print(f"OpenClaw version: {local_version or 'unknown'}")
        print(f"Cached version:   {cached_version or 'none'}")
        
        if local_version and cached_version:
            if local_version == cached_version:
                print("Status: ✓ In sync")
            else:
                print("Status: ✗ Out of sync (run sync_schema.py)")
        elif local_version:
            print("Status: ✗ No cached schema (run sync_schema.py)")
        else:
            print("Status: ✗ Cannot determine versions")
        
        return
    
    success = sync_schema(force=args.force)
    sys.exit(0 if success else 1)


if __name__ == "__main__":
    main()


path: scripts/validate_config.py
content: #!/usr/bin/env python3
"""
Validate an entire OpenClaw configuration file against the Zod schema.
Auto-syncs schema if needed.
"""

import sys
import json
import re
from pathlib import Path
from schema_loader import (
    ensure_schema_synced,
    get_field_info,
    find_similar_fields,
    SCHEMA_FIELDS,
    load_schema_json
)


def extract_field_paths(obj: any, prefix: str = "") -> list:
    """Extract all field paths from a configuration object."""
    paths = []
    
    if isinstance(obj, dict):
        for key, value in obj.items():
            # Skip special keys
            if key.startswith("$"):
                continue
            
            path = f"{prefix}.{key}" if prefix else key
            paths.append(path)
            
            if isinstance(value, (dict, list)):
                paths.extend(extract_field_paths(value, path))
    
    elif isinstance(obj, list):
        for i, item in enumerate(obj):
            if isinstance(item, (dict, list)):
                paths.extend(extract_field_paths(item, prefix))
    
    return paths


def validate_config_file(config_path: str) -> dict:
    """Validate a configuration file."""
    config_file = Path(config_path)
    
    if not config_file.exists():
        return {"error": f"File not found: {config_path}"}
    
    try:
        with open(config_file, 'r') as f:
            content = f.read()
        
        # Try to parse as JSON/JSON5
        try:
            config = json.loads(content)
        except json.JSONDecodeError:
            # Try JSON5-like parsing (allow comments and trailing commas)
            # Remove comments
            content = re.sub(r'//.*?$', '', content, flags=re.MULTILINE)
            content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL)
            # Remove trailing commas
            content = re.sub(r',(\s*[}\]])', r'\1', content)
            config = json.loads(content)
    
    except Exception as e:
        return {"error": f"Failed to parse config: {e}"}
    
    # Load schema fields
    fields = SCHEMA_FIELDS if SCHEMA_FIELDS else load_schema_json()
    
    if not fields:
        return {"error": "Schema not loaded. Run: python3 scripts/sync_schema.py"}
    
    # Extract all field paths
    field_paths = extract_field_paths(config)
    
    # Validate each path
    valid_fields = []
    invalid_fields = []
    warnings = []
    
    for path in field_paths:
        info = get_field_info(path, fields)
        if info:
            valid_fields.append({
                "path": path,
                "type": info.get("type", "unknown"),
            })
        else:
            suggestions = find_similar_fields(path, fields)
            invalid_fields.append({
                "path": path,
                "suggestions": suggestions,
            })
    
    return {
        "valid_fields": valid_fields,
        "invalid_fields": invalid_fields,
        "warnings": warnings,
        "total": len(field_paths),
    }


def print_validation_report(result: dict, config_path: str):
    """Print a formatted validation report."""
    if "error" in result:
        print(f"Error: {result['error']}")
        return
    
    print(f"Validating: {config_path}")
    print(f"Total fields: {result['total']}")
    print()
    
    if result["valid_fields"]:
        print("✓ Valid fields:")
        for field in sorted(result["valid_fields"], key=lambda x: x["path"]):
            print(f"  - {field['path']} ({field['type']})")
        print()
    
    if result["invalid_fields"]:
        print("✗ Invalid fields:")
        for field in sorted(result["invalid_fields"], key=lambda x: x["path"]):
            print(f"  - {field['path']}")
            if field["suggestions"]:
                print(f"    Suggestion: {field['suggestions'][0]}")
        print()
    
    if result["warnings"]:
        print("⚠ Warnings:")
        for warning in result["warnings"]:
            print(f"  - {warning}")
        print()
    
    # Summary
    valid_count = len(result["valid_fields"])
    invalid_count = len(result["invalid_fields"])
    
    print("=" * 50)
    if invalid_count == 0:
        print(f"✓ All {valid_count} fields are valid!")
    else:
        print(f"✗ Found {invalid_count} invalid field(s) out of {result['total']}")
        print(f"  Valid: {valid_count}")
        print(f"  Invalid: {invalid_count}")


def main():
    if len(sys.argv) < 2:
        print("Usage: python3 validate_config.py <config-file>")
        print("Example: python3 validate_config.py ~/.config/openclaw/openclaw.json")
        print()
        print("This command auto-syncs schema if needed.")
        sys.exit(1)
    
    config_path = sys.argv[1]
    
    # Ensure schema is synced
    if not ensure_schema_synced():
        print("Warning: Could not sync schema, using fallback if available")
    
    result = validate_config_file(config_path)
    print_validation_report(result, config_path)
    
    # Exit with error code if invalid fields found
    if result.get("invalid_fields"):
        sys.exit(1)


if __name__ == "__main__":
    main()


path: scripts/validate_field.py
content: #!/usr/bin/env python3
"""
Validate a single field path against the OpenClaw Zod schema.
Auto-syncs schema if needed.
"""

import sys
from schema_loader import (
    ensure_schema_synced,
    get_field_info,
    find_similar_fields,
    SCHEMA_FIELDS,
    load_schema_json
)


def validate_field(field_path: str) -> dict:
    """Validate a field path and return result."""
    # Ensure schema is loaded
    fields = SCHEMA_FIELDS if SCHEMA_FIELDS else load_schema_fields()
    
    if not fields:
        return {
            "valid": False,
            "error": "Schema not loaded. Run: python3 scripts/sync_schema.py",
        }
    
    info = get_field_info(field_path, fields)
    
    if info:
        return {
            "valid": True,
            "path": field_path,
            "type": info.get("type", "unknown"),
            "optional": info.get("optional", True),
            "values": info.get("values", None),
        }
    else:
        suggestions = find_similar_fields(field_path, fields)
        return {
            "valid": False,
            "path": field_path,
            "suggestions": suggestions,
        }


def main():
    if len(sys.argv) < 2:
        print("Usage: python3 validate_field.py <field-path>")
        print("Example: python3 validate_field.py agents.defaults.model.primary")
        print()
        print("This command auto-syncs schema if needed.")
        sys.exit(1)
    
    field_path = sys.argv[1]
    
    # Ensure schema is synced
    if not ensure_schema_synced():
        print("Warning: Could not sync schema, using fallback if available")
    
    result = validate_field(field_path)
    
    if "error" in result:
        print(f"Error: {result['error']}")
        sys.exit(1)
    
    if result["valid"]:
        type_str = result["type"]
        if result.get("values"):
            values_display = ", ".join(result["values"][:5])
            if len(result["values"]) > 5:
                values_display += ", ..."
            type_str += f" ({values_display})"
        opt_str = "optional" if result["optional"] else "required"
        print(f"✓ Field exists: {result['path']}")
        print(f"  Type: {type_str}")
        print(f"  Required: {opt_str}")
    else:
        print(f"✗ Field not found: {result['path']}")
        if result["suggestions"]:
            print(f"\nDid you mean:")
            for suggestion in result["suggestions"]:
                print(f"  - {suggestion}")
        sys.exit(1)


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)