BigPizzaV3/CodexPlusPlus · error · anyhow::Error
refusing to delete script outside user script directory
Error message
refusing to delete script outside user script directory
What it means
The final filesystem guard in delete_user_script: if the target exists, both the file and the user scripts directory are canonicalized (symlinks resolved), and the file's canonical path must start_with the canonical directory's path; otherwise it bails with "refusing to delete script outside user script directory". This defeats symlink attacks — a link placed inside user_dir but pointing elsewhere must not cause deletion of the link target.
Source
Thrown at crates/codex-plus-core/src/user_scripts.rs:139
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()
.with_context(|| format!("failed to resolve user script {}", path.display()))?;
if !canonical_path.starts_with(&canonical_user_dir) {
anyhow::bail!("refusing to delete script outside user script directory");
}
fs::remove_file(&canonical_path).with_context(|| {
format!("failed to delete user script {}", canonical_path.display())
})?;
}
let _guard = self.config_lock.lock().unwrap();
let mut config = self.load_config_unlocked();
config.scripts.remove(key);
config.market.remove(key);
self.save_config_unlocked(&config)?;
Ok(config)
}
pub fn user_script_path_for_market_id(&self, id: &str) -> PathBuf {
self.user_dir.join(market_script_filename(id))
}
View on GitHub (pinned to 1f431ae49b)
Solutions
- Replace symlinks in the user scripts directory with real copies of the scripts
- If deleting the symlink itself is the intent, remove it manually (rm <user_dir>/x.sh) — the library deliberately refuses
- Audit how the symlink got there if you did not create it; treat it as a possible tampering signal
Example fix
# instead of a symlink inside the scripts dir
cp /path/to/shared/tool.js ~/.codex-plus/user-scripts/tool.js
# then the manager delete works:
# manager.delete_user_script("user:tool.js") Defensive patterns
Strategy: try-catch
Validate before calling
let name = key.strip_prefix("user:").unwrap_or(key);
let path = user_dir.join(name);
if path.is_symlink() {
anyhow::bail!("refusing to operate on symlinked script: {}", path.display());
}
let config = manager.delete_user_script(&key)?; Try / catch
match manager.delete_user_script(&key) {
Ok(config) => { /* done */ }
Err(err) if err.to_string().contains("outside user script directory") => {
// security signal: possible symlink/tampering — report, do NOT retry or delete manually from app code
}
Err(err) => return Err(err),
} Prevention
- Never place symlinks inside the user scripts directory; copy scripts instead
- Run script creation and deletion under the same trust boundary to narrow TOCTOU windows
- Treat this specific error as a tampering indicator, not a transient failure
When it happens
Trigger: A file inside the user scripts directory is a symlink to a file elsewhere (e.g. ln -s ~/.bashrc <user_dir>/x.sh) and delete_user_script("user:x.sh") is called; canonicalize() resolves the link, the target lies outside canonical_user_dir, and the delete is refused. Also fires in the race window if the entry is swapped for a symlink between the exists() check and canonicalization.
Common situations: Users symlinking shared scripts into the scripts dir instead of copying; dotfile-manager cross-links; security tests verifying the guard; TOCTOU attempts.
Related errors
AI-assisted analysis of BigPizzaV3/CodexPlusPlus@1f431ae49b (2026-08-16).
Data as JSON: /api/errors/e95803fc6a6719e6.
Report an issue: GitHub.