gastownhall/beads · error
workspacegate: cannot gate %q: the guarded path must be a na
Error message
workspacegate: cannot gate %q: the guarded path must be a named directory
What it means
forDir refuses to build a gate for a path whose final element is empty, ".", or "..". Gate identity derives from the canonicalized parent plus the literal base name, so a path with no named base component has no stable gate file name and cannot be fenced. This is a programming/caller error, not a transient condition.
Source
Thrown at internal/workspacegate/gate.go:185
abs = filepath.Clean(abs)
// Gate identity must be stable across the guarded directory being
// absent, created, replaced, or recreated — that is the point of
// placing the gate beside it. So identity derives from the
// canonicalized PARENT plus the literal base name, never from
// resolving the guarded path itself: full-path resolution would
// silently select a different gate once the directory appears as a
// symlink, letting two exclusive holders coexist. The flip side is
// that a guarded directory that IS a symlink has no stable identity
// under this scheme, so it is refused outright rather than gated
// ambiguously.
if fi, err := os.Lstat(abs); err == nil && fi.Mode()&os.ModeSymlink != 0 {
return Gate{}, fmt.Errorf("workspacegate: %s is a symlink; gate the physical directory it points to", abs)
}
parent, base := filepath.Split(abs)
switch base {
case "", ".", "..":
return Gate{}, fmt.Errorf("workspacegate: cannot gate %q: the guarded path must be a named directory", dir)
}
canonParent, err := filepath.EvalSymlinks(filepath.Clean(parent))
if err != nil {
return Gate{}, fmt.Errorf("workspacegate: gate parent %s must exist: %w", parent, err)
}
return Gate{path: filepath.Join(canonParent, gateFileName(base))}, nil
}
// ForWorkspace returns the gate guarding a workspace's .beads directory
// (pass the .beads directory itself). The gate file is a sibling of
// .beads; bd's project gitignore management covers "*.gate.lock*".
func ForWorkspace(beadsDir string) (Gate, error) { return forDir(beadsDir) }
// ForPhysicalRoot returns the gate guarding a physical database root (a
// dolt server root such as .beads/dolt or ~/.beads/shared-server/dolt).
// Distinct workspaces that point at the same physical root resolve to the
// same gate file, which is the point: a workspace-level gate alone cannot
// stop workspace B from restarting the server workspace A is draining.View on GitHub (pinned to 71377f2769)
Solutions
- Strip any trailing separator before calling: dir = filepath.Clean(dir) (Clean removes trailing slashes unless the path is "/").
- Pass the concrete .beads directory path (e.g. /repo/.beads), not ".", "..", the repo root's parent, or "/".
- If the caller only knows the workspace root, join the .beads name: filepath.Join(root, ".beads") before calling ForWorkspace.
- For "/" or a drive root there is no valid guard — reject the input upstream instead of gating it.
Example fix
// before g, err := workspacegate.ForWorkspace(cfg.BeadsDir + "/") // after g, err := workspacegate.ForWorkspace(filepath.Clean(cfg.BeadsDir))
Defensive patterns
Strategy: validation
Validate before calling
func validateGatePath(dir string) error {
abs, err := filepath.Abs(dir)
if err != nil { return err }
abs = filepath.Clean(abs)
base := filepath.Base(abs)
if base == "." || base == ".." || abs == "/" {
return fmt.Errorf("cannot gate %q: need a named directory", dir)
}
return nil
} Type guard
func isNamedDirPath(dir string) bool {
base := filepath.Base(filepath.Clean(dir))
return base != "." && base != ".." && base != "/" && base != string(filepath.Separator)
} Prevention
- Always filepath.Clean() user- or config-supplied paths before gating.
- Never append a trailing separator to a directory path in Go code.
- Pass the .beads directory itself, never ".", "..", or a volume root.
- Reject root paths ("/", drive roots) in config parsing before they reach the gate.
When it happens
Trigger: Calling ForWorkspace or ForPhysicalRoot with a path ending in a separator (e.g. "/repo/.beads/"), with "." or ".." as the last element, or with the filesystem root "/" — after filepath.Abs+Clean such inputs reduce to an empty or dot/dotdot base.
Common situations: Concatenating dir + "/" before passing a .beads path; user-supplied config values like beadsDir=".." or "."; computing a path from os.Getwd() at the volume root; shell-style paths copied with trailing slashes into code or config.
Related errors
- expected relative path, got absolute: %s
- newExternalProxiedServerUOWProvider: proxied server root (fr
- newProxiedServerUOWProvider: proxied server root (from env o
- BEADS_DIR points to unsafe location: %s
- workspacegate: gate parent %s must exist: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/7b861685e3f92301.
Report an issue: GitHub.