gastownhall/beads · critical

BEADS_DIR points to unsafe location: %s

Error message

BEADS_DIR points to unsafe location: %s

What it means

As a security check (SEC-003), buildRepoContext rejects a resolved beadsDir that fails isPathInSafeBoundary and returns "BEADS_DIR points to unsafe location: %s". This prevents BEADS_DIR from redirecting beads storage to dangerous locations such as system paths, paths outside the repo boundary, or symlinked escapes. The error names the offending resolved path so you can see exactly what was rejected.

Source

Thrown at internal/beads/context.go:113

func GetRepoContext() (*RepoContext, error) {
	repoCtxOnce.Do(func() {
		repoCtx, repoCtxErr = buildRepoContext()
	})
	return repoCtx, repoCtxErr
}

// buildRepoContext constructs the RepoContext by resolving all paths.
// This is called once per process via sync.Once.
func buildRepoContext() (*RepoContext, error) {
	// 1. Find .beads directory (respects BEADS_DIR env var)
	beadsDir := FindBeadsDir()
	if beadsDir == "" {
		return nil, fmt.Errorf("no .beads directory found")
	}

	// 2. Security: Validate path boundary (SEC-003)
	if !isPathInSafeBoundary(beadsDir) {
		return nil, fmt.Errorf("BEADS_DIR points to unsafe location: %s", beadsDir)
	}

	// 3. Check for redirect file in the local repo
	redirectInfo := GetRedirectInfo()

	// 4. Determine RepoRoot based on external/redirect status
	var repoRoot string
	isExternal := redirectInfo.IsRedirected
	if !isExternal {
		if external, err := isExternalBeadsDir(beadsDir); err == nil {
			isExternal = external
		}
	}

	if isExternal {
		// Beads dir is in a different repo - use that repo's root
		repoRoot = repoRootForBeadsDir(beadsDir)
	} else {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Unset BEADS_DIR (or set it to a real .beads directory inside a repository) and retry.
  2. Resolve the printed path and check for symlinks: use `readlink -f` to see the canonical location.
  3. Point BEADS_DIR at a directory within a repository you control, not system/shared paths.
  4. Audit shell profiles and CI config for stray BEADS_DIR exports.
  5. Restart the process after fixing — the resolved context is cached via sync.Once.

Example fix

// before
export BEADS_DIR=/tmp/shared-beads
bd doctor // "BEADS_DIR points to unsafe location: /tmp/shared-beads"
// after
unset BEADS_DIR
# or: export BEADS_DIR=/path/to/repo/.beads
bd doctor
Defensive patterns

Strategy: validation

Validate before calling

if dir := os.Getenv("BEADS_DIR"); dir != "" {
    abs, _ := filepath.Abs(dir)
    for _, bad := range []string{"/", "/etc", "/usr", "/var", os.Getenv("HOME")} {
        if abs == bad { return fmt.Errorf("BEADS_DIR %s is unsafe", abs) }
    }
}

Try / catch

if _, err := beads.GetRepoContext(); err != nil {
    if strings.Contains(err.Error(), "BEADS_DIR points to unsafe location") {
        return fmt.Errorf("fix or unset BEADS_DIR (%v)", err)
    }
    return err
}

Prevention

When it happens

Trigger: Setting BEADS_DIR to /, /etc, $HOME, /tmp or another sensitive/system path; a BEADS_DIR whose canonical path resolves outside the allowed boundary via symlinks; relative paths that resolve unexpectedly; hostile or corrupted environment inherited from a parent process.

Common situations: Exporting BEADS_DIR globally in a shell profile and later running bd in repos where that path is unsafe; CI environments injecting BEADS_DIR for all jobs; developers experimenting with shared beads directories pointing at system locations.

Related errors


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