gastownhall/beads · error
workspacegate: probe %s: %w
Error message
workspacegate: probe %s: %w
What it means
ExclusiveHolder probed the gate by opening the file read-only; the open failed with an error other than NotExist (which is treated as 'never gated'). The os.PathError is wrapped so callers can inspect it. This happens before any flock test, so no holder information is returned.
Source
Thrown at internal/workspacegate/gate.go:584
// spurious failures into concurrent fail-fast acquirers.
//
// The error return is non-nil when the state could not be determined
// (unreadable gate file, unsupported filesystem); callers must not treat
// that as "not held".
func (g Gate) ExclusiveHolder() (held bool, info *Info, err error) {
if g.path == "" {
return false, nil, errors.New("workspacegate: zero Gate")
}
// O_RDONLY: this is a read-only probe (flock does not require a
// writable descriptor), and it widens reach — a gate file owned by
// another user with no write permission for us is still probeable.
f, err := os.OpenFile(g.path, os.O_RDONLY, 0o600)
if err != nil {
if os.IsNotExist(err) {
// No gate file: nothing has ever gated here.
return false, nil, nil
}
return false, nil, fmt.Errorf("workspacegate: probe %s: %w", g.path, err)
}
defer f.Close()
if lerr := lockfile.FlockSharedNonBlock(f); lerr == nil {
_ = lockfile.FlockUnlock(f)
return false, nil, nil
} else if !errors.Is(lerr, lockfile.ErrLockBusy) && !lockfile.IsLocked(lerr) {
return false, nil, fmt.Errorf("workspacegate: probe %s: %w", g.path, lerr)
}
return true, g.readInfo(), nil
}
View on GitHub (pinned to 71377f2769)
Solutions
- Run the probe as the same user that created the gate, or chmod the gate file to be group/world readable if cross-user probing is intended.
- Verify the path is a regular file (os.Stat, mode check) and not a directory or broken symlink.
- Handle os.IsNotExist as the normal 'nothing gated' case and only treat other errors as failures.
- Fix path configuration if the gate path was recently moved or renamed.
Example fix
// before
held, _, err := g.ExclusiveHolder(ctx)
// after
if fi, serr := os.Stat(g.Path()); serr == nil && fi.IsDir() {
return fmt.Errorf("gate path is a directory: %s", g.Path())
}
held, _, err := g.ExclusiveHolder(ctx)
if err != nil {
var pe *os.PathError
if errors.As(err, &pe) && errors.Is(pe.Err, os.ErrPermission) {
return fmt.Errorf("gate unreadable by current user: %w", err)
}
} Defensive patterns
Strategy: validation
Validate before calling
fi, err := os.Stat(gatePath)
if os.IsNotExist(err) {
// nothing has ever gated here — safe to proceed
} else if err != nil {
return err
} else if !fi.Mode().IsRegular() {
return fmt.Errorf("gate path is not a regular file: %s", gatePath)
} Try / catch
held, info, err := g.ExclusiveHolder(ctx)
if err != nil {
var pe *os.PathError
if errors.As(err, &pe) && errors.Is(pe.Err, os.ErrPermission) {
// run as same user as gate creator or fix permissions
return fmt.Errorf("gate unreadable: %w", err)
}
return err
} Prevention
- Probe as the same user that created the gate (0o600 default).
- Treat os.IsNotExist as the normal 'no gate' case before reporting failure.
- Verify the path is a regular file, not a directory or symlink loop.
When it happens
Trigger: Calling ExclusiveHolder when os.OpenFile(path, O_RDONLY) fails with e.g. EACCES (no read permission on an existing 0o600 gate file owned by another user), EISDIR (path is a directory), or ELOOP (symlink cycle).
Common situations: Probe run under a different uid than the Acquire process (0o600 file); the gate path was repointed at a directory; permissions changed after an earlier run created the gate.
Related errors
- workspacegate: open gate %s: %w
- workspacegate: lock %s: %w
- failed to remove %s: %w
- creating beads directory: %w
- opening lock file: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/7ab17fd8e804c878.
Report an issue: GitHub.