zeroclaw-labs/zeroclaw · error · anyhow::Error
Skill content is empty
Error message
Skill content is empty
What it means
Thrown by validate_skill_content (crates/zeroclaw-runtime/src/skills/improver.rs) when the skill content handed to SkillImprover::improve_skill is empty or whitespace-only. The improver refuses to write blank content because an atomic rename of empty text would erase the existing SKILL.md. It fires on the incoming improved_content argument, and again on the temp file after write as a self-check.
Source
Thrown at crates/zeroclaw-runtime/src/skills/improver.rs:136
md_path.display()
)
})?;
self.cooldowns.insert(slug.to_string(), Instant::now());
Ok(Some(slug.to_string()))
}
fn skills_dir(&self) -> PathBuf {
self.workspace_dir.join("skills")
}
}
/// Validate skill content: must be non-empty, have a parseable YAML front-matter
/// block with a non-empty `name` field.
pub fn validate_skill_content(content: &str) -> Result<()> {
if content.trim().is_empty() {
bail!("Skill content is empty");
}
let Some((front, _body)) = split_front_matter(content) else {
bail!("Skill content is missing YAML front-matter (expected `---` delimited block at top)");
};
let name = front_matter_value(&front, "name").unwrap_or_default();
if name.trim().is_empty() {
bail!("Skill front-matter missing required `name` field");
}
Ok(())
}
/// Split a SKILL.md into (front_matter_text, body_text).
/// Returns `None` if the file doesn't start with `---\n` or has no closing
/// `---` delimiter.
fn split_front_matter(content: &str) -> Option<(String, String)> {View on GitHub (pinned to 88bb9c8533)
Solutions
- Check improved_content.trim().is_empty() before calling improve_skill; skip or re-request the generation when empty
- If content comes from an LLM, retry the generation and require a non-empty completion before improving
- Log the slug and the raw provider response so empty generations are visible in telemetry
- If the message is prefixed 'Validation failed after write', the temp-file self-check failed: inspect what wrote .SKILL.md.tmp in your workspace skills dir
Example fix
// before
improver.improve_skill(slug, improved_content, reason).await?; // fails on empty LLM output
// after
if improved_content.trim().is_empty() {
tracing::warn!(slug, "empty improvement content; skipping");
return Ok(None);
}
improver.improve_skill(slug, improved_content, reason).await?; Defensive patterns
Strategy: validation
Validate before calling
// Before calling improve_skill:
if improved_content.trim().is_empty() {
tracing::warn!(slug, "empty improvement content; skipping");
return Ok(None);
}
improver.improve_skill(slug, improved_content, reason).await?; Type guard
fn has_skill_body(content: &str) -> bool {
!content.trim().is_empty()
} Prevention
- Never pass provider output to improve_skill unchecked; require a non-empty completion
- Treat empty LLM generations as a skip, not an error, in improvement loops
- Log slug + content length before every improvement so blanks are traceable
When it happens
Trigger: Calling improve_skill(slug, "", reason) or with content containing only whitespace/newlines. Passing through an LLM completion that came back empty (truncation, content filter, mis-parsed streaming chunk) without checking it.
Common situations: Self-improvement pipelines where the provider returns an empty message; an upstream bug that reads the wrong file into improved_content; a generation step that returns Ok(String::new()) on soft failure.
Related errors
- Skill content is missing YAML front-matter (expected `---` d
- Skill front-matter missing required `name` field
- Skill source must be a directory: {}
- invalid --skill name '{$skill}': use a bare skill name (lett
- URL cannot be empty
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/d8f626f19db855ad.
Report an issue: GitHub.