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

  1. Only pass keys that came from the user-scripts listing (they carry the user: prefix)
  2. Filter keys before calling: skip anything that does not strip to a non-empty user: remainder
  3. 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

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.