farion1231/cc-switch · error
Invalid backup id: {backup_id}
Error message
Invalid backup id: {backup_id} What it means
Path-traversal guard in backup_path_for_id: a backup id is rejected if it contains '..', '/', '\\', or is blank after trimming. Backup ids are used to build a path under the backup directory for restore/delete operations, so the check prevents crafted ids from escaping that directory.
Source
Thrown at src-tauri/src/services/skill.rs:3465
}
entries.sort_by_key(|(_, modified)| *modified);
let remove_count = entries.len().saturating_sub(SKILL_BACKUP_RETAIN_COUNT);
for (path, _) in entries.into_iter().take(remove_count) {
fs::remove_dir_all(&path)?;
}
Ok(())
}
fn backup_path_for_id(backup_id: &str) -> Result<PathBuf> {
if backup_id.contains("..")
|| backup_id.contains('/')
|| backup_id.contains('\\')
|| backup_id.trim().is_empty()
{
return Err(anyhow!("Invalid backup id: {backup_id}"));
}
Ok(Self::get_backup_dir()?.join(backup_id))
}
fn read_backup_metadata(backup_path: &Path) -> Result<SkillBackupMetadata> {
let metadata_path = backup_path.join("meta.json");
let content = fs::read_to_string(&metadata_path)
.with_context(|| format!("failed to read {}", metadata_path.display()))?;
serde_json::from_str(&content)
.with_context(|| format!("failed to parse {}", metadata_path.display()))
}
fn create_uninstall_backup(skill: &InstalledSkill) -> Result<Option<PathBuf>> {
Self::create_uninstall_backup_excluding(skill, None)
}
fn create_uninstall_backup_excluding(View on GitHub (pinned to a2e22f3302)
Solutions
- Pass the id exactly as returned by the backup-listing API (SkillBackupEntry.backup_id) — do not build it from parts
- Trim the input before calling restore/delete
- If you maintain a caller, validate ids against the listing result set rather than trusting free-text input
Example fix
// before
let path = backup_path_for_id(&format!("{user_input}"))?;
// after — only accept ids that exist in the listing
let ids: HashSet<_> = list_backups()?.iter().map(|b| b.backup_id.clone()).collect();
if !ids.contains(user_input.trim()) { return Err(anyhow!("unknown backup id")); }
let path = backup_path_for_id(user_input.trim())?; Defensive patterns
Strategy: type-guard
Validate before calling
// Rust — accept only ids present in the current backup listing
let valid: HashSet<&str> = backups.iter().map(|b| b.backup_id.as_str()).collect();
if !valid.contains(candidate.trim()) {
return Err(anyhow!("unknown backup id"));
} Type guard
// Rust predicate mirroring the guard
fn is_valid_backup_id(id: &str) -> bool {
!id.trim().is_empty() && !id.contains("..") && !id.contains('/') && !id.contains('\\')
} Try / catch
match backup_path_for_id(&id) {
Err(e) if e.to_string().contains("Invalid backup id") => {
// re-fetch the listing and pass an id from it verbatim; never sanitize-and-retry with a mutated id
}
other => other,
} Prevention
- Always source backup ids from the list-backups API response, never construct them from dates or names
- Trim whitespace before calling restore/delete
- Treat crafted ids from external input as path-traversal attempts and reject loudly
When it happens
Trigger: Restore/delete-by-id called with a stale, garbled, or hand-constructed id: UI passing an id with surrounding whitespace, an id concatenated from user text, or a scripted/deeplink call supplying '../../something'.
Common situations: Frontend passing the wrong field (e.g. skill name or timestamp instead of backup_id); ids copied from logs with line breaks; automated tooling constructing ids from dates.
Related errors
- Invalid skill directory (possible path traversal): {director
- Unsupported URL scheme
- INVALID_SKILL_DIRECTORY
- INVALID_REPO_REF
- OPENCLAW_ENV_EMPTY
AI-assisted analysis of farion1231/cc-switch@a2e22f3302 (2026-08-16).
Data as JSON: /api/errors/4694e21433881d76.
Report an issue: GitHub.