Install
openclaw skills install openclaw-json-editingAdvanced JSON editing for OpenClaw configuration files, tools, and data structures. Handles JSON5 configs, schema validation, merge patching, env var substitution, and type-safe modifications.
openclaw skills install openclaw-json-editingExpert guidance for editing JSON in the OpenClaw ecosystem. OpenClaw uses JSON5 for configuration (allows comments, trailing commas), has sophisticated config merging, and validates with Zod schemas.
| Task | Command/Pattern |
|---|---|
| Validate config | openclaw config validate |
| Apply config patch | openclaw config patch <file.json> |
| Safe JSON parse | Use safeParseJson() wrapper |
| Check config location | openclaw config path |
| Pretty print | JSON.stringify(data, null, 2) |
OpenClaw config files use JSON5 (not strict JSON):
{
// Single-line comments are allowed
"gateway": {
"mode": "http", // Trailing commas are allowed
},
/* Multi-line comments
are also supported */
"agents": {
"main": {
"model": "anthropic/claude-opus-4-6",
},
},
}
//) and multi-line (/* */){ key: "value" } is valid'string' is valid| Type | Path |
|---|---|
| User config | ~/.openclaw/config.json |
| Project config | ./openclaw.config.json |
| Agent config | ~/.openclaw/agents/<id>/config.json |
| Session store | ~/.openclaw/sessions/ |
| State dir | ~/.openclaw/ (or $OPENCLAW_STATE_DIR) |
OpenClaw uses JSON5.parse() for configs and safe wrappers:
// OpenClaw's safeParseJson pattern
function safeParseJson<T>(raw: string): T | null {
try {
return JSON.parse(raw) as T;
} catch {
return null;
}
}
// For OpenClaw configs, use JSON5
import JSON5 from "json5";
function loadConfigFile(path: string): unknown {
try {
const raw = fs.readFileSync(path, "utf8");
return JSON5.parse(raw); // Allows comments, trailing commas
} catch {
return undefined;
}
}
OpenClaw writes with specific formatting and permissions:
function saveJsonFile(pathname: string, data: unknown) {
const dir = path.dirname(pathname);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
}
// 2-space indentation, trailing newline
fs.writeFileSync(pathname, `${JSON.stringify(data, null, 2)}\n`, "utf8");
fs.chmodSync(pathname, 0o600); // User read/write only
}
Always validate before assuming structure:
// OpenClaw's isPlainObject (strictest)
function isPlainObject(value: unknown): value is Record<string, unknown> {
return (
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
Object.prototype.toString.call(value) === "[object Object]"
);
}
// Less strict version
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
OpenClaw uses merge patching for config updates:
// Apply a merge patch to base config
function applyMergePatch(base: unknown, patch: unknown): unknown {
if (!isPlainObject(patch)) {
return patch;
}
const result: Record<string, unknown> = isPlainObject(base) ? { ...base } : {};
for (const [key, value] of Object.entries(patch)) {
if (value === null) {
delete result[key]; // null = delete key
continue;
}
if (isPlainObject(value)) {
const baseValue = result[key];
result[key] = applyMergePatch(
isPlainObject(baseValue) ? baseValue : {},
value
);
continue;
}
result[key] = value;
}
return result;
}
// Add/update nested field
const patch = {
agents: {
main: {
model: "anthropic/claude-opus-4-6"
}
}
};
// Delete a field (set to null)
const deletePatch = {
agents: {
main: {
temperature: null // Removes temperature
}
}
};
// Replace entire section
const replacePatch = {
channels: {
telegram: null, // Delete old
discord: { token: "new-token" } // Add new
}
};
OpenClaw configs support ${VAR} and ${VAR:-default} syntax:
{
"auth": {
"profiles": {
"openai": {
"apiKey": "${OPENAI_API_KEY}" // Substituted at load time
},
"anthropic": {
"apiKey": "${ANTHROPIC_API_KEY:-fallback-key}"
}
}
}
}
// Check if string contains env var reference
function containsEnvVarReference(value: string): boolean {
return /\$\{[^}]+\}/.test(value);
}
// Collect all env var paths in an object
function collectEnvRefPaths(
value: unknown,
path: string,
output: Map<string, string>
): void {
if (typeof value === "string") {
if (containsEnvVarReference(value)) {
output.set(path, value);
}
return;
}
if (Array.isArray(value)) {
value.forEach((item, index) => {
collectEnvRefPaths(item, `${path}[${index}]`, output);
});
return;
}
if (isPlainObject(value)) {
for (const [key, child] of Object.entries(value)) {
const childPath = path ? `${path}.${key}` : key;
collectEnvRefPaths(child, childPath, output);
}
}
}
OpenClaw uses Zod for runtime validation:
import { z } from "zod";
// Define schema
const AgentConfigSchema = z.object({
model: z.string().optional(),
temperature: z.number().min(0).max(2).optional(),
maxTokens: z.number().positive().optional(),
enabled: z.boolean().default(true),
});
// Validate
type AgentConfig = z.infer<typeof AgentConfigSchema>;
function validateConfig(data: unknown): AgentConfig {
return AgentConfigSchema.parse(data);
}
// Safe validation
function safeValidateConfig(data: unknown): AgentConfig | null {
const result = AgentConfigSchema.safeParse(data);
return result.success ? result.data : null;
}
// Model reference: "provider/model-name"
const ModelRefSchema = z.string().regex(/^[a-z0-9-]+\/[a-z0-9-]+$/i);
// Channel ID
const ChannelIdSchema = z.enum([
"telegram", "discord", "slack", "whatsapp",
"signal", "imessage", "irc", "web"
]);
// Duration string: "30s", "5m", "1h"
const DurationSchema = z.string().regex(/^\d+[smhd]$/);
OpenClaw supports config file includes:
{
"include": [
"./base-config.json",
"~/.openclaw/shared-channels.json"
],
"agents": {
// Local overrides
}
}
# Pretty print OpenClaw config
jq . ~/.openclaw/config.json
# Get gateway mode
jq '.gateway.mode' ~/.openclaw/config.json
# List all agent IDs
jq '.agents | keys[]' ~/.openclaw/config.json
# Find agent using specific model
jq '.agents | to_entries[] | select(.value.model == "anthropic/claude-opus-4-6") | .key' ~/.openclaw/config.json
# Get all channel types
jq '.channels | keys[]' ~/.openclaw/config.json
# Check if Telegram is configured
jq '.channels.telegram != null' ~/.openclaw/config.json
# Extract all model references
jq '.. | objects | select(has("model")) | .model' ~/.openclaw/config.json
# Merge patch using jq
jq '.agents.main.model = "anthropic/claude-opus-4-6"' ~/.openclaw/config.json > tmp.json \
&& mv tmp.json ~/.openclaw/config.json
# Deep search for all API keys (for audit)
jq '.. | objects | .apiKey? // .token? // .password? | select(.)' ~/.openclaw/config.json
# Collect all environment variable references
jq -r '.. | strings | select(contains("${"))' ~/.openclaw/config.json
# Validate JSON structure (returns true/false)
jq 'if has("gateway") and has("agents") then true else false end' ~/.openclaw/config.json
# Create minimal config from full config
jq '{ gateway: .gateway, agents: { main: .agents.main } }' ~/.openclaw/config.json
{
"gateway": {
"mode": "http", // "http", "disabled", "process"
"http": {
"bind": "127.0.0.1",
"port": 3000,
},
"auth": {
"token": "${OPENCLAW_GATEWAY_TOKEN}",
},
},
}
{
"agents": {
"main": {
"model": "anthropic/claude-opus-4-6",
"temperature": 0.7,
"maxTokens": 4096,
// System prompt or reference to file
"systemPrompt": "You are a helpful assistant.",
"systemPromptFile": "~/.openclaw/agents/main/prompt.md",
},
"coder": {
"model": "anthropic/claude-sonnet-4-5",
"temperature": 0.2,
// Inherit from main with overrides
"inherits": "main",
},
},
}
{
"channels": {
"telegram": {
"botToken": "${TELEGRAM_BOT_TOKEN}",
"allowFrom": ["@username"],
},
"discord": {
"botToken": "${DISCORD_BOT_TOKEN}",
"applicationId": "123456789",
},
"slack": {
"botToken": "${SLACK_BOT_TOKEN}",
"appToken": "${SLACK_APP_TOKEN}",
},
},
}
{
"tools": {
"alsoAllow": ["web_search", "browser"],
"deny": ["exec"],
"config": {
"web_search": {
"provider": "brave",
"apiKey": "${BRAVE_API_KEY}",
},
},
},
}
// Schema validation errors provide detailed paths
const result = schema.safeParse(data);
if (!result.success) {
for (const error of result.error.errors) {
console.log(`${error.path.join('.')}: ${error.message}`);
// e.g., "agents.main.temperature: Number must be less than or equal to 2"
}
}
# If config is corrupted, OpenClaw keeps backups
ls -la ~/.openclaw/config.json.*
# Restore from backup
cp ~/.openclaw/config.json.2024-01-15T10-30-00.bak ~/.openclaw/config.json
# Or use OpenClaw's built-in rotation
openclaw config restore
# Validate config syntax and schema
openclaw config validate
# Test config loading
openclaw config get
# Create timestamped backup
cp ~/.openclaw/config.json ~/.openclaw/config.json.$(date +%Y%m%d_%H%M%S).bak
// Never assume structure - always validate
if (!isPlainObject(config.agents)) {
throw new Error("Invalid agents configuration");
}
// Preserve env var references when editing
const originalValue = "${API_KEY}";
const newValue = process.env.API_KEY || originalValue;
// Preferred for deep cloning
deepCopy = structuredClone(original);
// Fallback for older environments
deepCopy = JSON.parse(JSON.stringify(original));
// Write to temp file, then rename
fs.writeFileSync(tempPath, data);
fs.renameSync(tempPath, finalPath);
0o600 (user read/write only)${ENV_VAR} substitutionpath.resolve() and check traversalconfig-audit.jsonl| Issue | Cause | Solution |
|---|---|---|
Unexpected token / | Comments in JSON | Use JSON5 parser |
Trailing comma | Trailing comma in array | Use JSON5 parser |
Env var not substituted | Missing env var | Check ${VAR:-default} |
Validation failed | Schema mismatch | Run openclaw config validate |
Permission denied | Wrong file permissions | chmod 600 config.json |
# Check raw config (before env substitution)
cat ~/.openclaw/config.json
# Check effective config (after all processing)
openclaw config get --json
# List all env var references
openclaw config env-refs
# Trace config loading
OPENCLAW_DEBUG=config openclaw config get
When adding or updating AI providers in openclaw.config.json, you must discover actual model names from the provider's API and handle reasoning model variants correctly.
# 1. Fetch available models from provider API
# xAI example - requires XAI_API_KEY
XAI_API_KEY="your-key"
curl -s -H "Authorization: Bearer $XAI_API_KEY" \
https://api.x.ai/v1/models | jq '.data[] | {id: .id, name: .object}'
# OpenAI example
curl -s -H "Authorization: Bearer $OPENAI_API_KEY" \
https://api.openai.com/v1/models | jq '.data[] | select(.id | contains("gpt")) | .id'
# Together AI example
curl -s -H "Authorization: Bearer $TOGETHER_API_KEY" \
https://api.together.xyz/v1/models | jq '.[] | {id: .id, name: .display_name}'
OpenClaw uses ModelProviderConfig schema:
type ModelProviderConfig = {
baseUrl: string; // API endpoint base URL
apiKey?: string; // Optional: API key (prefer env vars)
auth?: "api-key" | "aws-sdk" | "oauth" | "token";
api?: "openai-completions" | "openai-responses" |
"anthropic-messages" | "google-generative-ai" |
"github-copilot" | "bedrock-converse-stream" | "ollama";
headers?: Record<string, string>; // Custom headers
models: ModelDefinitionConfig[]; // Model definitions
};
type ModelDefinitionConfig = {
id: string; // Model ID (e.g., "grok-4")
name: string; // Display name (e.g., "Grok 4")
api?: ModelApi; // Override API type per model
reasoning: boolean; // Whether model supports reasoning/thinking
input: Array<"text" | "image">; // Supported input types
cost: {
input: number; // Cost per 1M input tokens
output: number; // Cost per 1M output tokens
cacheRead: number; // Cost per 1M cached tokens read
cacheWrite: number; // Cost per 1M cached tokens written
};
contextWindow: number; // Max context window size
maxTokens: number; // Max output tokens
headers?: Record<string, string>;
compat?: ModelCompatConfig;
};
CRITICAL: Some models have reasoning variants handled specially by OpenClaw. For example, xAI's grok-4-1-fast has three variants:
| Model ID | Type | Notes |
|---|---|---|
grok-4-1-fast | Base | The "family" identifier |
grok-4-1-fast-reasoning | Reasoning | Full reasoning capabilities |
grok-4-1-fast-non-reasoning | Non-reasoning | Faster, no reasoning |
In OpenClaw, you typically configure ONLY the base model (grok-4-1-fast). The system automatically switches between reasoning/non-reasoning variants based on the thinking directive or configuration.
{
"models": {
"providers": {
"xai": {
"baseUrl": "https://api.x.ai/v1",
"api": "openai-completions",
"apiKey": "${XAI_API_KEY}",
"models": [
{
"id": "grok-4-1-fast",
"name": "Grok 4.1 Fast",
"reasoning": false, // Base model is non-reasoning
"input": ["text"],
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
"contextWindow": 128000,
"maxTokens": 8192
}
// NOTE: Do NOT add -reasoning or -non-reasoning variants separately
// OpenClaw handles these automatically via model family resolution
]
}
}
}
}
OpenClaw internally defines reasoning model families in src/agents/model-families.ts:
const REASONING_MODEL_FAMILIES = [
{
provider: "xai",
members: [
"grok-4-1-fast",
"grok-4-1-fast-reasoning",
"grok-4-1-fast-non-reasoning"
],
reasoningModel: "grok-4-1-fast-reasoning",
nonReasoningModel: "grok-4-1-fast-non-reasoning",
},
];
When a user requests a model with thinking: "on" or thinking: "off", OpenClaw:
thinking: "on" → uses reasoningModel variantthinking: "off" → uses nonReasoningModel variant{
"models": {
"mode": "merge", // "merge" or "replace"
"providers": {
// xAI - Grok models with reasoning variants
"xai": {
"baseUrl": "https://api.x.ai/v1",
"api": "openai-completions",
"apiKey": "${XAI_API_KEY}",
"models": [
{
"id": "grok-4-1-fast",
"name": "Grok 4.1 Fast",
"reasoning": false,
"input": ["text"],
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
"contextWindow": 128000,
"maxTokens": 8192
},
{
"id": "grok-4",
"name": "Grok 4",
"reasoning": false,
"input": ["text", "image"], // Vision-capable
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
"contextWindow": 128000,
"maxTokens": 8192,
"compat": {
"supportsReasoningEffort": false,
"maxTokensField": "max_completion_tokens"
}
}
]
},
// OpenAI - with response API and reasoning
"openai": {
"baseUrl": "https://api.openai.com/v1",
"api": "openai-responses",
"apiKey": "${OPENAI_API_KEY}",
"models": [
{
"id": "gpt-5.2",
"name": "GPT-5.2",
"reasoning": false,
"input": ["text", "image"],
"cost": { "input": 2.5, "output": 10, "cacheRead": 0.5, "cacheWrite": 1.25 },
"contextWindow": 200000,
"maxTokens": 16384,
"compat": {
"supportsReasoningEffort": true,
"thinkingFormat": "openai"
}
},
{
"id": "o3-mini",
"name": "o3 Mini",
"reasoning": true, // Built-in reasoning model
"input": ["text", "image"],
"cost": { "input": 1.1, "output": 4.4, "cacheRead": 0.275, "cacheWrite": 0.55 },
"contextWindow": 200000,
"maxTokens": 100000,
"compat": {
"supportsReasoningEffort": true,
"requiresAssistantAfterToolResult": true
}
}
]
},
// Anthropic - Messages API
"anthropic": {
"baseUrl": "https://api.anthropic.com",
"api": "anthropic-messages",
"apiKey": "${ANTHROPIC_API_KEY}",
"models": [
{
"id": "claude-opus-4-6",
"name": "Claude Opus 4.6",
"reasoning": false,
"input": ["text", "image"],
"cost": { "input": 15, "output": 75, "cacheRead": 1.88, "cacheWrite": 7.5 },
"contextWindow": 200000,
"maxTokens": 8192,
"compat": {
"supportsStore": false,
"supportsDeveloperRole": false
}
}
]
},
// Google Gemini
"google": {
"baseUrl": "https://generativelanguage.googleapis.com/v1beta",
"api": "google-generative-ai",
"apiKey": "${GEMINI_API_KEY}",
"models": [
{
"id": "gemini-3-pro-preview",
"name": "Gemini 3 Pro Preview",
"reasoning": false,
"input": ["text", "image"],
"cost": { "input": 1.25, "output": 10, "cacheRead": 0.31, "cacheWrite": 1.25 },
"contextWindow": 1000000,
"maxTokens": 8192,
"compat": {
"thinkingFormat": "qwen"
}
}
]
},
// Ollama - local models (auto-discovered)
"ollama": {
"baseUrl": "http://localhost:11434/v1",
"api": "ollama",
"models": [] // Auto-populated from /api/tags
}
}
}
}
type ModelCompatConfig = {
// OpenAI-specific features
supportsStore?: boolean; // Use 'store' parameter
supportsDeveloperRole?: boolean; // Use 'developer' vs 'system' role
supportsReasoningEffort?: boolean; // Support reasoning_effort param
supportsUsageInStreaming?: boolean; // Usage in streaming responses
supportsStrictMode?: boolean; // Strict tool mode
// Token handling
maxTokensField?: "max_completion_tokens" | "max_tokens";
// Thinking/reasoning format
thinkingFormat?: "openai" | "zai" | "qwen";
// Tool calling quirks
requiresToolResultName?: boolean; // Must include tool result name
requiresAssistantAfterToolResult?: boolean; // Assistant message after tool
requiresThinkingAsText?: boolean; // Thinking blocks as text
requiresMistralToolIds?: boolean; // Mistral-style tool IDs
};
# Validate the full config including models
openclaw config validate
# Check if models.json is correctly generated
openclaw models list
# Test a specific model provider
openclaw models test --provider xai --model grok-4-1-fast
# Debug model resolution
OPENCLAW_DEBUG=models openclaw models list
| Pitfall | Why It Happens | Solution |
|---|---|---|
Adding -reasoning variants | Don't manually add reasoning variants | Only add base model (e.g., grok-4-1-fast) |
Wrong reasoning boolean | Confusion about model capabilities | Set based on base model, not variants |
Missing api field | Defaults may not match provider | Explicitly set api to correct value |
| Hardcoded API keys | Security risk | Always use ${ENV_VAR} substitution |
| Wrong baseUrl | Provider-specific endpoints | Check provider documentation |
| Incorrect cost values | Tracking/budgeting issues | Verify per-provider pricing |
openai-completions APIopenai-responses for o-series and GPT-5openai-completions for legacy GPT-4supportsReasoningEffortanthropic-messages APIgoogle-generative-ai APIapi: "ollama" for native discovery/api/tagsDefine aliases for common models in agent defaults:
{
"agents": {
"defaults": {
"models": {
"fast": { "alias": "Grok Fast", "id": "xai/grok-4-1-fast" },
"smart": { "alias": "Claude Opus", "id": "anthropic/claude-opus-4-6" },
"vision": { "alias": "GPT Vision", "id": "openai/gpt-5.2" }
}
}
}
}
Use aliases in agent config:
{
"agents": {
"main": {
"model": "fast" // Resolves to xai/grok-4-1-fast
}
}
}