Hmbown/CodeWhale · error
bundle path is absolute; only paths inside the config…
Error message
bundle path {candidate:?} is absolute; only paths inside the config directory are accepted What it means
A path carried inside a config bundle is absolute. `resolve_bounded_path` only accepts relative paths that stay inside the config directory, so any absolute path (e.g. `/etc/passwd`, `C:\...`) is refused before resolution. This prevents a malicious bundle from writing outside the config directory.
Solutions
- Edit the bundle so the path is relative to the config directory (e.g. `settings/providers.json`).
- Re-export the bundle with the current CLI, which writes only relative bounded paths.
- If generating bundles yourself, strip or relativize any absolute path before embedding it.
Example fix
// before "path": "/home/alice/.codewhale/settings.json" // after "path": "settings.json"
Defensive patterns
Strategy: validation
Validate before calling
fn is_relative(candidate: &str) -> bool {
!std::path::Path::new(candidate).is_absolute()
} Type guard
fn is_bounded_candidate(candidate: &str) -> bool {
let p = std::path::Path::new(candidate);
!p.is_absolute() && !candidate.contains('\0')
} Try / catch
match resolve_bounded_path(&base_dir, candidate) {
Ok(path) => apply(path),
Err(e) if e.to_string().contains("is absolute") => log::warn!("skipped absolute bundle path: {candidate:?}"),
Err(e) => return Err(e),
} Prevention
- Store only paths relative to the config directory inside bundles.
- When exporting from scripts, strip the config-directory prefix from absolute paths.
- Reject absolute paths in any bundle you accept from third parties.
When it happens
Trigger: Importing a bundle whose path section contains an absolute path; passing an absolute `candidate` string to `resolve_bounded_path(base_dir, candidate)`.
Common situations: A bundle exported on another machine that embedded absolute paths; a hand-edited bundle entry; a third-party bundle attempting to escape the config directory.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- bundle path contains a NUL byte; refused
- audited skill path does not match owned package
- bundle path escapes the config directory via a symlink…
- external credential path must be absolute
- external credential path must be absolute and lexically…
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/281dba92b8325af9.
Report an issue: GitHub.
Appendix: source
Thrown at crates/cli/src/config_bundles.rs:718
/// Resolve `candidate` inside `base_dir`, refusing traversal and symlink
/// escapes. Returns the resolved path or an error naming the refusal — the
/// candidate string itself is safe to echo (it is config data, not a secret).
/// Resolve `candidate` inside `base_dir`, refusing traversal and symlink
/// escapes. Returns the joined path or an error naming the refusal.
/// Reserved for path-carrying bundle sections (none shipped yet); exercised
/// by the traversal tests so the contract cannot silently rot.
#[cfg_attr(
not(test),
expect(dead_code, reason = "path-carrying sections land with the next schema")
)]
pub fn resolve_bounded_path(base_dir: &Path, candidate: &str) -> Result<PathBuf> {
if candidate.contains('\0') {
bail!("bundle path contains a NUL byte; refused");
}
let candidate_path = Path::new(candidate);
if candidate_path.is_absolute() {
bail!(
"bundle path {candidate:?} is absolute; only paths inside the config directory are accepted"
);
}
let canonical_base = base_dir
.canonicalize()
.with_context(|| format!("config directory {} is unavailable", base_dir.display()))?;
let joined = base_dir.join(candidate_path);
// Walk the joined path's ancestors from the deepest existing component up:
// every existing component must canonicalize inside the base, so a symlink
// pointing outside the config directory is refused even when the final
// target does not exist yet.
let deepest_existing = joined
.ancestors()
.find(|ancestor| ancestor.symlink_metadata().is_ok())
.context("bundle path has no existing ancestor inside the config directory")?;
let resolved = deepest_existing.canonicalize().with_context(|| {
format!(
"could not resolve bundle path component {}",View on GitHub (pinned to 73e0f67d83)