BigPizzaV3/CodexPlusPlus · error · anyhow::Error
only user scripts can be deleted
Error message
only user scripts can be deleted
What it means
UserScriptManager::delete_user_script only manages user-provided scripts. The key must start with "user:" and have a non-empty remainder; the prefix is stripped and the remainder is used as a file name inside the user scripts directory. Keys without the prefix (built-in or market scripts) or the literal "user:" are rejected with "only user scripts can be deleted" before any filesystem access happens.
Source
Thrown at crates/codex-plus-core/src/user_scripts.rs:115
pub fn set_global_enabled(&self, enabled: bool) -> anyhow::Result<UserScriptConfig> {
let _guard = self.config_lock.lock().unwrap();
let mut config = self.load_config_unlocked();
config.enabled = enabled;
self.save_config_unlocked(&config)?;
Ok(config)
}
pub fn set_script_enabled(&self, key: &str, enabled: bool) -> anyhow::Result<UserScriptConfig> {
let _guard = self.config_lock.lock().unwrap();
let mut config = self.load_config_unlocked();
config.scripts.insert(key.to_string(), enabled);
self.save_config_unlocked(&config)?;
Ok(config)
}
pub fn delete_user_script(&self, key: &str) -> anyhow::Result<UserScriptConfig> {
let Some(file_name) = key.strip_prefix("user:").filter(|value| !value.is_empty()) else {
anyhow::bail!("only user scripts can be deleted");
};
if file_name.contains(['/', '\\']) || file_name == "." || file_name == ".." {
anyhow::bail!("invalid user script key");
}
let path = self.user_dir.join(file_name);
let canonical_user_dir = self
.user_dir
.canonicalize()
.or_else(|_| {
fs::create_dir_all(&self.user_dir)?;
self.user_dir.canonicalize()
})
.with_context(|| {
format!(
"failed to resolve user script directory {}",
self.user_dir.display()
)
})?;View on GitHub (pinned to 1f431ae49b)
Solutions
- Only pass keys that came from the user-scripts listing (they carry the user: prefix)
- Filter keys before calling: skip anything that does not strip to a non-empty user: remainder
- Ensure the UI never offers an empty user script id (the literal "user:" case)
Example fix
// before
manager.delete_user_script(&selected_key)?;
// after
if selected_key.strip_prefix("user:").is_some_and(|rest| !rest.is_empty()) {
manager.delete_user_script(&selected_key)?;
} else {
log::warn!("refusing to delete non-user script {selected_key}");
} Defensive patterns
Strategy: type-guard
Validate before calling
if !is_deletable_user_script_key(&key) {
anyhow::bail!("refusing to delete non-user script key: {key}");
}
let config = manager.delete_user_script(&key)?; Type guard
fn is_deletable_user_script_key(key: &str) -> bool {
key.strip_prefix("user:").is_some_and(|rest| !rest.is_empty())
} Prevention
- Only feed delete_user_script with keys obtained from the user-scripts listing
- Keep builtin, market, and user keys in distinct UI collections so origins are unambiguous
- Never synthesize keys by string concatenation across script kinds
When it happens
Trigger: delete_user_script("builtin:translate"), delete_user_script("some-market-script"), or delete_user_script("user:") — passing a key taken from the builtin/market listing instead of the user-scripts listing, or an empty user script id.
Common situations: A UI passing the selected script's key from a mixed list without checking its origin; configs migrated between sections so keys lose their prefix; cleanup automation iterating over ALL script keys including built-ins.
Related errors
AI-assisted analysis of BigPizzaV3/CodexPlusPlus@1f431ae49b (2026-08-16).
Data as JSON: /api/errors/c13cc8873a8c8266.
Report an issue: GitHub.