BigPizzaV3/CodexPlusPlus · error · anyhow::Error

invalid user script key

Error message

invalid user script key

What it means

After stripping the user: prefix in delete_user_script, the remainder is treated as a file name inside the user scripts directory. It must not contain '/' or '\' and must not be "." or ".."; violations bail with "invalid user script key". This is the traversal guard that keeps the delete operation inside user_dir even for adversarial keys, complementing the later canonicalization check.

Source

Thrown at crates/codex-plus-core/src/user_scripts.rs:118

        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()
                )
            })?;
        if path.exists() {
            let canonical_path = path
                .canonicalize()

View on GitHub (pinned to 1f431ae49b)

Solutions

  1. Build keys as "user:" + bare file name only — subdirectories under user_dir are not supported by delete
  2. Sanitize keys at the boundary where they enter your system (reject '/' and '\\')
  3. If scripts must live in subfolders, flatten the layout; that structure is unsupported by design

Example fix

// before
let key = format!("user:{dir}/{file}");
manager.delete_user_script(&key)?;

// after
let key = format!("user:{file}"); // flat layout only
manager.delete_user_script(&key)?;
Defensive patterns

Strategy: type-guard

Validate before calling

if !is_safe_user_script_key(&key) {
    anyhow::bail!("refusing unsafe user script key: {key}");
}
let config = manager.delete_user_script(&key)?;

Type guard

fn is_safe_user_script_key(key: &str) -> bool {
    let Some(name) = key.strip_prefix("user:") else {
        return false;
    };
    !name.is_empty()
        && !name.contains('/')
        && !name.contains('\\')
        && name != "."
        && name != ".."
}

Prevention

When it happens

Trigger: delete_user_script("user:../config.json") or delete_user_script("user:scripts/foo.js") — any key whose post-prefix part contains a path separator or is a dot component.

Common situations: UI bugs concatenating a directory and file name into the key; imported/migrated configs holding path-like keys; hostile automation or tampered config files trying to escape the scripts directory.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@1f431ae49b (2026-08-16). Data as JSON: /api/errors/74814a78330eb451. Report an issue: GitHub.