gastownhall/beads · error

workspacegate: gate parent

Error message

workspacegate: gate parent %s must exist: %w

What it means

forDir canonicalizes the gate file's PARENT directory with filepath.EvalSymlinks; the parent must exist even though the guarded directory itself may not (bd init gates a workspace it is creating). If EvalSymlinks fails (ENOENT, ENOTDIR, permission), no stable gate location can be computed, so construction fails with the underlying OS error wrapped.

Solutions

  1. Create the parent directory first: os.MkdirAll(filepath.Dir(beadsDir), 0o755) before calling ForWorkspace.
  2. Verify the workspace root path spelling — the parent must exist, only the guarded .beads dir itself may be missing.
  3. Check that no component of the parent is a regular file (ENOTDIR); fix or remove the offending entry.
  4. If permissions are the cause, run as a user with execute/search permission on every ancestor directory.

Example fix

// before
g, err := workspacegate.ForWorkspace(beadsDir)
// after
if err := os.MkdirAll(filepath.Dir(filepath.Clean(beadsDir)), 0o755); err != nil {
    return err
}
g, err := workspacegate.ForWorkspace(beadsDir)
Defensive patterns

Strategy: validation

Validate before calling

func ensureGateParent(dir string) error {
    parent := filepath.Dir(filepath.Clean(dir))
    fi, err := os.Stat(parent)
    if err != nil {
        return fmt.Errorf("gate parent %s: %w", parent, err)
    }
    if !fi.IsDir() {
        return fmt.Errorf("gate parent %s is not a directory", parent)
    }
    return nil
}

Try / catch

g, err := workspacegate.ForWorkspace(beadsDir)
if err != nil {
    var pe *fs.PathError
    if errors.As(err, &pe) && errors.Is(pe.Err, fs.ErrNotExist) {
        return fmt.Errorf("workspace root missing: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling ForWorkspace(beadsDir) or ForPhysicalRoot(root) where the parent directory of the given path does not exist (e.g. ForWorkspace("/repo/.beads") before /repo exists), the parent is actually a file (ENOTDIR), or a permission error blocks resolution.

Common situations: Running bd init in a not-yet-cloned repo path; a typo'd workspace root in config; mounting/renaming the workspace between path computation and gating; running as a user without traverse permission on the parent.

Related errors


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

Appendix: source

Thrown at internal/workspacegate/gate.go:189

	// 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.
//
// Cross-user shared roots are unsupported: the gate file is created 0o600
// (see Acquire), so a second OS user attempting to gate a shared root such
// as ~/.beads/shared-server/dolt hits EACCES on the sibling gate file, not

View on GitHub (pinned to 71377f2769)