gastownhall/beads · error
path escapes workspace: %s
Error message
path escapes workspace: %s
What it means
This is the workspace-escape (path traversal) guard in safeWorkspacePath. If the resolved path relative to root is ".." or starts with "../", the requested relPath would land outside the workspace, so it is rejected. This protects doctor fix operations from reading/writing arbitrary files via inputs like "../../etc/passwd".
Source
Thrown at cmd/bd/doctor/fix/common.go:121
func safeWorkspacePath(root, relPath string) (string, error) {
absRoot, err := filepath.Abs(root)
if err != nil {
return "", fmt.Errorf("invalid workspace path: %w", err)
}
cleanRel := filepath.Clean(relPath)
if filepath.IsAbs(cleanRel) {
return "", fmt.Errorf("expected relative path, got absolute: %s", relPath)
}
joined := filepath.Join(absRoot, cleanRel)
rel, err := filepath.Rel(absRoot, joined)
if err != nil {
return "", fmt.Errorf("failed to resolve path: %w", err)
}
if rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
return "", fmt.Errorf("path escapes workspace: %s", relPath)
}
return joined, nil
}
// isWithinWorkspace reports whether candidate resides within the workspace root.
func isWithinWorkspace(root, candidate string) bool {
cleanRoot, err := filepath.Abs(root)
if err != nil {
return false
}
cleanCandidate := filepath.Clean(candidate)
rel, err := filepath.Rel(cleanRoot, cleanCandidate)
if err != nil {
return false
}
return rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator)))
}View on GitHub (pinned to 71377f2769)
Solutions
- Provide a genuinely workspace-relative path that stays inside root
- Clean the input and strip/reject any ".." segments before calling (filepath.Clean alone is not enough)
- If the target must be outside the workspace, use an explicit, separately-audited absolute-path API instead of this containment-checked helper
- If this fires on legitimate input, check for symlinks in root that resolve outside the workspace (Join/Rel is lexical, not symlink-aware)
Example fix
// before safeWorkspacePath(root, "../../etc/passwd") // error // after safeWorkspacePath(root, filepath.Clean(strings.TrimPrefix(input, root+"/"))) // or reject input containing ".."
Defensive patterns
Strategy: validation
Validate before calling
clean := filepath.Clean(input)
if strings.Contains(clean, "..") || filepath.IsAbs(clean) {
return fmt.Errorf("unsafe path: %s", input)
} Type guard
func isInsideWorkspace(root, candidate string) bool {
rel, err := filepath.Rel(root, filepath.Clean(candidate))
if err != nil { return false }
return rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator)))
} Try / catch
p, err := safeWorkspacePath(root, rel)
if err != nil {
if strings.HasPrefix(err.Error(), "path escapes workspace") {
return fmt.Errorf("refusing unsafe path %q", rel)
}
return err
} Prevention
- Sanitize any externally sourced filename: reject ".." segments early
- Remember Clean/Join is lexical — check symlink targets with filepath.EvalSymlinks for adversarial input
- Route all file access inside the workspace through safeWorkspacePath instead of filepath.Join
- Log rejected traversal attempts for security review
When it happens
Trigger: Calling safeWorkspacePath with relPath containing traversal segments, e.g. "../secrets", "a/../../b", or any input whose Join+Rel result escapes root.
Common situations: Hostile or corrupted input (user data, issue text, config values) used as a filename; a caller assuming symlinks or ".." segments are acceptable; migrating code that previously used filepath.Join directly without containment checks.
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
- unsafe path %q in porcelain status
- BEADS_DIR points to unsafe location: %s
- remote URL cannot be empty
- remote URL contains control character at position %d (0x%02x
- remote URL must not start with a dash
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/4e73d8c520488406.
Report an issue: GitHub.