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, notView on GitHub (pinned to 71377f2769)
Solutions
- Create the parent directory first: os.MkdirAll(filepath.Dir(beadsDir), 0o755) before calling ForWorkspace.
- Verify the workspace root path spelling — the parent must exist, only the guarded .beads dir itself may be missing.
- Check that no component of the parent is a regular file (ENOTDIR); fix or remove the offending entry.
- 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
- MkdirAll the workspace root before initializing .beads.
- Validate configured workspace paths at startup (stat the parent).
- Remember only the guarded dir may be missing — its parent must exist.
- Watch for ENOTDIR: a file where a directory is expected also triggers this.
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
- dolt path is not executable
- server: NewDoltServer: failed to determine absolute path of
- server: NewDoltServer: failed to determine absolute path of
- server: NewDoltServer: failed to determine absolute path of
- server: NewDoltServer: failed to determine absolute path of
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/836bb600ce8351b8.
Report an issue: GitHub.