| name | config-migrator |
| description | Help with config schema changes and migrations in ClosedClaw. Use when updating config types, syncing Zod schemas, creating migrations, or handling breaking config changes. Covers type definitions, validation, and legacy migration patterns. |
Config Migrator
This skill helps you safely modify ClosedClaw's configuration schema, sync type definitions with Zod validators, and create migration paths for breaking changes.
When to Use
- Adding new config fields
- Modifying existing config structure
- Creating breaking config changes
- Syncing TypeScript types with Zod schemas
- Writing migration logic for legacy configs
- Updating config documentation
Prerequisites
- Understanding of TypeScript types and Zod validation
- Familiarity with JSON5 syntax
- Knowledge of
src/config/ structure
Configuration Architecture
File Structure
src/config/
โโโ config.ts # Main exports
โโโ io.ts # Config loading/saving
โโโ types.ts # Base config types
โโโ types.agents.ts # Agent-specific config
โโโ types.channels.ts # Channel-specific config
โโโ types.gateway.ts # Gateway-specific config
โโโ types.models.ts # Model provider config
โโโ zod-schema.ts # Zod validation schemas
โโโ legacy-migrate.ts # Migration logic
โโโ defaults.ts # Default value application
โโโ validation.ts # Config validation
โโโ env-substitution.ts # Environment variable handling
โโโ includes.ts # Config file includes
Config Flow
User edits config.json5
โ
loadConfig() reads file (io.ts)
โ
resolveConfigIncludes() handles $include (includes.ts)
โ
resolveConfigEnvVars() substitutes ${ENV_VAR} (env-substitution.ts)
โ
migrateLegacyConfig() updates old formats (legacy-migrate.ts)
โ
validateConfigObject() checks against Zod (validation.ts)
โ
applyDefaults() fills missing values (defaults.ts)
โ
Config ready for use
Common Tasks
Task 1: Add New Config Field
Step 1: Update TypeScript Types
export type AgentConfig = {
newFeature?: {
enabled: boolean;
threshold?: number;
mode?: "fast" | "accurate";
};
};
Step 2: Update Zod Schema
const agentConfigSchema = z.object({
newFeature: z
.object({
enabled: z.boolean(),
threshold: z.number().min(0).max(100).optional(),
mode: z.enum(["fast", "accurate"]).optional(),
})
.optional(),
});
Step 3: Add Default Values
export function applyAgentDefaults(config: ClosedClawConfig): void {
if (config.agents?.main?.newFeature === undefined) {
config.agents.main.newFeature = {
enabled: true,
threshold: 50,
mode: "fast",
};
}
}
Step 4: Document in Config
// Example in config.json5 comments or docs
{
agents: {
main: {
// New feature configuration
newFeature: {
enabled: true, // Enable new feature
threshold: 50, // Threshold value (0-100)
mode: "fast", // Mode: "fast" or "accurate"
},
},
},
}
Task 2: Create Breaking Change Migration
Step 1: Identify Breaking Change
Example: Renaming oldField to newField
Step 2: Add Migration Logic
export function migrateLegacyConfig(config: unknown): {
config: unknown;
issues: LegacyConfigIssue[];
} {
const issues: LegacyConfigIssue[] = [];
if (isObject(config) && isObject(config.agents)) {
const agents = config.agents as Record<string, unknown>;
for (const [agentId, agentConfig] of Object.entries(agents)) {
if (isObject(agentConfig) && "oldField" in agentConfig) {
const value = agentConfig.oldField;
delete agentConfig.oldField;
agentConfig.newField = value;
issues.push({
level: "warning",
message: `Agent "${agentId}": renamed "oldField" to "newField"`,
fix: "automatic",
path: ["agents", agentId, "oldField"],
});
}
}
}
return { config, issues };
}
Step 3: Update Version Detection
export function detectConfigVersion(config: unknown): string {
if (isObject(config) && isObject(config.agents)) {
const agents = config.agents as Record<string, unknown>;
if (Object.values(agents).some((a) => isObject(a) && "oldField" in a)) {
return "2025.12.0";
}
}
return "latest";
}
Step 4: Test Migration
describe("migrateLegacyConfig", () => {
it("migrates oldField to newField", () => {
const oldConfig = {
agents: {
main: {
oldField: "value",
},
},
};
const { config, issues } = migrateLegacyConfig(oldConfig);
expect(config).toMatchObject({
agents: {
main: {
newField: "value",
},
},
});
expect(issues).toHaveLength(1);
expect(issues[0].message).toMatch(/renamed.*oldField.*newField/);
});
});
Task 3: Sync Types with Zod Schema
The most common mistake is updating types but not Zod schemas, or vice versa.
Workflow
-
Update TypeScript type first:
export type AgentConfig = {
newField: string;
};
-
Update Zod schema immediately:
const agentConfigSchema = z.object({
newField: z.string(),
});
-
Run tests to catch mismatches:
pnpm test -- src/config/
-
Use type guards for runtime safety:
function isValidAgentConfig(value: unknown): value is AgentConfig {
return agentConfigSchema.safeParse(value).success;
}
Task 4: Handle Environment Variables
Config supports ${ENV_VAR} substitution:
// config.json5
{
telegram: {
botToken: "${TELEGRAM_BOT_TOKEN}",
},
}
Error Handling
resolveConfigEnvVars(config);
try {
const config = loadConfig();
} catch (error) {
if (error instanceof MissingEnvVarError) {
console.error(`Missing env var: ${error.varName}`);
}
}
Task 5: Config Includes
Support for splitting config across files:
// config.json5
{
$include: ["./agents.json5", "./channels.json5"],
gateway: {
/* ... */
},
}
Error Handling
try {
const config = resolveConfigIncludes(baseConfig, configPath);
} catch (error) {
if (error instanceof CircularIncludeError) {
console.error("Circular include detected:", error.cycle);
}
}
Schema Design Patterns
Optional vs Required Fields
type Config = {
requiredField: string;
};
const schema = z.object({
requiredField: z.string(),
});
type Config = {
optionalField?: string;
};
const schema = z.object({
optionalField: z.string().optional(),
optionalField: z.optional(z.string()),
});
Enums and Unions
type Mode = "fast" | "accurate" | "balanced";
const modeSchema = z.enum(["fast", "accurate", "balanced"]);
type Result = { success: true; data: string } | { success: false; error: string };
const resultSchema = z.discriminatedUnion("success", [
z.object({ success: z.literal(true), data: z.string() }),
z.object({ success: z.literal(false), error: z.string() }),
]);
Nested Objects
type Config = {
feature: {
enabled: boolean;
options: {
timeout: number;
retries: number;
};
};
};
const configSchema = z.object({
feature: z.object({
enabled: z.boolean(),
options: z.object({
timeout: z.number().positive(),
retries: z.number().min(0).max(5),
}),
}),
});
Arrays and Records
type Config = {
tags: string[];
};
const schema = z.object({
tags: z.array(z.string()),
});
type Config = {
agents: Record<string, AgentConfig>;
};
const schema = z.object({
agents: z.record(z.string(), agentConfigSchema),
});
Validation Patterns
Custom Validation
const schema = z.object({
port: z.number().min(1024, "Port must be >= 1024").max(65535, "Port must be <= 65535"),
url: z
.string()
.url("Must be valid URL")
.refine((url) => url.startsWith("https://"), "Must use HTTPS"),
password: z
.string()
.min(8, "Password must be at least 8 characters")
.regex(/[A-Z]/, "Must contain uppercase letter")
.regex(/[0-9]/, "Must contain number"),
});
Conditional Validation
const schema = z
.object({
enabled: z.boolean(),
apiKey: z.string().optional(),
})
.refine((data) => !data.enabled || data.apiKey !== undefined, {
message: "apiKey required when enabled is true",
path: ["apiKey"],
});
Transform and Coerce
const schema = z.object({
port: z.coerce.number(),
});
const schema = z.object({
tags: z.string().transform((s) => s.split(",")),
});
Testing Strategies
Unit Test Pattern
import { describe, it, expect } from "vitest";
import { ClosedClawSchema } from "./zod-schema.js";
describe("Agent config schema", () => {
it("validates valid config", () => {
const config = {
agents: {
main: {
model: "claude-opus-4",
thinking: "high",
},
},
};
const result = ClosedClawSchema.safeParse(config);
expect(result.success).toBe(true);
});
it("rejects invalid config", () => {
const config = {
agents: {
main: {
model: 123,
},
},
};
const result = ClosedClawSchema.safeParse(config);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].path).toEqual(["agents", "main", "model"]);
}
});
it("applies defaults", () => {
const config = { agents: { main: {} } };
const validated = ClosedClawSchema.parse(config);
applyAgentDefaults(validated);
expect(validated.agents.main.thinking).toBe("medium");
});
});
Migration Test Pattern
describe("Config migration", () => {
it("migrates v1 to v2", () => {
const v1Config = {
oldStructure: "value",
};
const { config, issues } = migrateLegacyConfig(v1Config);
expect(config).toMatchObject({
newStructure: "value",
});
expect(issues).toContainEqual(
expect.objectContaining({
level: "warning",
message: expect.stringMatching(/migrated/i),
}),
);
});
});
Diagnostic Commands
closedclaw doctor
node --import tsx -e "
import { loadConfig } from './src/config/config.js';
try {
const config = loadConfig();
console.log('โ Config valid');
} catch (error) {
console.error('โ Config invalid:', error);
}
"
closedclaw doctor | grep -i "unknown"
node --import tsx scripts/test-migration.ts
cp ~/.closedclaw/config.json5 ~/.closedclaw/config.backup.json5
Common Pitfalls
Pitfall 1: Type/Schema Mismatch
Problem: Types updated but Zod not, or vice versa
Detection:
pnpm test -- src/config/
pnpm build
Prevention: Always update both in same commit
Pitfall 2: Breaking Changes Without Migration
Problem: Old configs break after update
Prevention: Always add migration in legacy-migrate.ts
Pitfall 3: Missing Defaults
Problem: Optional fields undefined at runtime
Prevention: Add defaults in defaults.ts for all optional fields
Pitfall 4: Strict Validation Too Strict
Problem: Users can't add experimental fields
Solution: Document that unknown keys will fail, or add passthrough():
const schema = z
.object({
})
.passthrough();
Checklist
Related Files
src/config/types.*.ts - TypeScript type definitions
src/config/zod-schema.ts - Zod validation schemas
src/config/legacy-migrate.ts - Migration logic
src/config/defaults.ts - Default value application
src/config/validation.ts - Validation orchestration
src/config/io.ts - Config loading/saving
docs/configuration.md - User-facing config docs