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
- Unset BEADS_DIR (or set it to a real .beads directory inside a repository) and retry.
- Resolve the printed path and check for symlinks: use `readlink -f` to see the canonical location.
- Point BEADS_DIR at a directory within a repository you control, not system/shared paths.
- Audit shell profiles and CI config for stray BEADS_DIR exports.
- 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
- Never point BEADS_DIR at system, home, or temp root paths.
- Set BEADS_DIR only per-project (direnv, CI job env), not globally in shell profiles.
- Resolve symlinks before choosing a BEADS_DIR target.
- Audit CI configuration for inherited BEADS_DIR values.
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
- no config.yaml found in BEADS_DIR (%s) (run 'bd init' first)
- server: NewDoltServer: doltBinExec is required
- server: NewDoltServer: rootDir is required
- server: NewDoltServer: configPath is required
- failed to find backup directory: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/1a687d73e64eb4bc.
Report an issue: GitHub.