gastownhall/beads · error

workspacegate: gate parent %s must exist: %w

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.

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)

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.