koala73/worldmonitor · error · Error
Cloudflare rules changed after planning. Run --plan again be
Error message
Cloudflare rules changed after planning. Run --plan again before applying.
What it means
During --apply, runAgentReadiness re-reads each phase's ruleset right before writing and compares it deep-strictly against the ruleset snapshot taken during planning. If the rules changed between plan and apply, the plan is stale, so it throws instead of applying blind edits. This is an optimistic-concurrency guard against lost updates.
Solutions
- Re-run the script with --plan to take a fresh snapshot, then run --apply again.
- Coordinate with whoever changed the rules and ensure no concurrent automation is touching the phase during apply.
- If changes keep racing, serialize applies through a single pipeline or lock.
Defensive patterns
Strategy: retry
Validate before calling
// re-plan immediately before apply and bail if the plan already differs from the last snapshot
const fresh = planAgentReadiness(await readFirewall(token));
if (JSON.stringify(fresh) !== JSON.stringify(lastPlan)) {
console.error('plan is stale; re-run --plan');
process.exit(2);
} Try / catch
try {
await runAgentReadiness('--apply', { env, fetchImpl });
} catch (e) {
if (e.message.includes('changed after planning')) {
console.error('Concurrent Cloudflare edit detected; re-running plan/apply once.');
await runAgentReadiness('--plan', { env, fetchImpl });
await runAgentReadiness('--apply', { env, fetchImpl });
return;
}
throw e;
} Prevention
- Freeze manual dashboard edits during automated apply windows.
- Run applies from a single serialized pipeline.
- Keep plan→apply latency short (run them back-to-back).
- Subscribe to Cloudflare audit/change notifications for the zone.
When it happens
Trigger: Another operator or automation modifies any rule in the phase between the initial plan read and the apply-time re-read, so the freshly fetched rules array differs from the planned snapshot.
Common situations: A teammate editing firewall rules in the Cloudflare dashboard while the script runs, Terraform/CI concurrently reconciling rules, or two apply runs racing.
Understand the failure class
Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.
Related errors
- COMPANY_MONITORING_ADMISSION_EVIDENCE_MISSING
- DNS ${recordType} lookup failed: HTTP ${response.status}
- Sign in to view your brief.
- Authenticated account changed during push setup
- Dashboard is no longer available.
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/a9050e041d9007d5.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/cloudflare-agent-readiness.mjs:57
}
}
return changes;
}
export async function runAgentReadiness(mode, { env = process.env, fetchImpl } = {}) {
if (!['--plan', '--check', '--apply'].includes(mode)) throw new Error('Use --plan, --check, or --apply');
const token = resolveToken(env);
const zoneId = await resolveZoneId(token, { env, fetchImpl });
const read = (phase) => cloudflareRequest(`/zones/${zoneId}/rulesets/phases/${phase}/entrypoint`, { token, fetchImpl });
const firewall = await read(FIREWALL_PHASE);
const changes = planAgentReadiness(firewall);
if (mode !== '--apply' || changes.length === 0) return { zone: 'worldmonitor.app', ready: changes.length === 0, changes };
for (const change of changes) {
const current = await read(change.phase);
if (!isDeepStrictEqual(current.rules, firewall.rules)) {
throw new Error('Cloudflare rules changed after planning. Run --plan again before applying.');
}
const updated = await cloudflareRequest(`/zones/${zoneId}/rulesets/${change.rulesetId}/rules/${change.ruleId}`, {
token, fetchImpl, method: change.method, body: change.body,
});
firewall.rules = updated.rules;
}
const remaining = planAgentReadiness(await read(FIREWALL_PHASE));
if (remaining.length) throw new Error('Cloudflare verification failed after apply');
return { zone: 'worldmonitor.app', ready: true, applied: changes.map((change) => change.description) };
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const mode = process.argv[2];
if (process.argv.length !== 3 || !['--plan', '--check', '--apply'].includes(mode)) {
console.error('Usage: node scripts/cloudflare-agent-readiness.mjs --plan|--check|--apply');
process.exitCode = 1;
} else {
loadEnvFile(import.meta.url, { only: ['CLOUDFLARE_API_TOKEN', 'CLOUDFLARE_ALL_ACCESS_TOKEN', 'CLOUDFLARE_ZONE_ID'] });View on GitHub (pinned to 7d06c8633d)