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

  1. Strip any trailing separator before calling: dir = filepath.Clean(dir) (Clean removes trailing slashes unless the path is "/").
  2. Pass the concrete .beads directory path (e.g. /repo/.beads), not ".", "..", the repo root's parent, or "/".
  3. If the caller only knows the workspace root, join the .beads name: filepath.Join(root, ".beads") before calling ForWorkspace.
  4. 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

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


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