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

  1. Provide a genuinely workspace-relative path that stays inside root
  2. Clean the input and strip/reject any ".." segments before calling (filepath.Clean alone is not enough)
  3. If the target must be outside the workspace, use an explicit, separately-audited absolute-path API instead of this containment-checked helper
  4. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/4e73d8c520488406. Report an issue: GitHub.